Update docs, MCP server, and skills for v0.3.0 features
- Add skill docs for batch ops, pagination, soft delete, transactions - Update sqlx-entity.md with new attributes and methods - Update sqlx-record.md with quick reference for all features - Update MCP server with new feature documentation resources - Fix .gitignore paths for renamed directories Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
f785bb1bf6
commit
44ac78d67e
|
|
@ -0,0 +1,196 @@
|
||||||
|
# sqlx-record Batch Operations Skill
|
||||||
|
|
||||||
|
Guide to insert_many() and upsert() for efficient bulk operations.
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
- "batch insert", "bulk insert"
|
||||||
|
- "insert many", "insert_many"
|
||||||
|
- "upsert", "insert or update"
|
||||||
|
- "on conflict", "on duplicate key"
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
`sqlx-record` provides efficient batch operations:
|
||||||
|
- `insert_many()` - Insert multiple records in a single query
|
||||||
|
- `upsert()` - Insert or update on primary key conflict
|
||||||
|
|
||||||
|
## insert_many()
|
||||||
|
|
||||||
|
Insert multiple entities in a single SQL statement:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub async fn insert_many(executor, entities: &[Self]) -> Result<Vec<PkType>, Error>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use sqlx_record::prelude::*;
|
||||||
|
|
||||||
|
let users = vec![
|
||||||
|
User { id: new_uuid(), name: "Alice".into(), email: "alice@example.com".into() },
|
||||||
|
User { id: new_uuid(), name: "Bob".into(), email: "bob@example.com".into() },
|
||||||
|
User { id: new_uuid(), name: "Carol".into(), email: "carol@example.com".into() },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Insert all in single query
|
||||||
|
let ids = User::insert_many(&pool, &users).await?;
|
||||||
|
|
||||||
|
println!("Inserted {} users", ids.len());
|
||||||
|
```
|
||||||
|
|
||||||
|
### SQL Generated
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- MySQL
|
||||||
|
INSERT INTO users (id, name, email) VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?)
|
||||||
|
|
||||||
|
-- PostgreSQL
|
||||||
|
INSERT INTO users (id, name, email) VALUES ($1, $2, $3), ($4, $5, $6), ($7, $8, $9)
|
||||||
|
|
||||||
|
-- SQLite
|
||||||
|
INSERT INTO users (id, name, email) VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Benefits
|
||||||
|
|
||||||
|
- Single round-trip to database
|
||||||
|
- Much faster than N individual inserts
|
||||||
|
- Atomic - all succeed or all fail
|
||||||
|
|
||||||
|
### Limitations
|
||||||
|
|
||||||
|
- Entity must implement `Clone` (for collecting PKs)
|
||||||
|
- Empty slice returns empty vec without database call
|
||||||
|
- Very large batches may hit database limits (split into chunks if needed)
|
||||||
|
|
||||||
|
### Chunked Insert
|
||||||
|
|
||||||
|
For very large datasets:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
const BATCH_SIZE: usize = 1000;
|
||||||
|
|
||||||
|
async fn insert_large_dataset(pool: &Pool, users: Vec<User>) -> Result<Vec<Uuid>, sqlx::Error> {
|
||||||
|
let mut all_ids = Vec::with_capacity(users.len());
|
||||||
|
|
||||||
|
for chunk in users.chunks(BATCH_SIZE) {
|
||||||
|
let ids = User::insert_many(pool, chunk).await?;
|
||||||
|
all_ids.extend(ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(all_ids)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## upsert() / insert_or_update()
|
||||||
|
|
||||||
|
Insert a new record, or update if primary key already exists:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub async fn upsert(&self, executor) -> Result<PkType, Error>
|
||||||
|
pub async fn insert_or_update(&self, executor) -> Result<PkType, Error> // alias
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let user = User {
|
||||||
|
id: existing_or_new_id,
|
||||||
|
name: "Alice".into(),
|
||||||
|
email: "alice@example.com".into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Insert if new, update if exists
|
||||||
|
user.upsert(&pool).await?;
|
||||||
|
|
||||||
|
// Or using alias
|
||||||
|
user.insert_or_update(&pool).await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
### SQL Generated
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- MySQL
|
||||||
|
INSERT INTO users (id, name, email) VALUES (?, ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE name = VALUES(name), email = VALUES(email)
|
||||||
|
|
||||||
|
-- PostgreSQL
|
||||||
|
INSERT INTO users (id, name, email) VALUES ($1, $2, $3)
|
||||||
|
ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, email = EXCLUDED.email
|
||||||
|
|
||||||
|
-- SQLite
|
||||||
|
INSERT INTO users (id, name, email) VALUES (?, ?, ?)
|
||||||
|
ON CONFLICT(id) DO UPDATE SET name = excluded.name, email = excluded.email
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use Cases
|
||||||
|
|
||||||
|
1. **Sync external data**: Import data that may already exist
|
||||||
|
2. **Idempotent operations**: Safe to retry without duplicates
|
||||||
|
3. **Cache refresh**: Update cached records atomically
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
#### Sync Products
|
||||||
|
|
||||||
|
```rust
|
||||||
|
async fn sync_products(pool: &Pool, external_products: Vec<ExternalProduct>) -> Result<(), sqlx::Error> {
|
||||||
|
for ext in external_products {
|
||||||
|
let product = Product {
|
||||||
|
id: ext.id, // Use external ID as PK
|
||||||
|
name: ext.name,
|
||||||
|
price: ext.price,
|
||||||
|
updated_at: chrono::Utc::now().timestamp_millis(),
|
||||||
|
};
|
||||||
|
product.upsert(pool).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Idempotent Event Processing
|
||||||
|
|
||||||
|
```rust
|
||||||
|
async fn process_event(pool: &Pool, event: Event) -> Result<(), sqlx::Error> {
|
||||||
|
let record = ProcessedEvent {
|
||||||
|
id: event.id, // Event ID as PK - prevents duplicates
|
||||||
|
event_type: event.event_type,
|
||||||
|
payload: event.payload,
|
||||||
|
processed_at: chrono::Utc::now().timestamp_millis(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Safe to call multiple times - won't create duplicates
|
||||||
|
record.upsert(pool).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### With Transaction
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use sqlx_record::transaction;
|
||||||
|
|
||||||
|
transaction!(&pool, |tx| {
|
||||||
|
// Upsert multiple records atomically
|
||||||
|
for item in items {
|
||||||
|
item.upsert(&mut *tx).await?;
|
||||||
|
}
|
||||||
|
Ok::<_, sqlx::Error>(())
|
||||||
|
}).await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Comparison
|
||||||
|
|
||||||
|
| Operation | Behavior on Existing PK | SQL Efficiency |
|
||||||
|
|-----------|------------------------|----------------|
|
||||||
|
| `insert()` | Error (duplicate key) | Single row |
|
||||||
|
| `insert_many()` | Error (duplicate key) | Multiple rows, single query |
|
||||||
|
| `upsert()` | Updates all non-PK fields | Single row |
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- `upsert()` updates ALL non-PK fields, not just changed ones
|
||||||
|
- Primary key must be properly indexed (usually automatic)
|
||||||
|
- For partial updates, use `insert()` + `update_by_id()` with conflict check
|
||||||
|
- `insert_many()` requires all entities have unique PKs among themselves
|
||||||
|
|
@ -56,11 +56,45 @@ large_count: i64,
|
||||||
- SQLx type hint for compile-time validation
|
- SQLx type hint for compile-time validation
|
||||||
- Adds type annotation in SELECT: `field as "field: TYPE"`
|
- Adds type annotation in SELECT: `field as "field: TYPE"`
|
||||||
|
|
||||||
|
### #[soft_delete]
|
||||||
|
```rust
|
||||||
|
#[soft_delete]
|
||||||
|
is_deleted: bool,
|
||||||
|
```
|
||||||
|
- Enables soft delete functionality
|
||||||
|
- Generates `delete()`, `restore()`, `hard_delete()` methods
|
||||||
|
- Field must be `bool` type
|
||||||
|
|
||||||
|
### #[created_at]
|
||||||
|
```rust
|
||||||
|
#[created_at]
|
||||||
|
created_at: i64,
|
||||||
|
```
|
||||||
|
- Auto-set to current timestamp (milliseconds) on insert
|
||||||
|
- Field must be `i64` type
|
||||||
|
- Excluded from UpdateForm
|
||||||
|
|
||||||
|
### #[updated_at]
|
||||||
|
```rust
|
||||||
|
#[updated_at]
|
||||||
|
updated_at: i64,
|
||||||
|
```
|
||||||
|
- Auto-set to current timestamp (milliseconds) on every update
|
||||||
|
- Field must be `i64` type
|
||||||
|
- Excluded from UpdateForm
|
||||||
|
|
||||||
## Generated Methods
|
## Generated Methods
|
||||||
|
|
||||||
### Insert
|
### Insert
|
||||||
```rust
|
```rust
|
||||||
pub async fn insert<E>(&self, executor: E) -> Result<PkType, sqlx::Error>
|
pub async fn insert<E>(&self, executor: E) -> Result<PkType, sqlx::Error>
|
||||||
|
|
||||||
|
// Batch insert
|
||||||
|
pub async fn insert_many(executor, entities: &[Self]) -> Result<Vec<PkType>, Error>
|
||||||
|
|
||||||
|
// Insert or update on conflict
|
||||||
|
pub async fn upsert(&self, executor) -> Result<PkType, Error>
|
||||||
|
pub async fn insert_or_update(&self, executor) -> Result<PkType, Error> // alias
|
||||||
```
|
```
|
||||||
|
|
||||||
### Get Methods
|
### Get Methods
|
||||||
|
|
@ -102,6 +136,23 @@ pub async fn find_ordered_with_limit(
|
||||||
|
|
||||||
// Count matching
|
// Count matching
|
||||||
pub async fn count(executor, filters: Vec<Filter>, index: Option<&str>) -> Result<u64, Error>
|
pub async fn count(executor, filters: Vec<Filter>, index: Option<&str>) -> Result<u64, Error>
|
||||||
|
|
||||||
|
// Paginated results
|
||||||
|
pub async fn paginate(
|
||||||
|
executor,
|
||||||
|
filters: Vec<Filter>,
|
||||||
|
index: Option<&str>,
|
||||||
|
order_by: Vec<(&str, bool)>,
|
||||||
|
page_request: PageRequest
|
||||||
|
) -> Result<Page<Self>, Error>
|
||||||
|
|
||||||
|
// Select specific columns only
|
||||||
|
pub async fn find_partial(
|
||||||
|
executor,
|
||||||
|
select_fields: &[&str],
|
||||||
|
filters: Vec<Filter>,
|
||||||
|
index: Option<&str>
|
||||||
|
) -> Result<Vec<Row>, Error>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Update Methods
|
### Update Methods
|
||||||
|
|
@ -143,6 +194,24 @@ pub async fn get_version(executor, pk: &PkType) -> Result<Option<VersionType>, E
|
||||||
pub async fn get_versions(executor, pks: &[PkType]) -> Result<HashMap<PkType, VersionType>, Error>
|
pub async fn get_versions(executor, pks: &[PkType]) -> Result<HashMap<PkType, VersionType>, Error>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Soft Delete Methods (if #[soft_delete] exists)
|
||||||
|
```rust
|
||||||
|
// Soft delete - sets field to true
|
||||||
|
pub async fn delete(&self, executor) -> Result<(), Error>
|
||||||
|
pub async fn delete_by_id(executor, id: &Uuid) -> Result<(), Error>
|
||||||
|
|
||||||
|
// Hard delete - permanently removes row
|
||||||
|
pub async fn hard_delete(&self, executor) -> Result<(), Error>
|
||||||
|
pub async fn hard_delete_by_id(executor, id: &Uuid) -> Result<(), Error>
|
||||||
|
|
||||||
|
// Restore - sets field to false
|
||||||
|
pub async fn restore(&self, executor) -> Result<(), Error>
|
||||||
|
pub async fn restore_by_id(executor, id: &Uuid) -> Result<(), Error>
|
||||||
|
|
||||||
|
// Get field name
|
||||||
|
pub const fn soft_delete_field() -> &'static str
|
||||||
|
```
|
||||||
|
|
||||||
### Metadata Methods
|
### Metadata Methods
|
||||||
```rust
|
```rust
|
||||||
pub const fn table_name() -> &'static str
|
pub const fn table_name() -> &'static str
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,164 @@
|
||||||
|
# sqlx-record Pagination Skill
|
||||||
|
|
||||||
|
Guide to pagination with Page<T> and PageRequest.
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
- "pagination", "paginate"
|
||||||
|
- "page request", "page size"
|
||||||
|
- "total pages", "has next"
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
`sqlx-record` provides built-in pagination support with the `Page<T>` container and `PageRequest` options.
|
||||||
|
|
||||||
|
## PageRequest
|
||||||
|
|
||||||
|
Create pagination options with 1-indexed page numbers:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use sqlx_record::prelude::PageRequest;
|
||||||
|
|
||||||
|
// Create request for page 1 with 20 items per page
|
||||||
|
let request = PageRequest::new(1, 20);
|
||||||
|
|
||||||
|
// First page shorthand
|
||||||
|
let request = PageRequest::first(20);
|
||||||
|
|
||||||
|
// Access offset/limit for manual queries
|
||||||
|
request.offset() // 0 for page 1, 20 for page 2, etc.
|
||||||
|
request.limit() // page_size
|
||||||
|
```
|
||||||
|
|
||||||
|
## Page<T>
|
||||||
|
|
||||||
|
Paginated results container:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use sqlx_record::prelude::Page;
|
||||||
|
|
||||||
|
// Properties
|
||||||
|
page.items // Vec<T> - items for this page
|
||||||
|
page.total_count // u64 - total records matching filters
|
||||||
|
page.page // u32 - current page (1-indexed)
|
||||||
|
page.page_size // u32 - items per page
|
||||||
|
|
||||||
|
// Computed methods
|
||||||
|
page.total_pages() // u32 - ceil(total_count / page_size)
|
||||||
|
page.has_next() // bool - page < total_pages
|
||||||
|
page.has_prev() // bool - page > 1
|
||||||
|
page.is_empty() // bool - items.is_empty()
|
||||||
|
page.len() // usize - items.len()
|
||||||
|
|
||||||
|
// Transformation
|
||||||
|
page.map(|item| transform(item)) // Page<U>
|
||||||
|
page.into_items() // Vec<T>
|
||||||
|
page.iter() // impl Iterator<Item = &T>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Entity Paginate Method
|
||||||
|
|
||||||
|
Generated on all entities:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub async fn paginate(
|
||||||
|
executor,
|
||||||
|
filters: Vec<Filter>,
|
||||||
|
index: Option<&str>, // MySQL index hint
|
||||||
|
order_by: Vec<(&str, bool)>, // (field, is_ascending)
|
||||||
|
page_request: PageRequest
|
||||||
|
) -> Result<Page<Self>, Error>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Basic Pagination
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use sqlx_record::prelude::*;
|
||||||
|
|
||||||
|
// Get first page of 20 users
|
||||||
|
let page = User::paginate(
|
||||||
|
&pool,
|
||||||
|
filters![],
|
||||||
|
None,
|
||||||
|
vec![("created_at", false)], // ORDER BY created_at DESC
|
||||||
|
PageRequest::new(1, 20)
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
println!("Page {} of {}", page.page, page.total_pages());
|
||||||
|
println!("Showing {} of {} users", page.len(), page.total_count);
|
||||||
|
|
||||||
|
for user in page.iter() {
|
||||||
|
println!("{}: {}", user.id, user.name);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Filters
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Active users only, page 3
|
||||||
|
let page = User::paginate(
|
||||||
|
&pool,
|
||||||
|
filters![("is_active", true)],
|
||||||
|
None,
|
||||||
|
vec![("name", true)], // ORDER BY name ASC
|
||||||
|
PageRequest::new(3, 10)
|
||||||
|
).await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Index Hint (MySQL)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Use specific index for performance
|
||||||
|
let page = User::paginate(
|
||||||
|
&pool,
|
||||||
|
filters![("status", "active")],
|
||||||
|
Some("idx_users_status"), // MySQL: USE INDEX(idx_users_status)
|
||||||
|
vec![("created_at", false)],
|
||||||
|
PageRequest::new(1, 50)
|
||||||
|
).await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Navigation Logic
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let page = User::paginate(&pool, filters![], None, vec![], PageRequest::new(current, 20)).await?;
|
||||||
|
|
||||||
|
if page.has_prev() {
|
||||||
|
println!("Previous: page {}", page.page - 1);
|
||||||
|
}
|
||||||
|
if page.has_next() {
|
||||||
|
println!("Next: page {}", page.page + 1);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transform Results
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Convert to DTOs
|
||||||
|
let dto_page: Page<UserDto> = page.map(|user| UserDto::from(user));
|
||||||
|
|
||||||
|
// Or consume items
|
||||||
|
let items: Vec<User> = page.into_items();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Comparison with Manual Pagination
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Manual approach (still available)
|
||||||
|
let offset = (page_num - 1) * page_size;
|
||||||
|
let items = User::find_ordered_with_limit(
|
||||||
|
&pool, filters, None, order_by, Some((offset, page_size))
|
||||||
|
).await?;
|
||||||
|
let total = User::count(&pool, filters.clone(), None).await?;
|
||||||
|
|
||||||
|
// With paginate() - simpler
|
||||||
|
let page = User::paginate(&pool, filters, None, order_by, PageRequest::new(page_num, page_size)).await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Page numbers are 1-indexed (page 1 is first page)
|
||||||
|
- `paginate()` executes two queries: count + select
|
||||||
|
- For very large tables, consider cursor-based pagination instead
|
||||||
|
- Index hints only work on MySQL, ignored on Postgres/SQLite
|
||||||
|
|
@ -117,11 +117,68 @@ let id = new_uuid(); // Timestamp prefix for better indexing
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[dependencies]
|
[dependencies]
|
||||||
sqlx-record = { version = "0.2", features = ["mysql", "derive"] }
|
sqlx-record = { version = "0.3", features = ["mysql", "derive"] }
|
||||||
# Database: "mysql", "postgres", or "sqlite" (pick one)
|
# Database: "mysql", "postgres", or "sqlite" (pick one)
|
||||||
# Optional: "derive", "static-validation"
|
# Optional: "derive", "static-validation"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Soft Delete, Timestamps, Batch Operations
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Entity, FromRow)]
|
||||||
|
struct User {
|
||||||
|
#[primary_key] id: Uuid,
|
||||||
|
name: String,
|
||||||
|
|
||||||
|
#[soft_delete] // Enables delete/restore/hard_delete
|
||||||
|
is_deleted: bool,
|
||||||
|
|
||||||
|
#[created_at] // Auto-set on insert
|
||||||
|
created_at: i64,
|
||||||
|
|
||||||
|
#[updated_at] // Auto-set on update
|
||||||
|
updated_at: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Soft delete
|
||||||
|
user.delete(&pool).await?; // is_deleted = true
|
||||||
|
user.restore(&pool).await?; // is_deleted = false
|
||||||
|
user.hard_delete(&pool).await?; // DELETE FROM
|
||||||
|
|
||||||
|
// Batch insert
|
||||||
|
User::insert_many(&pool, &users).await?;
|
||||||
|
|
||||||
|
// Upsert (insert or update on conflict)
|
||||||
|
user.upsert(&pool).await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pagination
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use sqlx_record::prelude::{Page, PageRequest};
|
||||||
|
|
||||||
|
let page = User::paginate(&pool, filters![], None,
|
||||||
|
vec![("name", true)], PageRequest::new(1, 20)).await?;
|
||||||
|
|
||||||
|
page.items // Vec<User>
|
||||||
|
page.total_count // Total matching records
|
||||||
|
page.total_pages() // Calculated pages
|
||||||
|
page.has_next() // bool
|
||||||
|
page.has_prev() // bool
|
||||||
|
```
|
||||||
|
|
||||||
|
## Transaction Helper
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use sqlx_record::transaction;
|
||||||
|
|
||||||
|
transaction!(&pool, |tx| {
|
||||||
|
user.insert(&mut *tx).await?;
|
||||||
|
order.insert(&mut *tx).await?;
|
||||||
|
Ok::<_, sqlx::Error>(())
|
||||||
|
}).await?;
|
||||||
|
```
|
||||||
|
|
||||||
## Advanced Updates (UpdateExpr)
|
## Advanced Updates (UpdateExpr)
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,241 @@
|
||||||
|
# sqlx-record Soft Delete Skill
|
||||||
|
|
||||||
|
Guide to soft delete functionality with #[soft_delete] attribute.
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
- "soft delete", "soft-delete"
|
||||||
|
- "is_deleted", "deleted"
|
||||||
|
- "restore", "undelete"
|
||||||
|
- "hard delete", "permanent delete"
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Soft delete allows marking records as deleted without removing them from the database. This enables:
|
||||||
|
- Recovery of accidentally deleted data
|
||||||
|
- Audit trails of deletions
|
||||||
|
- Referential integrity preservation
|
||||||
|
|
||||||
|
## Enabling Soft Delete
|
||||||
|
|
||||||
|
Add `#[soft_delete]` to a boolean field:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use sqlx_record::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Entity, FromRow)]
|
||||||
|
#[table_name = "users"]
|
||||||
|
struct User {
|
||||||
|
#[primary_key]
|
||||||
|
id: Uuid,
|
||||||
|
name: String,
|
||||||
|
|
||||||
|
#[soft_delete]
|
||||||
|
is_deleted: bool, // Must be bool type
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Auto-detection: Fields named `is_deleted` or `deleted` with `bool` type are automatically treated as soft delete fields even without the attribute.
|
||||||
|
|
||||||
|
## Generated Methods
|
||||||
|
|
||||||
|
### delete() / delete_by_{pk}()
|
||||||
|
Sets the soft delete field to `true`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Instance method
|
||||||
|
user.delete(&pool).await?;
|
||||||
|
|
||||||
|
// Static method by primary key
|
||||||
|
User::delete_by_id(&pool, &user_id).await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
**SQL generated:**
|
||||||
|
```sql
|
||||||
|
UPDATE users SET is_deleted = TRUE WHERE id = ?
|
||||||
|
```
|
||||||
|
|
||||||
|
### hard_delete() / hard_delete_by_{pk}()
|
||||||
|
Permanently removes the row:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Instance method
|
||||||
|
user.hard_delete(&pool).await?;
|
||||||
|
|
||||||
|
// Static method by primary key
|
||||||
|
User::hard_delete_by_id(&pool, &user_id).await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
**SQL generated:**
|
||||||
|
```sql
|
||||||
|
DELETE FROM users WHERE id = ?
|
||||||
|
```
|
||||||
|
|
||||||
|
### restore() / restore_by_{pk}()
|
||||||
|
Sets the soft delete field to `false`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Instance method
|
||||||
|
user.restore(&pool).await?;
|
||||||
|
|
||||||
|
// Static method by primary key
|
||||||
|
User::restore_by_id(&pool, &user_id).await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
**SQL generated:**
|
||||||
|
```sql
|
||||||
|
UPDATE users SET is_deleted = FALSE WHERE id = ?
|
||||||
|
```
|
||||||
|
|
||||||
|
### soft_delete_field()
|
||||||
|
Returns the field name:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let field = User::soft_delete_field(); // "is_deleted"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Filtering Deleted Records
|
||||||
|
|
||||||
|
Soft delete does **NOT** automatically filter `find()` queries. You must add the filter manually:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Include only non-deleted
|
||||||
|
let users = User::find(&pool, filters![("is_deleted", false)], None).await?;
|
||||||
|
|
||||||
|
// Include only deleted (trash view)
|
||||||
|
let deleted = User::find(&pool, filters![("is_deleted", true)], None).await?;
|
||||||
|
|
||||||
|
// Include all records
|
||||||
|
let all = User::find(&pool, filters![], None).await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Helper Pattern
|
||||||
|
|
||||||
|
Create a helper function for consistent filtering:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
impl User {
|
||||||
|
pub async fn find_active(
|
||||||
|
pool: &Pool,
|
||||||
|
mut filters: Vec<Filter<'_>>,
|
||||||
|
index: Option<&str>
|
||||||
|
) -> Result<Vec<Self>, sqlx::Error> {
|
||||||
|
filters.push(Filter::Equal("is_deleted", false.into()));
|
||||||
|
Self::find(pool, filters, index).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
let users = User::find_active(&pool, filters![("role", "admin")], None).await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Basic Soft Delete Flow
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Create user
|
||||||
|
let user = User {
|
||||||
|
id: new_uuid(),
|
||||||
|
name: "Alice".into(),
|
||||||
|
is_deleted: false,
|
||||||
|
};
|
||||||
|
user.insert(&pool).await?;
|
||||||
|
|
||||||
|
// Soft delete
|
||||||
|
user.delete(&pool).await?;
|
||||||
|
// user still exists in DB with is_deleted = true
|
||||||
|
|
||||||
|
// Find won't return deleted users (with proper filter)
|
||||||
|
let users = User::find(&pool, filters![("is_deleted", false)], None).await?;
|
||||||
|
// Alice not in results
|
||||||
|
|
||||||
|
// Restore
|
||||||
|
User::restore_by_id(&pool, &user.id).await?;
|
||||||
|
// user.is_deleted = false again
|
||||||
|
|
||||||
|
// Hard delete (permanent)
|
||||||
|
User::hard_delete_by_id(&pool, &user.id).await?;
|
||||||
|
// Row completely removed from database
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Audit Trail
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use sqlx_record::{transaction, prelude::*};
|
||||||
|
|
||||||
|
async fn soft_delete_with_audit(
|
||||||
|
pool: &Pool,
|
||||||
|
user_id: &Uuid,
|
||||||
|
actor_id: &Uuid
|
||||||
|
) -> Result<(), sqlx::Error> {
|
||||||
|
transaction!(&pool, |tx| {
|
||||||
|
// Soft delete the user
|
||||||
|
User::delete_by_id(&mut *tx, user_id).await?;
|
||||||
|
|
||||||
|
// Record the deletion
|
||||||
|
let change = EntityChange {
|
||||||
|
id: new_uuid(),
|
||||||
|
entity_id: *user_id,
|
||||||
|
action: "delete".into(),
|
||||||
|
changed_at: chrono::Utc::now().timestamp_millis(),
|
||||||
|
actor_id: *actor_id,
|
||||||
|
session_id: Uuid::nil(),
|
||||||
|
change_set_id: Uuid::nil(),
|
||||||
|
new_value: None,
|
||||||
|
};
|
||||||
|
create_entity_change(&mut *tx, "entity_changes_users", &change).await?;
|
||||||
|
|
||||||
|
Ok::<_, sqlx::Error>(())
|
||||||
|
}).await
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cascade Soft Delete
|
||||||
|
|
||||||
|
```rust
|
||||||
|
async fn delete_user_cascade(pool: &Pool, user_id: &Uuid) -> Result<(), sqlx::Error> {
|
||||||
|
transaction!(&pool, |tx| {
|
||||||
|
// Soft delete user's orders
|
||||||
|
let orders = Order::find(&mut *tx, filters![("user_id", user_id)], None).await?;
|
||||||
|
for order in orders {
|
||||||
|
order.delete(&mut *tx).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Soft delete user
|
||||||
|
User::delete_by_id(&mut *tx, user_id).await?;
|
||||||
|
|
||||||
|
Ok::<_, sqlx::Error>(())
|
||||||
|
}).await
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
Recommended column definition:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- MySQL
|
||||||
|
is_deleted BOOLEAN NOT NULL DEFAULT FALSE
|
||||||
|
|
||||||
|
-- PostgreSQL
|
||||||
|
is_deleted BOOLEAN NOT NULL DEFAULT FALSE
|
||||||
|
|
||||||
|
-- SQLite
|
||||||
|
is_deleted INTEGER NOT NULL DEFAULT 0 -- 0=false, 1=true
|
||||||
|
```
|
||||||
|
|
||||||
|
Add an index for efficient filtering:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_users_is_deleted ON users (is_deleted);
|
||||||
|
|
||||||
|
-- Or composite index for common queries
|
||||||
|
CREATE INDEX idx_users_active_name ON users (is_deleted, name);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Soft delete field must be `bool` type
|
||||||
|
- The field is included in UpdateForm (can be manually toggled)
|
||||||
|
- Consider adding `deleted_at: Option<i64>` for deletion timestamps
|
||||||
|
- For complex filtering, consider database views
|
||||||
|
|
@ -0,0 +1,209 @@
|
||||||
|
# sqlx-record Transaction Skill
|
||||||
|
|
||||||
|
Guide to the transaction! macro for ergonomic transactions.
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
- "transaction", "transactions"
|
||||||
|
- "commit", "rollback"
|
||||||
|
- "atomic", "transactional"
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The `transaction!` macro provides ergonomic transaction handling with automatic commit on success and rollback on error.
|
||||||
|
|
||||||
|
## Basic Syntax
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use sqlx_record::transaction;
|
||||||
|
|
||||||
|
let result = transaction!(&pool, |tx| {
|
||||||
|
// Operations using &mut *tx as executor
|
||||||
|
user.insert(&mut *tx).await?;
|
||||||
|
order.insert(&mut *tx).await?;
|
||||||
|
Ok::<_, sqlx::Error>(order.id) // Return value type annotation
|
||||||
|
}).await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Points
|
||||||
|
|
||||||
|
1. **Automatic commit**: Transaction commits if closure returns `Ok`
|
||||||
|
2. **Automatic rollback**: Transaction rolls back if closure returns `Err` or panics
|
||||||
|
3. **Return values**: The closure can return any value wrapped in `Result<T, sqlx::Error>`
|
||||||
|
4. **Executor access**: Use `&mut *tx` to pass the transaction as an executor
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Basic Transaction
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use sqlx_record::{transaction, prelude::*};
|
||||||
|
|
||||||
|
async fn create_user_with_profile(pool: &Pool, user: User, profile: Profile) -> Result<Uuid, sqlx::Error> {
|
||||||
|
transaction!(&pool, |tx| {
|
||||||
|
let user_id = user.insert(&mut *tx).await?;
|
||||||
|
|
||||||
|
let mut profile = profile;
|
||||||
|
profile.user_id = user_id;
|
||||||
|
profile.insert(&mut *tx).await?;
|
||||||
|
|
||||||
|
Ok::<_, sqlx::Error>(user_id)
|
||||||
|
}).await
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple Operations
|
||||||
|
|
||||||
|
```rust
|
||||||
|
async fn transfer_funds(
|
||||||
|
pool: &Pool,
|
||||||
|
from_id: &Uuid,
|
||||||
|
to_id: &Uuid,
|
||||||
|
amount: i64
|
||||||
|
) -> Result<(), sqlx::Error> {
|
||||||
|
transaction!(&pool, |tx| {
|
||||||
|
// Debit from source
|
||||||
|
Account::update_by_id(&mut *tx, from_id,
|
||||||
|
Account::update_form().eval_balance(UpdateExpr::Sub(amount.into()))
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
// Credit to destination
|
||||||
|
Account::update_by_id(&mut *tx, to_id,
|
||||||
|
Account::update_form().eval_balance(UpdateExpr::Add(amount.into()))
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
// Create transfer record
|
||||||
|
let transfer = Transfer {
|
||||||
|
id: new_uuid(),
|
||||||
|
from_account: *from_id,
|
||||||
|
to_account: *to_id,
|
||||||
|
amount,
|
||||||
|
created_at: chrono::Utc::now().timestamp_millis(),
|
||||||
|
};
|
||||||
|
transfer.insert(&mut *tx).await?;
|
||||||
|
|
||||||
|
Ok::<_, sqlx::Error>(())
|
||||||
|
}).await
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Error Handling
|
||||||
|
|
||||||
|
```rust
|
||||||
|
async fn create_order(pool: &Pool, cart: Cart) -> Result<Order, AppError> {
|
||||||
|
transaction!(&pool, |tx| {
|
||||||
|
// Verify stock
|
||||||
|
for item in &cart.items {
|
||||||
|
let product = Product::get_by_id(&mut *tx, &item.product_id).await?
|
||||||
|
.ok_or(sqlx::Error::RowNotFound)?;
|
||||||
|
|
||||||
|
if product.stock < item.quantity {
|
||||||
|
return Err(sqlx::Error::Protocol("Insufficient stock".into()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create order
|
||||||
|
let order = Order {
|
||||||
|
id: new_uuid(),
|
||||||
|
user_id: cart.user_id,
|
||||||
|
status: OrderStatus::PENDING.into(),
|
||||||
|
total: cart.total(),
|
||||||
|
created_at: chrono::Utc::now().timestamp_millis(),
|
||||||
|
};
|
||||||
|
order.insert(&mut *tx).await?;
|
||||||
|
|
||||||
|
// Create order items and decrement stock
|
||||||
|
for item in cart.items {
|
||||||
|
let order_item = OrderItem {
|
||||||
|
id: new_uuid(),
|
||||||
|
order_id: order.id,
|
||||||
|
product_id: item.product_id,
|
||||||
|
quantity: item.quantity,
|
||||||
|
price: item.price,
|
||||||
|
};
|
||||||
|
order_item.insert(&mut *tx).await?;
|
||||||
|
|
||||||
|
Product::update_by_id(&mut *tx, &item.product_id,
|
||||||
|
Product::update_form().eval_stock(UpdateExpr::Sub(item.quantity.into()))
|
||||||
|
).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok::<_, sqlx::Error>(order)
|
||||||
|
}).await.map_err(AppError::from)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nested Operations (Not Nested Transactions)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Helper function that accepts any executor
|
||||||
|
async fn create_audit_log<'a, E>(executor: E, action: &str, entity_id: Uuid) -> Result<(), sqlx::Error>
|
||||||
|
where
|
||||||
|
E: sqlx::Executor<'a, Database = sqlx::MySql>,
|
||||||
|
{
|
||||||
|
let log = AuditLog {
|
||||||
|
id: new_uuid(),
|
||||||
|
action: action.into(),
|
||||||
|
entity_id,
|
||||||
|
created_at: chrono::Utc::now().timestamp_millis(),
|
||||||
|
};
|
||||||
|
log.insert(executor).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use in transaction
|
||||||
|
transaction!(&pool, |tx| {
|
||||||
|
user.insert(&mut *tx).await?;
|
||||||
|
create_audit_log(&mut *tx, "user_created", user.id).await?;
|
||||||
|
Ok::<_, sqlx::Error>(())
|
||||||
|
}).await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Type Annotation
|
||||||
|
|
||||||
|
The closure must have an explicit return type annotation:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Correct - with type annotation
|
||||||
|
Ok::<_, sqlx::Error>(value)
|
||||||
|
|
||||||
|
// Also correct
|
||||||
|
Ok::<i32, sqlx::Error>(42)
|
||||||
|
|
||||||
|
// Incorrect - missing annotation (won't compile)
|
||||||
|
// Ok(value)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Comparison with Manual Transactions
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Manual approach
|
||||||
|
let mut tx = pool.begin().await?;
|
||||||
|
match async {
|
||||||
|
user.insert(&mut *tx).await?;
|
||||||
|
order.insert(&mut *tx).await?;
|
||||||
|
Ok::<_, sqlx::Error>(order.id)
|
||||||
|
}.await {
|
||||||
|
Ok(result) => {
|
||||||
|
tx.commit().await?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tx.rollback().await?;
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// With transaction! macro - cleaner
|
||||||
|
transaction!(&pool, |tx| {
|
||||||
|
user.insert(&mut *tx).await?;
|
||||||
|
order.insert(&mut *tx).await?;
|
||||||
|
Ok::<_, sqlx::Error>(order.id)
|
||||||
|
}).await
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The macro works with all supported databases (MySQL, PostgreSQL, SQLite)
|
||||||
|
- Transactions use the pool's default isolation level
|
||||||
|
- For custom isolation levels, use sqlx's native transaction API
|
||||||
|
- The closure is async - use `.await` for all database operations
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
/target
|
/target
|
||||||
/entity-update_derive/target
|
/sqlx-record-derive/target
|
||||||
/entity-changes-ctl/target
|
/sqlx-record-ctl/target
|
||||||
|
/mcp/target
|
||||||
.idea
|
.idea
|
||||||
/Cargo.lock
|
/Cargo.lock
|
||||||
|
|
|
||||||
267
mcp/src/main.rs
267
mcp/src/main.rs
|
|
@ -1033,6 +1033,239 @@ let users = User::find(&*provider, filters![("active", true)], None).await?;
|
||||||
```
|
```
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
|
const PAGINATION: &str = r#"# Pagination
|
||||||
|
|
||||||
|
Built-in pagination support with Page<T> and PageRequest.
|
||||||
|
|
||||||
|
## PageRequest
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use sqlx_record::prelude::PageRequest;
|
||||||
|
|
||||||
|
// Create request (1-indexed pages)
|
||||||
|
let request = PageRequest::new(1, 20); // page 1, 20 items
|
||||||
|
|
||||||
|
// First page shorthand
|
||||||
|
let request = PageRequest::first(20);
|
||||||
|
|
||||||
|
// For manual queries
|
||||||
|
request.offset() // (page - 1) * page_size
|
||||||
|
request.limit() // page_size
|
||||||
|
```
|
||||||
|
|
||||||
|
## Page<T>
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let page = User::paginate(&pool, filters, None, order_by, request).await?;
|
||||||
|
|
||||||
|
page.items // Vec<T>
|
||||||
|
page.total_count // u64
|
||||||
|
page.page // u32 (current page)
|
||||||
|
page.page_size // u32
|
||||||
|
|
||||||
|
page.total_pages() // ceil(total / page_size)
|
||||||
|
page.has_next() // page < total_pages
|
||||||
|
page.has_prev() // page > 1
|
||||||
|
page.is_empty() // items.is_empty()
|
||||||
|
page.len() // items.len()
|
||||||
|
|
||||||
|
page.map(|t| f(t)) // Page<U>
|
||||||
|
page.into_items() // Vec<T>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let page = User::paginate(
|
||||||
|
&pool,
|
||||||
|
filters![("is_active", true)],
|
||||||
|
Some("idx_users"), // MySQL index hint
|
||||||
|
vec![("created_at", false)], // ORDER BY created_at DESC
|
||||||
|
PageRequest::new(1, 20)
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
for user in page.iter() {
|
||||||
|
println!("{}", user.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if page.has_next() {
|
||||||
|
let next = User::paginate(&pool, filters, None, order, PageRequest::new(page.page + 1, 20)).await?;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
"#;
|
||||||
|
|
||||||
|
const SOFT_DELETE: &str = r#"# Soft Delete
|
||||||
|
|
||||||
|
Mark records as deleted without removing from database.
|
||||||
|
|
||||||
|
## Enable
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Entity, FromRow)]
|
||||||
|
struct User {
|
||||||
|
#[primary_key]
|
||||||
|
id: Uuid,
|
||||||
|
|
||||||
|
#[soft_delete] // Must be bool
|
||||||
|
is_deleted: bool,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Auto-detection: Fields named `is_deleted` or `deleted` with `bool` type work without attribute.
|
||||||
|
|
||||||
|
## Generated Methods
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Soft delete (set to true)
|
||||||
|
user.delete(&pool).await?;
|
||||||
|
User::delete_by_id(&pool, &id).await?;
|
||||||
|
|
||||||
|
// Hard delete (permanent)
|
||||||
|
user.hard_delete(&pool).await?;
|
||||||
|
User::hard_delete_by_id(&pool, &id).await?;
|
||||||
|
|
||||||
|
// Restore (set to false)
|
||||||
|
user.restore(&pool).await?;
|
||||||
|
User::restore_by_id(&pool, &id).await?;
|
||||||
|
|
||||||
|
// Field name
|
||||||
|
User::soft_delete_field() // "is_deleted"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Filtering
|
||||||
|
|
||||||
|
Soft delete does NOT auto-filter. Add filter manually:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Only non-deleted
|
||||||
|
let users = User::find(&pool, filters![("is_deleted", false)], None).await?;
|
||||||
|
|
||||||
|
// Only deleted (trash)
|
||||||
|
let deleted = User::find(&pool, filters![("is_deleted", true)], None).await?;
|
||||||
|
|
||||||
|
// All records
|
||||||
|
let all = User::find(&pool, filters![], None).await?;
|
||||||
|
```
|
||||||
|
"#;
|
||||||
|
|
||||||
|
const BATCH_OPS: &str = r#"# Batch Operations
|
||||||
|
|
||||||
|
Efficient bulk insert and upsert operations.
|
||||||
|
|
||||||
|
## insert_many()
|
||||||
|
|
||||||
|
Insert multiple entities in a single query:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let users = vec![
|
||||||
|
User { id: new_uuid(), name: "Alice".into() },
|
||||||
|
User { id: new_uuid(), name: "Bob".into() },
|
||||||
|
];
|
||||||
|
|
||||||
|
let ids = User::insert_many(&pool, &users).await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
SQL: `INSERT INTO users (id, name) VALUES (?, ?), (?, ?)`
|
||||||
|
|
||||||
|
## upsert() / insert_or_update()
|
||||||
|
|
||||||
|
Insert or update on primary key conflict:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
user.upsert(&pool).await?;
|
||||||
|
// or
|
||||||
|
user.insert_or_update(&pool).await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
SQL (MySQL):
|
||||||
|
```sql
|
||||||
|
INSERT INTO users (id, name) VALUES (?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE name = VALUES(name)
|
||||||
|
```
|
||||||
|
|
||||||
|
SQL (PostgreSQL/SQLite):
|
||||||
|
```sql
|
||||||
|
INSERT INTO users (id, name) VALUES ($1, $2)
|
||||||
|
ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Sync external data
|
||||||
|
for product in external_products {
|
||||||
|
Product { id: product.id, name: product.name }.upsert(&pool).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chunked batch insert
|
||||||
|
for chunk in users.chunks(1000) {
|
||||||
|
User::insert_many(&pool, chunk).await?;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
"#;
|
||||||
|
|
||||||
|
const TRANSACTIONS: &str = r#"# Transactions
|
||||||
|
|
||||||
|
Ergonomic transaction handling with automatic commit/rollback.
|
||||||
|
|
||||||
|
## transaction! Macro
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use sqlx_record::transaction;
|
||||||
|
|
||||||
|
let result = transaction!(&pool, |tx| {
|
||||||
|
user.insert(&mut *tx).await?;
|
||||||
|
order.insert(&mut *tx).await?;
|
||||||
|
Ok::<_, sqlx::Error>(order.id)
|
||||||
|
}).await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
- Commits on `Ok`
|
||||||
|
- Rolls back on `Err` or panic
|
||||||
|
- Use `&mut *tx` as executor
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Transfer Funds
|
||||||
|
|
||||||
|
```rust
|
||||||
|
transaction!(&pool, |tx| {
|
||||||
|
Account::update_by_id(&mut *tx, &from_id,
|
||||||
|
Account::update_form().eval_balance(UpdateExpr::Sub(amount.into()))
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
Account::update_by_id(&mut *tx, &to_id,
|
||||||
|
Account::update_form().eval_balance(UpdateExpr::Add(amount.into()))
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
Ok::<_, sqlx::Error>(())
|
||||||
|
}).await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Return Value
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let order_id = transaction!(&pool, |tx| {
|
||||||
|
let user = User { id: new_uuid(), name: "Alice".into() };
|
||||||
|
user.insert(&mut *tx).await?;
|
||||||
|
|
||||||
|
let order = Order { id: new_uuid(), user_id: user.id, total: 100 };
|
||||||
|
order.insert(&mut *tx).await?;
|
||||||
|
|
||||||
|
Ok::<_, sqlx::Error>(order.id)
|
||||||
|
}).await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Type Annotation
|
||||||
|
|
||||||
|
Must include return type annotation:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
Ok::<_, sqlx::Error>(value) // Correct
|
||||||
|
Ok::<i32, sqlx::Error>(42) // Also correct
|
||||||
|
```
|
||||||
|
"#;
|
||||||
|
|
||||||
const CLI_TOOL: &str = r#"# sqlx-record-ctl CLI
|
const CLI_TOOL: &str = r#"# sqlx-record-ctl CLI
|
||||||
|
|
||||||
Command-line tool for managing audit tables.
|
Command-line tool for managing audit tables.
|
||||||
|
|
@ -1488,7 +1721,7 @@ fn handle_list_tools() -> Value {
|
||||||
"properties": {
|
"properties": {
|
||||||
"feature": {
|
"feature": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["overview", "derive", "filters", "values", "lookup", "audit", "update_form", "update_expr", "conn_provider", "databases", "uuid", "cli", "examples"],
|
"enum": ["overview", "derive", "filters", "values", "lookup", "audit", "update_form", "update_expr", "conn_provider", "databases", "uuid", "cli", "examples", "pagination", "soft_delete", "batch_ops", "transactions"],
|
||||||
"description": "Feature to explain"
|
"description": "Feature to explain"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -1547,6 +1780,10 @@ fn handle_call_tool(params: &Value) -> Value {
|
||||||
"uuid" => NEW_UUID,
|
"uuid" => NEW_UUID,
|
||||||
"cli" => CLI_TOOL,
|
"cli" => CLI_TOOL,
|
||||||
"examples" => EXAMPLES,
|
"examples" => EXAMPLES,
|
||||||
|
"pagination" => PAGINATION,
|
||||||
|
"soft_delete" => SOFT_DELETE,
|
||||||
|
"batch_ops" => BATCH_OPS,
|
||||||
|
"transactions" => TRANSACTIONS,
|
||||||
_ => OVERVIEW,
|
_ => OVERVIEW,
|
||||||
};
|
};
|
||||||
json!({
|
json!({
|
||||||
|
|
@ -1646,6 +1883,30 @@ fn handle_list_resources() -> Value {
|
||||||
"name": "Examples",
|
"name": "Examples",
|
||||||
"description": "Complete usage examples",
|
"description": "Complete usage examples",
|
||||||
"mimeType": "text/markdown"
|
"mimeType": "text/markdown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uri": "sqlx-record://docs/pagination",
|
||||||
|
"name": "Pagination",
|
||||||
|
"description": "Page<T> and PageRequest for paginated queries",
|
||||||
|
"mimeType": "text/markdown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uri": "sqlx-record://docs/soft_delete",
|
||||||
|
"name": "Soft Delete",
|
||||||
|
"description": "#[soft_delete] attribute and delete/restore methods",
|
||||||
|
"mimeType": "text/markdown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uri": "sqlx-record://docs/batch_ops",
|
||||||
|
"name": "Batch Operations",
|
||||||
|
"description": "insert_many() and upsert() for bulk operations",
|
||||||
|
"mimeType": "text/markdown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uri": "sqlx-record://docs/transactions",
|
||||||
|
"name": "Transactions",
|
||||||
|
"description": "transaction! macro for ergonomic transactions",
|
||||||
|
"mimeType": "text/markdown"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
@ -1668,6 +1929,10 @@ fn handle_read_resource(params: &Value) -> Value {
|
||||||
"sqlx-record://docs/uuid" => NEW_UUID,
|
"sqlx-record://docs/uuid" => NEW_UUID,
|
||||||
"sqlx-record://docs/cli" => CLI_TOOL,
|
"sqlx-record://docs/cli" => CLI_TOOL,
|
||||||
"sqlx-record://docs/examples" => EXAMPLES,
|
"sqlx-record://docs/examples" => EXAMPLES,
|
||||||
|
"sqlx-record://docs/pagination" => PAGINATION,
|
||||||
|
"sqlx-record://docs/soft_delete" => SOFT_DELETE,
|
||||||
|
"sqlx-record://docs/batch_ops" => BATCH_OPS,
|
||||||
|
"sqlx-record://docs/transactions" => TRANSACTIONS,
|
||||||
_ => "Resource not found",
|
_ => "Resource not found",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue