Compare commits
No commits in common. "main" and "v0.3.2" have entirely different histories.
|
|
@ -6,14 +6,12 @@ Guide to flexible connection management.
|
||||||
- "connection provider", "conn provider"
|
- "connection provider", "conn provider"
|
||||||
- "borrow connection", "pool connection"
|
- "borrow connection", "pool connection"
|
||||||
- "lazy connection", "connection management"
|
- "lazy connection", "connection management"
|
||||||
- "transaction provider", "use transaction"
|
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
`ConnProvider` enables flexible connection handling:
|
`ConnProvider` enables flexible connection handling:
|
||||||
- **Borrowed**: Use an existing connection reference
|
- **Borrowed**: Use an existing connection reference
|
||||||
- **Owned**: Lazily acquire from pool on first use
|
- **Owned**: Lazily acquire from pool on first use
|
||||||
- **Transaction**: Use a transaction reference (all operations participate in the transaction)
|
|
||||||
|
|
||||||
## Enum Variants
|
## Enum Variants
|
||||||
|
|
||||||
|
|
@ -28,10 +26,6 @@ pub enum ConnProvider<'a> {
|
||||||
pool: Pool,
|
pool: Pool,
|
||||||
conn: Option<PoolConnection<DB>>,
|
conn: Option<PoolConnection<DB>>,
|
||||||
},
|
},
|
||||||
/// Reference to a transaction
|
|
||||||
Transaction {
|
|
||||||
tx: &'a mut Transaction<'static, DB>,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -51,29 +45,15 @@ let mut provider = ConnProvider::from_pool(pool.clone());
|
||||||
// Connection acquired on first get_conn() call
|
// Connection acquired on first get_conn() call
|
||||||
```
|
```
|
||||||
|
|
||||||
### from_tx
|
|
||||||
Use a transaction (all operations participate in the transaction):
|
|
||||||
```rust
|
|
||||||
let mut tx = pool.begin().await?;
|
|
||||||
let mut provider = ConnProvider::from_tx(&mut tx);
|
|
||||||
|
|
||||||
// All operations through provider use the transaction
|
|
||||||
do_work(&mut provider).await?;
|
|
||||||
|
|
||||||
// You must commit/rollback the transaction yourself
|
|
||||||
tx.commit().await?;
|
|
||||||
```
|
|
||||||
|
|
||||||
## Getting the Connection
|
## Getting the Connection
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
let conn = provider.get_conn().await?;
|
let conn = provider.get_conn().await?;
|
||||||
// Returns &mut <DB>Connection (e.g., &mut MySqlConnection)
|
// Returns &mut PoolConnection<DB>
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Borrowed**: Returns underlying connection immediately
|
- **Borrowed**: Returns reference immediately
|
||||||
- **Owned**: Acquires on first call, returns same connection on subsequent calls
|
- **Owned**: Acquires on first call, returns same connection on subsequent calls
|
||||||
- **Transaction**: Returns transaction's underlying connection
|
|
||||||
|
|
||||||
## Use Cases
|
## Use Cases
|
||||||
|
|
||||||
|
|
@ -125,37 +105,15 @@ let mut conn = pool.acquire().await?;
|
||||||
do_database_work(&mut ConnProvider::from_ref(&mut conn)).await?;
|
do_database_work(&mut ConnProvider::from_ref(&mut conn)).await?;
|
||||||
|
|
||||||
// Call with pool
|
// Call with pool
|
||||||
do_database_work(&mut ConnProvider::from_pool(pool.clone())).await?;
|
do_database_work(&mut ConnProvider::from_pool(pool)).await?;
|
||||||
|
|
||||||
// Call with transaction
|
|
||||||
let mut tx = pool.begin().await?;
|
|
||||||
do_database_work(&mut ConnProvider::from_tx(&mut tx)).await?;
|
|
||||||
tx.commit().await?;
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Using Transactions
|
### Transaction-like Patterns
|
||||||
```rust
|
|
||||||
async fn transactional_operation(pool: MySqlPool) -> Result<()> {
|
|
||||||
let mut tx = pool.begin().await?;
|
|
||||||
let mut provider = ConnProvider::from_tx(&mut tx);
|
|
||||||
|
|
||||||
// All operations participate in the transaction
|
|
||||||
step_1(&mut provider).await?;
|
|
||||||
step_2(&mut provider).await?;
|
|
||||||
step_3(&mut provider).await?;
|
|
||||||
|
|
||||||
// Commit (or rollback on error)
|
|
||||||
tx.commit().await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Same Connection Pattern
|
|
||||||
```rust
|
```rust
|
||||||
async fn multi_step_operation(pool: MySqlPool) -> Result<()> {
|
async fn multi_step_operation(pool: MySqlPool) -> Result<()> {
|
||||||
let mut provider = ConnProvider::from_pool(pool);
|
let mut provider = ConnProvider::from_pool(pool);
|
||||||
|
|
||||||
// All operations use same connection (but no transaction)
|
// All operations use same connection
|
||||||
step_1(&mut provider).await?;
|
step_1(&mut provider).await?;
|
||||||
step_2(&mut provider).await?;
|
step_2(&mut provider).await?;
|
||||||
step_3(&mut provider).await?;
|
step_3(&mut provider).await?;
|
||||||
|
|
@ -169,13 +127,11 @@ async fn multi_step_operation(pool: MySqlPool) -> Result<()> {
|
||||||
|
|
||||||
The concrete types depend on the enabled feature:
|
The concrete types depend on the enabled feature:
|
||||||
|
|
||||||
| Feature | Pool Type | Connection Type | Transaction Type |
|
| Feature | Pool Type | Connection Type |
|
||||||
|---------|-----------|-----------------|------------------|
|
|---------|-----------|-----------------|
|
||||||
| `mysql` | `MySqlPool` | `MySqlConnection` | `Transaction<'static, MySql>` |
|
| `mysql` | `MySqlPool` | `PoolConnection<MySql>` |
|
||||||
| `postgres` | `PgPool` | `PgConnection` | `Transaction<'static, Postgres>` |
|
| `postgres` | `PgPool` | `PoolConnection<Postgres>` |
|
||||||
| `sqlite` | `SqlitePool` | `SqliteConnection` | `Transaction<'static, Sqlite>` |
|
| `sqlite` | `SqlitePool` | `PoolConnection<Sqlite>` |
|
||||||
|
|
||||||
Note: `get_conn()` returns `&mut <DB>Connection` (the underlying connection type).
|
|
||||||
|
|
||||||
## Example: Service Layer
|
## Example: Service Layer
|
||||||
|
|
||||||
|
|
@ -219,28 +175,28 @@ let user_id = UserService::create_with_profile(&mut provider, "Alice", "Hello!")
|
||||||
## Connection Lifecycle
|
## Connection Lifecycle
|
||||||
|
|
||||||
```
|
```
|
||||||
from_pool(pool) from_ref(&mut conn) from_tx(&mut tx)
|
from_pool(pool) from_ref(&mut conn)
|
||||||
│ │ │
|
│ │
|
||||||
▼ ▼ ▼
|
▼ ▼
|
||||||
Owned { Borrowed { Transaction {
|
Owned { Borrowed {
|
||||||
pool, conn: &mut tx: &mut
|
pool, conn: &mut PoolConnection
|
||||||
conn: None PoolConnection Transaction
|
conn: None }
|
||||||
} } }
|
} │
|
||||||
│ │ │
|
│ │
|
||||||
│ get_conn() │ get_conn() │ get_conn()
|
│ get_conn() │ get_conn()
|
||||||
▼ ▼ ▼
|
▼ ▼
|
||||||
pool.acquire() deref conn deref tx
|
pool.acquire() return conn
|
||||||
│ │ │
|
│ │
|
||||||
▼ ▼ ▼
|
▼ │
|
||||||
Owned { return &mut return &mut
|
Owned { │
|
||||||
pool, Connection Connection
|
pool, │
|
||||||
conn: Some(acquired) │ │
|
conn: Some(acquired) │
|
||||||
} │ │
|
} │
|
||||||
│ │ │
|
│ │
|
||||||
│ get_conn() (subsequent) │ │
|
│ get_conn() (subsequent) │
|
||||||
▼ ▼ ▼
|
▼ │
|
||||||
return &mut conn Drop: nothing Drop: nothing
|
return &mut acquired │
|
||||||
│ (borrowed) (tx managed
|
│ │
|
||||||
▼ externally)
|
▼ ▼
|
||||||
Drop: conn returned
|
Drop: conn returned Drop: nothing (borrowed)
|
||||||
```
|
```
|
||||||
|
|
@ -59,13 +59,11 @@ large_count: i64,
|
||||||
### #[soft_delete]
|
### #[soft_delete]
|
||||||
```rust
|
```rust
|
||||||
#[soft_delete]
|
#[soft_delete]
|
||||||
is_active: bool,
|
is_deleted: bool,
|
||||||
```
|
```
|
||||||
- Enables soft delete functionality
|
- Enables soft delete functionality
|
||||||
- Generates `soft_delete()`, `soft_delete_by_{pk}()`, `restore()`, `restore_by_{pk}()` methods
|
- Generates `delete()`, `restore()`, `hard_delete()` methods
|
||||||
- Field must be `bool` type
|
- Field must be `bool` type
|
||||||
- Convention: `is_active` fields are auto-detected (FALSE = deleted)
|
|
||||||
- `#[soft_delete]` attribute means field is FALSE when entity is deleted
|
|
||||||
|
|
||||||
### #[created_at]
|
### #[created_at]
|
||||||
```rust
|
```rust
|
||||||
|
|
@ -196,20 +194,17 @@ 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>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Hard Delete (always generated)
|
### Soft Delete Methods (if #[soft_delete] exists)
|
||||||
```rust
|
```rust
|
||||||
// Permanently removes row from database
|
// 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(&self, executor) -> Result<(), Error>
|
||||||
pub async fn hard_delete_by_id(executor, id: &Uuid) -> Result<(), Error>
|
pub async fn hard_delete_by_id(executor, id: &Uuid) -> Result<(), Error>
|
||||||
```
|
|
||||||
|
|
||||||
### Soft Delete Methods (if `is_active` field or `#[soft_delete]` exists)
|
// Restore - sets field to false
|
||||||
```rust
|
|
||||||
// Soft delete - marks as deleted (is_active = FALSE)
|
|
||||||
pub async fn soft_delete(&self, executor) -> Result<(), Error>
|
|
||||||
pub async fn soft_delete_by_id(executor, id: &Uuid) -> Result<(), Error>
|
|
||||||
|
|
||||||
// Restore - marks as active (is_active = TRUE)
|
|
||||||
pub async fn restore(&self, executor) -> Result<(), Error>
|
pub async fn restore(&self, executor) -> Result<(), Error>
|
||||||
pub async fn restore_by_id(executor, id: &Uuid) -> Result<(), Error>
|
pub async fn restore_by_id(executor, id: &Uuid) -> Result<(), Error>
|
||||||
|
|
||||||
|
|
@ -122,30 +122,28 @@ sqlx-record = { version = "0.3", features = ["mysql", "derive"] }
|
||||||
# Optional: "derive", "static-validation"
|
# Optional: "derive", "static-validation"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Delete, Soft Delete, Timestamps, Batch Operations
|
## Soft Delete, Timestamps, Batch Operations
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
#[derive(Entity, FromRow)]
|
#[derive(Entity, FromRow)]
|
||||||
struct User {
|
struct User {
|
||||||
#[primary_key] id: Uuid,
|
#[primary_key] id: Uuid,
|
||||||
name: String,
|
name: String,
|
||||||
is_active: bool, // Auto-detected for soft delete (is_active = FALSE when deleted)
|
|
||||||
|
|
||||||
#[created_at] // Auto-set on insert
|
#[soft_delete] // Enables delete/restore/hard_delete
|
||||||
|
is_deleted: bool,
|
||||||
|
|
||||||
|
#[created_at] // Auto-set on insert
|
||||||
created_at: i64,
|
created_at: i64,
|
||||||
|
|
||||||
#[updated_at] // Auto-set on update
|
#[updated_at] // Auto-set on update
|
||||||
updated_at: i64,
|
updated_at: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hard delete (always available on all entities)
|
// Soft delete
|
||||||
user.hard_delete(&pool).await?; // DELETE FROM
|
user.delete(&pool).await?; // is_deleted = true
|
||||||
User::hard_delete_by_id(&pool, &id).await?;
|
user.restore(&pool).await?; // is_deleted = false
|
||||||
|
user.hard_delete(&pool).await?; // DELETE FROM
|
||||||
// Soft delete (when is_active or #[soft_delete] field exists)
|
|
||||||
user.soft_delete(&pool).await?; // is_active = false
|
|
||||||
User::soft_delete_by_id(&pool, &id).await?;
|
|
||||||
user.restore(&pool).await?; // is_active = true
|
|
||||||
|
|
||||||
// Batch insert
|
// Batch insert
|
||||||
User::insert_many(&pool, &users).await?;
|
User::insert_many(&pool, &users).await?;
|
||||||
|
|
@ -208,23 +206,11 @@ User::update_by_id(&pool, &id,
|
||||||
## ConnProvider (Flexible Connections)
|
## ConnProvider (Flexible Connections)
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use sqlx_record::prelude::ConnProvider;
|
use sqlx_record::ConnProvider;
|
||||||
|
|
||||||
// From borrowed connection
|
// Borrowed or owned pool connections
|
||||||
let mut conn = pool.acquire().await?;
|
let conn = ConnProvider::Borrowed(&pool);
|
||||||
let mut provider = ConnProvider::from_ref(&mut conn);
|
let users = User::find(&*conn, filters![], None).await?;
|
||||||
|
|
||||||
// From pool (lazy acquisition)
|
|
||||||
let mut provider = ConnProvider::from_pool(pool.clone());
|
|
||||||
|
|
||||||
// From transaction (operations participate in the transaction)
|
|
||||||
let mut tx = pool.begin().await?;
|
|
||||||
let mut provider = ConnProvider::from_tx(&mut tx);
|
|
||||||
// ... use provider ...
|
|
||||||
tx.commit().await?;
|
|
||||||
|
|
||||||
// Get underlying connection
|
|
||||||
let conn = provider.get_conn().await?;
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Database Differences
|
## Database Differences
|
||||||
|
|
@ -1,17 +1,61 @@
|
||||||
# sqlx-record Delete & Soft Delete Skill
|
# sqlx-record Soft Delete Skill
|
||||||
|
|
||||||
Guide to hard delete and soft delete functionality.
|
Guide to soft delete functionality with #[soft_delete] attribute.
|
||||||
|
|
||||||
## Triggers
|
## Triggers
|
||||||
- "soft delete", "soft-delete"
|
- "soft delete", "soft-delete"
|
||||||
- "hard delete", "permanent delete"
|
- "is_deleted", "deleted"
|
||||||
- "is_active", "is_deleted", "deleted"
|
|
||||||
- "restore", "undelete"
|
- "restore", "undelete"
|
||||||
- "delete_by_id", "hard_delete_by_id"
|
- "hard delete", "permanent delete"
|
||||||
|
|
||||||
## Hard Delete (Always Generated)
|
## Overview
|
||||||
|
|
||||||
Every Entity gets `hard_delete()` and `hard_delete_by_{pk}()` methods. No configuration needed.
|
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
|
```rust
|
||||||
// Instance method
|
// Instance method
|
||||||
|
|
@ -26,73 +70,8 @@ User::hard_delete_by_id(&pool, &user_id).await?;
|
||||||
DELETE FROM users WHERE id = ?
|
DELETE FROM users WHERE id = ?
|
||||||
```
|
```
|
||||||
|
|
||||||
## Soft Delete
|
|
||||||
|
|
||||||
Marks 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
|
|
||||||
|
|
||||||
**Preferred: `is_active` convention** (auto-detected, no attribute needed):
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use sqlx_record::prelude::*;
|
|
||||||
|
|
||||||
#[derive(Entity, FromRow)]
|
|
||||||
#[table_name = "users"]
|
|
||||||
struct User {
|
|
||||||
#[primary_key]
|
|
||||||
id: Uuid,
|
|
||||||
name: String,
|
|
||||||
is_active: bool, // Auto-detected: FALSE = deleted, TRUE = active
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Alternative: `#[soft_delete]` attribute** on any bool field:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[derive(Entity, FromRow)]
|
|
||||||
#[table_name = "users"]
|
|
||||||
struct User {
|
|
||||||
#[primary_key]
|
|
||||||
id: Uuid,
|
|
||||||
name: String,
|
|
||||||
|
|
||||||
#[soft_delete] // Field will be FALSE when deleted
|
|
||||||
is_active: bool,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Legacy: `is_deleted`/`deleted` fields** are also auto-detected (TRUE = deleted).
|
|
||||||
|
|
||||||
### Detection Priority
|
|
||||||
|
|
||||||
1. Field with `#[soft_delete]` attribute (FALSE = deleted)
|
|
||||||
2. Field named `is_active` with bool type (FALSE = deleted)
|
|
||||||
3. Field named `is_deleted` or `deleted` with bool type (TRUE = deleted)
|
|
||||||
|
|
||||||
## Generated Methods
|
|
||||||
|
|
||||||
### soft_delete() / soft_delete_by_{pk}()
|
|
||||||
Marks the record as deleted:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Instance method
|
|
||||||
user.soft_delete(&pool).await?;
|
|
||||||
|
|
||||||
// Static method by primary key
|
|
||||||
User::soft_delete_by_id(&pool, &user_id).await?;
|
|
||||||
```
|
|
||||||
|
|
||||||
**SQL generated (is_active convention):**
|
|
||||||
```sql
|
|
||||||
UPDATE users SET is_active = FALSE WHERE id = ?
|
|
||||||
```
|
|
||||||
|
|
||||||
### restore() / restore_by_{pk}()
|
### restore() / restore_by_{pk}()
|
||||||
Restores a soft-deleted record:
|
Sets the soft delete field to `false`:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// Instance method
|
// Instance method
|
||||||
|
|
@ -102,16 +81,16 @@ user.restore(&pool).await?;
|
||||||
User::restore_by_id(&pool, &user_id).await?;
|
User::restore_by_id(&pool, &user_id).await?;
|
||||||
```
|
```
|
||||||
|
|
||||||
**SQL generated (is_active convention):**
|
**SQL generated:**
|
||||||
```sql
|
```sql
|
||||||
UPDATE users SET is_active = TRUE WHERE id = ?
|
UPDATE users SET is_deleted = FALSE WHERE id = ?
|
||||||
```
|
```
|
||||||
|
|
||||||
### soft_delete_field()
|
### soft_delete_field()
|
||||||
Returns the field name:
|
Returns the field name:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
let field = User::soft_delete_field(); // "is_active"
|
let field = User::soft_delete_field(); // "is_deleted"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Filtering Deleted Records
|
## Filtering Deleted Records
|
||||||
|
|
@ -119,11 +98,11 @@ let field = User::soft_delete_field(); // "is_active"
|
||||||
Soft delete does **NOT** automatically filter `find()` queries. You must add the filter manually:
|
Soft delete does **NOT** automatically filter `find()` queries. You must add the filter manually:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// Include only active (non-deleted)
|
// Include only non-deleted
|
||||||
let users = User::find(&pool, filters![("is_active", true)], None).await?;
|
let users = User::find(&pool, filters![("is_deleted", false)], None).await?;
|
||||||
|
|
||||||
// Include only deleted (trash view)
|
// Include only deleted (trash view)
|
||||||
let deleted = User::find(&pool, filters![("is_active", false)], None).await?;
|
let deleted = User::find(&pool, filters![("is_deleted", true)], None).await?;
|
||||||
|
|
||||||
// Include all records
|
// Include all records
|
||||||
let all = User::find(&pool, filters![], None).await?;
|
let all = User::find(&pool, filters![], None).await?;
|
||||||
|
|
@ -140,7 +119,7 @@ impl User {
|
||||||
mut filters: Vec<Filter<'_>>,
|
mut filters: Vec<Filter<'_>>,
|
||||||
index: Option<&str>
|
index: Option<&str>
|
||||||
) -> Result<Vec<Self>, sqlx::Error> {
|
) -> Result<Vec<Self>, sqlx::Error> {
|
||||||
filters.push(Filter::Equal("is_active", true.into()));
|
filters.push(Filter::Equal("is_deleted", false.into()));
|
||||||
Self::find(pool, filters, index).await
|
Self::find(pool, filters, index).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -151,28 +130,28 @@ let users = User::find_active(&pool, filters![("role", "admin")], None).await?;
|
||||||
|
|
||||||
## Usage Examples
|
## Usage Examples
|
||||||
|
|
||||||
### Basic Flow
|
### Basic Soft Delete Flow
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// Create user
|
// Create user
|
||||||
let user = User {
|
let user = User {
|
||||||
id: new_uuid(),
|
id: new_uuid(),
|
||||||
name: "Alice".into(),
|
name: "Alice".into(),
|
||||||
is_active: true,
|
is_deleted: false,
|
||||||
};
|
};
|
||||||
user.insert(&pool).await?;
|
user.insert(&pool).await?;
|
||||||
|
|
||||||
// Soft delete
|
// Soft delete
|
||||||
user.soft_delete(&pool).await?;
|
user.delete(&pool).await?;
|
||||||
// user still exists in DB with is_active = false
|
// user still exists in DB with is_deleted = true
|
||||||
|
|
||||||
// Find won't return deleted users (with proper filter)
|
// Find won't return deleted users (with proper filter)
|
||||||
let users = User::find(&pool, filters![("is_active", true)], None).await?;
|
let users = User::find(&pool, filters![("is_deleted", false)], None).await?;
|
||||||
// Alice not in results
|
// Alice not in results
|
||||||
|
|
||||||
// Restore
|
// Restore
|
||||||
User::restore_by_id(&pool, &user.id).await?;
|
User::restore_by_id(&pool, &user.id).await?;
|
||||||
// user.is_active = true again
|
// user.is_deleted = false again
|
||||||
|
|
||||||
// Hard delete (permanent)
|
// Hard delete (permanent)
|
||||||
User::hard_delete_by_id(&pool, &user.id).await?;
|
User::hard_delete_by_id(&pool, &user.id).await?;
|
||||||
|
|
@ -191,13 +170,13 @@ async fn soft_delete_with_audit(
|
||||||
) -> Result<(), sqlx::Error> {
|
) -> Result<(), sqlx::Error> {
|
||||||
transaction!(&pool, |tx| {
|
transaction!(&pool, |tx| {
|
||||||
// Soft delete the user
|
// Soft delete the user
|
||||||
User::soft_delete_by_id(&mut *tx, user_id).await?;
|
User::delete_by_id(&mut *tx, user_id).await?;
|
||||||
|
|
||||||
// Record the deletion
|
// Record the deletion
|
||||||
let change = EntityChange {
|
let change = EntityChange {
|
||||||
id: new_uuid(),
|
id: new_uuid(),
|
||||||
entity_id: *user_id,
|
entity_id: *user_id,
|
||||||
action: "soft_delete".into(),
|
action: "delete".into(),
|
||||||
changed_at: chrono::Utc::now().timestamp_millis(),
|
changed_at: chrono::Utc::now().timestamp_millis(),
|
||||||
actor_id: *actor_id,
|
actor_id: *actor_id,
|
||||||
session_id: Uuid::nil(),
|
session_id: Uuid::nil(),
|
||||||
|
|
@ -219,11 +198,11 @@ async fn delete_user_cascade(pool: &Pool, user_id: &Uuid) -> Result<(), sqlx::Er
|
||||||
// Soft delete user's orders
|
// Soft delete user's orders
|
||||||
let orders = Order::find(&mut *tx, filters![("user_id", user_id)], None).await?;
|
let orders = Order::find(&mut *tx, filters![("user_id", user_id)], None).await?;
|
||||||
for order in orders {
|
for order in orders {
|
||||||
order.soft_delete(&mut *tx).await?;
|
order.delete(&mut *tx).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Soft delete user
|
// Soft delete user
|
||||||
User::soft_delete_by_id(&mut *tx, user_id).await?;
|
User::delete_by_id(&mut *tx, user_id).await?;
|
||||||
|
|
||||||
Ok::<_, sqlx::Error>(())
|
Ok::<_, sqlx::Error>(())
|
||||||
}).await
|
}).await
|
||||||
|
|
@ -236,28 +215,27 @@ Recommended column definition:
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
-- MySQL
|
-- MySQL
|
||||||
is_active BOOLEAN NOT NULL DEFAULT TRUE
|
is_deleted BOOLEAN NOT NULL DEFAULT FALSE
|
||||||
|
|
||||||
-- PostgreSQL
|
-- PostgreSQL
|
||||||
is_active BOOLEAN NOT NULL DEFAULT TRUE
|
is_deleted BOOLEAN NOT NULL DEFAULT FALSE
|
||||||
|
|
||||||
-- SQLite
|
-- SQLite
|
||||||
is_active INTEGER NOT NULL DEFAULT 1 -- 1=true, 0=false
|
is_deleted INTEGER NOT NULL DEFAULT 0 -- 0=false, 1=true
|
||||||
```
|
```
|
||||||
|
|
||||||
Add an index for efficient filtering:
|
Add an index for efficient filtering:
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
CREATE INDEX idx_users_is_active ON users (is_active);
|
CREATE INDEX idx_users_is_deleted ON users (is_deleted);
|
||||||
|
|
||||||
-- Or composite index for common queries
|
-- Or composite index for common queries
|
||||||
CREATE INDEX idx_users_active_name ON users (is_active, name);
|
CREATE INDEX idx_users_active_name ON users (is_deleted, name);
|
||||||
```
|
```
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- Soft delete field must be `bool` type
|
- Soft delete field must be `bool` type
|
||||||
- The field is included in UpdateForm (can be manually toggled)
|
- The field is included in UpdateForm (can be manually toggled)
|
||||||
- `hard_delete()` / `hard_delete_by_{pk}()` are always available, even on entities with soft delete
|
|
||||||
- Consider adding `deleted_at: Option<i64>` for deletion timestamps
|
- Consider adding `deleted_at: Option<i64>` for deletion timestamps
|
||||||
- For complex filtering, consider database views
|
- For complex filtering, consider database views
|
||||||
23
CLAUDE.md
23
CLAUDE.md
|
|
@ -31,14 +31,13 @@ sqlx-record/
|
||||||
│ └── src/main.rs
|
│ └── src/main.rs
|
||||||
├── mcp/ # MCP server for documentation/code generation
|
├── mcp/ # MCP server for documentation/code generation
|
||||||
│ └── src/main.rs # sqlx-record-expert executable
|
│ └── src/main.rs # sqlx-record-expert executable
|
||||||
├── .claude/skills/sqlx-record/ # Claude Code skills documentation
|
├── .claude/skills/ # Claude Code skills documentation
|
||||||
│ ├── sqlx-record.md # Overview and quick reference
|
│ ├── sqlx-record.md # Overview and quick reference
|
||||||
│ ├── sqlx-entity.md # #[derive(Entity)] detailed guide
|
│ ├── sqlx-entity.md # #[derive(Entity)] detailed guide
|
||||||
│ ├── sqlx-filters.md # Filter system guide
|
│ ├── sqlx-filters.md # Filter system guide
|
||||||
│ ├── sqlx-audit.md # Audit trail guide
|
│ ├── sqlx-audit.md # Audit trail guide
|
||||||
│ ├── sqlx-lookup.md # Lookup tables guide
|
│ ├── sqlx-lookup.md # Lookup tables guide
|
||||||
│ ├── sqlx-values.md # Value types guide
|
│ └── sqlx-values.md # Value types guide
|
||||||
│ └── sqlx-conn-provider.md # Connection provider guide
|
|
||||||
└── Cargo.toml # Workspace root
|
└── Cargo.toml # Workspace root
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -123,7 +122,7 @@ let id = new_uuid(); // Timestamp prefix (8 bytes) + random (8 bytes)
|
||||||
|
|
||||||
## Connection Provider
|
## Connection Provider
|
||||||
|
|
||||||
Flexible connection management - borrow existing connection, lazily acquire from pool, or use a transaction:
|
Flexible connection management - borrow existing or lazily acquire from pool:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use sqlx_record::prelude::ConnProvider;
|
use sqlx_record::prelude::ConnProvider;
|
||||||
|
|
@ -134,12 +133,6 @@ let mut provider = ConnProvider::from_ref(&mut conn);
|
||||||
// From pool (lazy acquisition)
|
// From pool (lazy acquisition)
|
||||||
let mut provider = ConnProvider::from_pool(pool.clone());
|
let mut provider = ConnProvider::from_pool(pool.clone());
|
||||||
|
|
||||||
// From transaction (operations participate in the transaction)
|
|
||||||
let mut tx = pool.begin().await?;
|
|
||||||
let mut provider = ConnProvider::from_tx(&mut tx);
|
|
||||||
// ... use provider ...
|
|
||||||
tx.commit().await?;
|
|
||||||
|
|
||||||
// Get connection (acquires on first call for Owned variant)
|
// Get connection (acquires on first call for Owned variant)
|
||||||
let conn = provider.get_conn().await?;
|
let conn = provider.get_conn().await?;
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ edition.workspace = true
|
||||||
description = "Entity CRUD and change tracking for SQL databases with SQLx"
|
description = "Entity CRUD and change tracking for SQL databases with SQLx"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.3.7"
|
version = "0.3.2"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
@ -16,7 +16,6 @@ uuid = { version = "1", features = ["v4"] }
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
paste = "1.0"
|
paste = "1.0"
|
||||||
rust_decimal = { version = "1", optional = true }
|
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
|
|
@ -28,8 +27,7 @@ members = [
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
derive = ["dep:sqlx-record-derive"]
|
derive = ["dep:sqlx-record-derive"]
|
||||||
static-check = ["sqlx-record-derive?/static-check"]
|
static-validation = ["sqlx-record-derive?/static-validation"]
|
||||||
decimal = ["dep:rust_decimal", "sqlx/rust_decimal"]
|
|
||||||
|
|
||||||
# Database backends - user must enable at least one
|
# Database backends - user must enable at least one
|
||||||
mysql = ["sqlx/mysql", "sqlx-record-derive?/mysql"]
|
mysql = ["sqlx/mysql", "sqlx-record-derive?/mysql"]
|
||||||
|
|
|
||||||
|
|
@ -51,8 +51,7 @@ A Rust library providing derive macros for automatic CRUD operations and compreh
|
||||||
- **Audit Trails**: Track who changed what, when, and why
|
- **Audit Trails**: Track who changed what, when, and why
|
||||||
- **Type-Safe Filters**: Composable query building with `Filter` enum
|
- **Type-Safe Filters**: Composable query building with `Filter` enum
|
||||||
- **UpdateExpr**: Advanced updates with arithmetic, CASE/WHEN, conditionals
|
- **UpdateExpr**: Advanced updates with arithmetic, CASE/WHEN, conditionals
|
||||||
- **Hard Delete**: `hard_delete_by_{pk}()` always generated for all entities
|
- **Soft Deletes**: `#[soft_delete]` with delete/restore/hard_delete methods
|
||||||
- **Soft Deletes**: `#[soft_delete]` or `is_active` convention with soft_delete/restore methods
|
|
||||||
- **Auto Timestamps**: `#[created_at]`, `#[updated_at]` auto-populated
|
- **Auto Timestamps**: `#[created_at]`, `#[updated_at]` auto-populated
|
||||||
- **Batch Operations**: `insert_many()`, `upsert()` for efficient bulk operations
|
- **Batch Operations**: `insert_many()`, `upsert()` for efficient bulk operations
|
||||||
- **Pagination**: `Page<T>` with `paginate()` method
|
- **Pagination**: `Page<T>` with `paginate()` method
|
||||||
|
|
@ -167,21 +166,6 @@ pub async fn update_by_ids(executor, ids: &[Uuid], form: UpdateForm) -> Result<(
|
||||||
pub fn update_form() -> UpdateForm
|
pub fn update_form() -> UpdateForm
|
||||||
```
|
```
|
||||||
|
|
||||||
### Delete (always generated)
|
|
||||||
```rust
|
|
||||||
pub async fn hard_delete(&self, executor) -> Result<(), Error>
|
|
||||||
pub async fn hard_delete_by_id(executor, id: &Uuid) -> Result<(), Error>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Soft Delete (if `is_active` field or `#[soft_delete]` exists)
|
|
||||||
```rust
|
|
||||||
pub async fn soft_delete(&self, executor) -> Result<(), Error>
|
|
||||||
pub async fn soft_delete_by_id(executor, id: &Uuid) -> Result<(), Error>
|
|
||||||
pub async fn restore(&self, executor) -> Result<(), Error>
|
|
||||||
pub async fn restore_by_id(executor, id: &Uuid) -> Result<(), Error>
|
|
||||||
pub const fn soft_delete_field() -> &'static str
|
|
||||||
```
|
|
||||||
|
|
||||||
### Diff (Change Detection)
|
### Diff (Change Detection)
|
||||||
```rust
|
```rust
|
||||||
pub fn model_diff(form: &UpdateForm, model: &Self) -> serde_json::Value
|
pub fn model_diff(form: &UpdateForm, model: &Self) -> serde_json::Value
|
||||||
|
|
@ -1116,43 +1100,11 @@ if page.has_next() {
|
||||||
```
|
```
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
const SOFT_DELETE: &str = r#"# Delete Methods
|
const SOFT_DELETE: &str = r#"# Soft Delete
|
||||||
|
|
||||||
## Hard Delete (always generated)
|
|
||||||
|
|
||||||
Every Entity gets `hard_delete` and `hard_delete_by_{pk}` methods:
|
|
||||||
|
|
||||||
```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 = ?
|
|
||||||
```
|
|
||||||
|
|
||||||
## Soft Delete
|
|
||||||
|
|
||||||
Mark records as deleted without removing from database.
|
Mark records as deleted without removing from database.
|
||||||
|
|
||||||
### Enable
|
## Enable
|
||||||
|
|
||||||
Convention: an `is_active` bool field is auto-detected (preferred):
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[derive(Entity, FromRow)]
|
|
||||||
struct User {
|
|
||||||
#[primary_key]
|
|
||||||
id: Uuid,
|
|
||||||
is_active: bool, // Auto-detected: FALSE = deleted
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Or use `#[soft_delete]` on any bool field:
|
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
#[derive(Entity, FromRow)]
|
#[derive(Entity, FromRow)]
|
||||||
|
|
@ -1160,38 +1112,42 @@ struct User {
|
||||||
#[primary_key]
|
#[primary_key]
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
|
|
||||||
#[soft_delete] // Field will be FALSE when deleted
|
#[soft_delete] // Must be bool
|
||||||
is_active: bool,
|
is_deleted: bool,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Auto-detection also works for `is_deleted` or `deleted` bool fields (TRUE = deleted).
|
Auto-detection: Fields named `is_deleted` or `deleted` with `bool` type work without attribute.
|
||||||
|
|
||||||
### Generated Methods
|
## Generated Methods
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// Soft delete (set is_active = FALSE)
|
// Soft delete (set to true)
|
||||||
user.soft_delete(&pool).await?;
|
user.delete(&pool).await?;
|
||||||
User::soft_delete_by_id(&pool, &id).await?;
|
User::delete_by_id(&pool, &id).await?;
|
||||||
|
|
||||||
// Restore (set is_active = TRUE)
|
// 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(&pool).await?;
|
||||||
User::restore_by_id(&pool, &id).await?;
|
User::restore_by_id(&pool, &id).await?;
|
||||||
|
|
||||||
// Field name
|
// Field name
|
||||||
User::soft_delete_field() // "is_active"
|
User::soft_delete_field() // "is_deleted"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Filtering
|
## Filtering
|
||||||
|
|
||||||
Soft delete does NOT auto-filter. Add filter manually:
|
Soft delete does NOT auto-filter. Add filter manually:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// Only active (non-deleted)
|
// Only non-deleted
|
||||||
let users = User::find(&pool, filters![("is_active", true)], None).await?;
|
let users = User::find(&pool, filters![("is_deleted", false)], None).await?;
|
||||||
|
|
||||||
// Only deleted
|
// Only deleted (trash)
|
||||||
let deleted = User::find(&pool, filters![("is_active", false)], None).await?;
|
let deleted = User::find(&pool, filters![("is_deleted", true)], None).await?;
|
||||||
|
|
||||||
// All records
|
// All records
|
||||||
let all = User::find(&pool, filters![], None).await?;
|
let all = User::find(&pool, filters![], None).await?;
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ futures = "0.3"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
static-check = []
|
static-validation = []
|
||||||
mysql = []
|
mysql = []
|
||||||
postgres = []
|
postgres = []
|
||||||
sqlite = []
|
sqlite = []
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ fn db_type() -> TokenStream2 {
|
||||||
}
|
}
|
||||||
#[cfg(feature = "sqlite")]
|
#[cfg(feature = "sqlite")]
|
||||||
{
|
{
|
||||||
return quote! { sqlx::Sqlite };
|
quote! { sqlx::Sqlite }
|
||||||
}
|
}
|
||||||
#[cfg(feature = "mysql")]
|
#[cfg(feature = "mysql")]
|
||||||
{
|
{
|
||||||
|
|
@ -82,7 +82,7 @@ fn db_arguments() -> TokenStream2 {
|
||||||
}
|
}
|
||||||
#[cfg(feature = "sqlite")]
|
#[cfg(feature = "sqlite")]
|
||||||
{
|
{
|
||||||
return quote! { sqlx::sqlite::SqliteArguments<'q> };
|
quote! { sqlx::sqlite::SqliteArguments<'static> }
|
||||||
}
|
}
|
||||||
#[cfg(feature = "mysql")]
|
#[cfg(feature = "mysql")]
|
||||||
{
|
{
|
||||||
|
|
@ -99,21 +99,13 @@ fn table_quote() -> &'static str {
|
||||||
#[cfg(feature = "postgres")]
|
#[cfg(feature = "postgres")]
|
||||||
{ "\"" }
|
{ "\"" }
|
||||||
#[cfg(feature = "sqlite")]
|
#[cfg(feature = "sqlite")]
|
||||||
{ return "\""; }
|
{ "\"" }
|
||||||
#[cfg(feature = "mysql")]
|
#[cfg(feature = "mysql")]
|
||||||
{ "`" }
|
{ "`" }
|
||||||
#[cfg(not(any(feature = "mysql", feature = "postgres", feature = "sqlite")))]
|
#[cfg(not(any(feature = "mysql", feature = "postgres", feature = "sqlite")))]
|
||||||
{ "`" }
|
{ "`" }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get compile-time placeholder for static-check SQL
|
|
||||||
fn static_placeholder(index: usize) -> String {
|
|
||||||
#[cfg(feature = "postgres")]
|
|
||||||
{ format!("${}", index) }
|
|
||||||
#[cfg(not(feature = "postgres"))]
|
|
||||||
{ let _ = index; "?".to_string() }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn derive_entity_internal(input: TokenStream) -> TokenStream {
|
fn derive_entity_internal(input: TokenStream) -> TokenStream {
|
||||||
let input = parse_macro_input!(input as DeriveInput);
|
let input = parse_macro_input!(input as DeriveInput);
|
||||||
let name = &input.ident;
|
let name = &input.ident;
|
||||||
|
|
@ -139,11 +131,10 @@ fn derive_entity_internal(input: TokenStream) -> TokenStream {
|
||||||
.or_else(|| fields.iter().find(|&f| is_version_field(f)));
|
.or_else(|| fields.iter().find(|&f| is_version_field(f)));
|
||||||
|
|
||||||
// Find soft delete field (by attribute or by name convention)
|
// Find soft delete field (by attribute or by name convention)
|
||||||
// Convention: `is_active` (FALSE = deleted), `is_deleted`/`deleted` (TRUE = deleted)
|
|
||||||
let soft_delete_field = fields.iter()
|
let soft_delete_field = fields.iter()
|
||||||
.find(|f| f.is_soft_delete)
|
.find(|f| f.is_soft_delete)
|
||||||
.or_else(|| fields.iter().find(|f| {
|
.or_else(|| fields.iter().find(|f| {
|
||||||
(f.ident == "is_active" || f.ident == "is_deleted" || f.ident == "deleted") &&
|
(f.ident == "is_deleted" || f.ident == "deleted") &&
|
||||||
matches!(&f.ty, Type::Path(p) if p.path.is_ident("bool"))
|
matches!(&f.ty, Type::Path(p) if p.path.is_ident("bool"))
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
@ -152,7 +143,6 @@ fn derive_entity_internal(input: TokenStream) -> TokenStream {
|
||||||
let get_impl = generate_get_impl(&name, &table_name, primary_key, version_field, soft_delete_field, &fields, &impl_generics, &ty_generics, &where_clause);
|
let get_impl = generate_get_impl(&name, &table_name, primary_key, version_field, soft_delete_field, &fields, &impl_generics, &ty_generics, &where_clause);
|
||||||
let update_impl = generate_update_impl(&name, &update_form_name, &table_name, &fields, primary_key, version_field, has_updated_at, &impl_generics, &ty_generics, &where_clause);
|
let update_impl = generate_update_impl(&name, &update_form_name, &table_name, &fields, primary_key, version_field, has_updated_at, &impl_generics, &ty_generics, &where_clause);
|
||||||
let diff_impl = generate_diff_impl(&name, &update_form_name, &fields, primary_key, version_field, &impl_generics, &ty_generics, &where_clause);
|
let diff_impl = generate_diff_impl(&name, &update_form_name, &fields, primary_key, version_field, &impl_generics, &ty_generics, &where_clause);
|
||||||
let delete_impl = generate_delete_impl(&name, &table_name, primary_key, &impl_generics, &ty_generics, &where_clause);
|
|
||||||
let soft_delete_impl = generate_soft_delete_impl(&name, &table_name, primary_key, soft_delete_field, &impl_generics, &ty_generics, &where_clause);
|
let soft_delete_impl = generate_soft_delete_impl(&name, &table_name, primary_key, soft_delete_field, &impl_generics, &ty_generics, &where_clause);
|
||||||
|
|
||||||
let pk_type = &primary_key.ty;
|
let pk_type = &primary_key.ty;
|
||||||
|
|
@ -163,7 +153,6 @@ fn derive_entity_internal(input: TokenStream) -> TokenStream {
|
||||||
#get_impl
|
#get_impl
|
||||||
#update_impl
|
#update_impl
|
||||||
#diff_impl
|
#diff_impl
|
||||||
#delete_impl
|
|
||||||
#soft_delete_impl
|
#soft_delete_impl
|
||||||
|
|
||||||
impl #impl_generics #name #ty_generics #where_clause {
|
impl #impl_generics #name #ty_generics #where_clause {
|
||||||
|
|
@ -370,13 +359,52 @@ fn generate_insert_impl(
|
||||||
.filter(|f| *f != #pk_db_name)
|
.filter(|f| *f != #pk_db_name)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let upsert_stmt = ::sqlx_record::prelude::build_upsert_stmt(
|
#[cfg(feature = "mysql")]
|
||||||
#table_name,
|
let upsert_stmt = {
|
||||||
&[#(#db_names),*],
|
let update_clause = non_pk_fields.iter()
|
||||||
#pk_db_name,
|
.map(|f| format!("{} = VALUES({})", f, f))
|
||||||
&non_pk_fields,
|
.collect::<Vec<_>>()
|
||||||
&placeholders,
|
.join(", ");
|
||||||
);
|
format!(
|
||||||
|
"INSERT INTO {}{}{} ({}) VALUES ({}) ON DUPLICATE KEY UPDATE {}",
|
||||||
|
#tq, #table_name, #tq,
|
||||||
|
vec![#(#db_names),*].join(", "),
|
||||||
|
placeholders,
|
||||||
|
update_clause
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "postgres")]
|
||||||
|
let upsert_stmt = {
|
||||||
|
let update_clause = non_pk_fields.iter()
|
||||||
|
.map(|f| format!("{} = EXCLUDED.{}", f, f))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ");
|
||||||
|
format!(
|
||||||
|
"INSERT INTO {}{}{} ({}) VALUES ({}) ON CONFLICT ({}) DO UPDATE SET {}",
|
||||||
|
#tq, #table_name, #tq,
|
||||||
|
vec![#(#db_names),*].join(", "),
|
||||||
|
placeholders,
|
||||||
|
#pk_db_name,
|
||||||
|
update_clause
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "sqlite")]
|
||||||
|
let upsert_stmt = {
|
||||||
|
let update_clause = non_pk_fields.iter()
|
||||||
|
.map(|f| format!("{} = excluded.{}", f, f))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ");
|
||||||
|
format!(
|
||||||
|
"INSERT INTO {}{}{} ({}) VALUES ({}) ON CONFLICT({}) DO UPDATE SET {}",
|
||||||
|
#tq, #table_name, #tq,
|
||||||
|
vec![#(#db_names),*].join(", "),
|
||||||
|
placeholders,
|
||||||
|
#pk_db_name,
|
||||||
|
update_clause
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
sqlx::query(&upsert_stmt)
|
sqlx::query(&upsert_stmt)
|
||||||
#(.bind(#bindings))*
|
#(.bind(#bindings))*
|
||||||
|
|
@ -535,18 +563,14 @@ fn generate_get_impl(
|
||||||
quote! {}
|
quote! {}
|
||||||
};
|
};
|
||||||
|
|
||||||
let field_list = fields.iter().map(|f| f.db_name.clone()).collect::<Vec<_>>();
|
// Check if static-validation feature is enabled at macro expansion time
|
||||||
|
let use_static_validation = cfg!(feature = "static-validation");
|
||||||
// Check if static-check feature is enabled at macro expansion time
|
|
||||||
let use_static_validation = cfg!(feature = "static-check");
|
|
||||||
|
|
||||||
let get_by_impl = if use_static_validation {
|
let get_by_impl = if use_static_validation {
|
||||||
// Static validation: use sqlx::query_as! with compile-time checked SQL
|
|
||||||
let select_stmt = format!(
|
let select_stmt = format!(
|
||||||
r#"SELECT DISTINCT {} FROM {}{}{} WHERE {} = {}"#,
|
r#"SELECT DISTINCT {} FROM {}{}{} WHERE {} = $1"#,
|
||||||
select_fields.clone().collect::<Vec<_>>().join(", "),
|
select_fields.clone().collect::<Vec<_>>().join(", "),
|
||||||
tq, table_name, tq, pk_db_field_name,
|
tq, table_name, tq, pk_db_field_name
|
||||||
static_placeholder(1)
|
|
||||||
);
|
);
|
||||||
quote! {
|
quote! {
|
||||||
pub async fn #get_by_func<'a, E>(executor: E, #pk_field: &#pk_type) -> Result<Option<Self>, sqlx::Error>
|
pub async fn #get_by_func<'a, E>(executor: E, #pk_field: &#pk_type) -> Result<Option<Self>, sqlx::Error>
|
||||||
|
|
@ -580,7 +604,8 @@ fn generate_get_impl(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Runtime: use sqlx::query_as with dynamic SQL
|
let field_list = fields.iter().map(|f| f.db_name.clone()).collect::<Vec<_>>();
|
||||||
|
|
||||||
quote! {
|
quote! {
|
||||||
pub async fn #get_by_func<'a, E>(executor: E, #pk_field: &#pk_type) -> Result<Option<Self>, sqlx::Error>
|
pub async fn #get_by_func<'a, E>(executor: E, #pk_field: &#pk_type) -> Result<Option<Self>, sqlx::Error>
|
||||||
where
|
where
|
||||||
|
|
@ -726,7 +751,13 @@ fn generate_get_impl(
|
||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
let index_clause = ::sqlx_record::prelude::build_index_clause(index);
|
// Index hints are MySQL-specific
|
||||||
|
#[cfg(feature = "mysql")]
|
||||||
|
let index_clause = index
|
||||||
|
.map(|idx| format!("USE INDEX ({})", idx))
|
||||||
|
.unwrap_or_default();
|
||||||
|
#[cfg(not(feature = "mysql"))]
|
||||||
|
let index_clause = { let _ = index; String::new() };
|
||||||
|
|
||||||
//Filter order_by fields to only those managed
|
//Filter order_by fields to only those managed
|
||||||
let fields = Self::select_fields().into_iter().collect::<::std::collections::HashSet<_>>();
|
let fields = Self::select_fields().into_iter().collect::<::std::collections::HashSet<_>>();
|
||||||
|
|
@ -790,8 +821,23 @@ fn generate_get_impl(
|
||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
let index_clause = ::sqlx_record::prelude::build_index_clause(index);
|
// Index hints are MySQL-specific
|
||||||
let count_expr = ::sqlx_record::prelude::build_count_expr(#pk_db_field_name);
|
#[cfg(feature = "mysql")]
|
||||||
|
let index_clause = index
|
||||||
|
.map(|idx| format!("USE INDEX ({})", idx))
|
||||||
|
.unwrap_or_default();
|
||||||
|
#[cfg(not(feature = "mysql"))]
|
||||||
|
let index_clause = { let _ = index; String::new() };
|
||||||
|
|
||||||
|
// Use database-appropriate COUNT syntax
|
||||||
|
#[cfg(feature = "postgres")]
|
||||||
|
let count_expr = format!("COUNT({})::BIGINT", #pk_db_field_name);
|
||||||
|
#[cfg(feature = "sqlite")]
|
||||||
|
let count_expr = format!("COUNT({})", #pk_db_field_name);
|
||||||
|
#[cfg(feature = "mysql")]
|
||||||
|
let count_expr = format!("CAST(COUNT({}) AS SIGNED)", #pk_db_field_name);
|
||||||
|
#[cfg(not(any(feature = "mysql", feature = "postgres", feature = "sqlite")))]
|
||||||
|
let count_expr = format!("COUNT({})", #pk_db_field_name);
|
||||||
|
|
||||||
let query = format!(
|
let query = format!(
|
||||||
r#"SELECT {} FROM {}{}{} {} {}"#,
|
r#"SELECT {} FROM {}{}{} {} {}"#,
|
||||||
|
|
@ -882,7 +928,13 @@ fn generate_get_impl(
|
||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
let index_clause = ::sqlx_record::prelude::build_index_clause(index);
|
// Index hints are MySQL-specific
|
||||||
|
#[cfg(feature = "mysql")]
|
||||||
|
let index_clause = index
|
||||||
|
.map(|idx| format!("USE INDEX ({})", idx))
|
||||||
|
.unwrap_or_default();
|
||||||
|
#[cfg(not(feature = "mysql"))]
|
||||||
|
let index_clause = { let _ = index; String::new() };
|
||||||
|
|
||||||
let query = format!(
|
let query = format!(
|
||||||
"SELECT DISTINCT {} FROM {}{}{} {} {}",
|
"SELECT DISTINCT {} FROM {}{}{} {} {}",
|
||||||
|
|
@ -1006,15 +1058,12 @@ fn generate_update_impl(
|
||||||
quote! {}
|
quote! {}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Auto-update updated_at timestamp (only if not manually set)
|
// Auto-update updated_at timestamp
|
||||||
let updated_at_increment = if has_updated_at {
|
let updated_at_increment = if has_updated_at {
|
||||||
quote! {
|
quote! {
|
||||||
// Only auto-set updated_at if not already set in form or via expression
|
parts.push(format!("updated_at = {}", ::sqlx_record::prelude::placeholder(idx)));
|
||||||
if self.updated_at.is_none() && !self._exprs.contains_key("updated_at") {
|
values.push(::sqlx_record::prelude::Value::Int64(chrono::Utc::now().timestamp_millis()));
|
||||||
parts.push(format!("updated_at = {}", ::sqlx_record::prelude::placeholder(idx)));
|
idx += 1;
|
||||||
values.push(::sqlx_record::prelude::Value::Int64(chrono::Utc::now().timestamp_millis()));
|
|
||||||
idx += 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
quote! {}
|
quote! {}
|
||||||
|
|
@ -1109,31 +1158,40 @@ fn generate_update_impl(
|
||||||
|
|
||||||
/// Bind all form values to query in correct order.
|
/// Bind all form values to query in correct order.
|
||||||
/// Handles both simple values and expression values, respecting expression precedence.
|
/// Handles both simple values and expression values, respecting expression precedence.
|
||||||
/// Uses Value enum for proper type handling of Option<T> fields.
|
pub fn bind_all_values(&self, mut query: sqlx::query::Query<'_, #db, #db_args>)
|
||||||
pub fn bind_all_values<'q>(&'q self, mut query: sqlx::query::Query<'q, #db, #db_args>)
|
-> sqlx::query::Query<'_, #db, #db_args>
|
||||||
-> sqlx::query::Query<'q, #db, #db_args>
|
|
||||||
{
|
{
|
||||||
// Use update_stmt_with_values to get properly converted values
|
#(
|
||||||
// This handles nested Options (Option<Option<T>>) correctly
|
// Expression takes precedence over simple value
|
||||||
let (_, values) = self.update_stmt_with_values();
|
if let Some(expr) = self._exprs.get(#db_names) {
|
||||||
for value in values {
|
let (_, expr_values) = expr.build_sql(#db_names, 1);
|
||||||
query = ::sqlx_record::prelude::bind_value_owned(query, value);
|
for value in expr_values {
|
||||||
}
|
query = ::sqlx_record::prelude::bind_value_owned(query, value);
|
||||||
|
}
|
||||||
|
} else if let Some(ref value) = self.#field_idents {
|
||||||
|
query = query.bind(value);
|
||||||
|
}
|
||||||
|
)*
|
||||||
query
|
query
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Legacy binding method - binds values through the Value enum for proper type handling.
|
/// Legacy binding method - only binds simple Option values (ignores expressions).
|
||||||
/// For backward compatibility. New code should use bind_all_values().
|
/// For backward compatibility. New code should use bind_all_values().
|
||||||
pub fn bind_form_values<'q>(&'q self, mut query: sqlx::query::Query<'q, #db, #db_args>)
|
pub fn bind_form_values<'q>(&'q self, mut query: sqlx::query::Query<'q, #db, #db_args>)
|
||||||
-> sqlx::query::Query<'q, #db, #db_args>
|
-> sqlx::query::Query<'q, #db, #db_args>
|
||||||
{
|
{
|
||||||
// Always use Value-based binding to properly handle Option<T> fields
|
if self._exprs.is_empty() {
|
||||||
// This ensures nested Options (Option<Option<T>>) are unwrapped correctly
|
// No expressions, use simple binding
|
||||||
let (_, values) = self.update_stmt_with_values();
|
#(
|
||||||
for value in values {
|
if let Some(ref value) = self.#field_idents {
|
||||||
query = ::sqlx_record::prelude::bind_value_owned(query, value);
|
query = query.bind(value);
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
query
|
||||||
|
} else {
|
||||||
|
// Has expressions, use full binding
|
||||||
|
self.bind_all_values(query)
|
||||||
}
|
}
|
||||||
query
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if this form uses any expressions
|
/// Check if this form uses any expressions
|
||||||
|
|
@ -1264,7 +1322,6 @@ fn generate_diff_impl(
|
||||||
pub fn to_update_form(&self) -> #update_form_name #ty_generics {
|
pub fn to_update_form(&self) -> #update_form_name #ty_generics {
|
||||||
#update_form_name {
|
#update_form_name {
|
||||||
#(#field_idents: Some(self.#field_idents.clone()),)*
|
#(#field_idents: Some(self.#field_idents.clone()),)*
|
||||||
_exprs: std::collections::HashMap::new(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1399,97 +1456,6 @@ fn generate_diff_impl(
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update all records matching the filter conditions
|
|
||||||
/// Returns the number of affected rows
|
|
||||||
pub async fn update_by_filter<'a, E>(
|
|
||||||
executor: E,
|
|
||||||
filters: Vec<::sqlx_record::prelude::Filter<'a>>,
|
|
||||||
form: #update_form_name,
|
|
||||||
) -> Result<u64, sqlx::Error>
|
|
||||||
where
|
|
||||||
E: sqlx::Executor<'a, Database=#db>,
|
|
||||||
{
|
|
||||||
use ::sqlx_record::prelude::{Filter, bind_values};
|
|
||||||
|
|
||||||
if filters.is_empty() {
|
|
||||||
// Require at least one filter to prevent accidental table-wide updates
|
|
||||||
return Err(sqlx::Error::Protocol(
|
|
||||||
"update_by_filter requires at least one filter to prevent accidental table-wide updates".to_string()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let (update_stmt, form_values) = form.update_stmt_with_values();
|
|
||||||
if update_stmt.is_empty() {
|
|
||||||
return Ok(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
let form_param_count = form_values.len();
|
|
||||||
let (where_conditions, filter_values) = Filter::build_where_clause_with_offset(&filters, form_param_count + 1);
|
|
||||||
|
|
||||||
let query_str = format!(
|
|
||||||
r#"UPDATE {}{}{} SET {} WHERE {}"#,
|
|
||||||
#tq, Self::table_name(), #tq,
|
|
||||||
update_stmt,
|
|
||||||
where_conditions,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Combine form values and filter values
|
|
||||||
let mut all_values = form_values;
|
|
||||||
all_values.extend(filter_values);
|
|
||||||
|
|
||||||
let query = sqlx::query(&query_str);
|
|
||||||
let result = bind_values(query, &all_values)
|
|
||||||
.execute(executor)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(result.rows_affected())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate delete implementation - always generated for ALL entities
|
|
||||||
fn generate_delete_impl(
|
|
||||||
name: &Ident,
|
|
||||||
table_name: &str,
|
|
||||||
primary_key: &EntityField,
|
|
||||||
impl_generics: &ImplGenerics,
|
|
||||||
ty_generics: &TypeGenerics,
|
|
||||||
where_clause: &Option<&WhereClause>,
|
|
||||||
) -> TokenStream2 {
|
|
||||||
let pk_field = &primary_key.ident;
|
|
||||||
let pk_type = &primary_key.ty;
|
|
||||||
let pk_db_name = &primary_key.db_name;
|
|
||||||
let db = db_type();
|
|
||||||
let tq = table_quote();
|
|
||||||
|
|
||||||
let pk_field_name = primary_key.ident.to_string();
|
|
||||||
let hard_delete_by_func = format_ident!("hard_delete_by_{}", pk_field_name);
|
|
||||||
|
|
||||||
quote! {
|
|
||||||
impl #impl_generics #name #ty_generics #where_clause {
|
|
||||||
/// Hard delete - permanently removes the row from database
|
|
||||||
pub async fn hard_delete<'a, E>(&self, executor: E) -> Result<(), sqlx::Error>
|
|
||||||
where
|
|
||||||
E: sqlx::Executor<'a, Database = #db>,
|
|
||||||
{
|
|
||||||
Self::#hard_delete_by_func(executor, &self.#pk_field).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Hard delete by primary key - permanently removes the row from database
|
|
||||||
pub async fn #hard_delete_by_func<'a, E>(executor: E, #pk_field: &#pk_type) -> Result<(), sqlx::Error>
|
|
||||||
where
|
|
||||||
E: sqlx::Executor<'a, Database = #db>,
|
|
||||||
{
|
|
||||||
let query = format!(
|
|
||||||
"DELETE FROM {}{}{} WHERE {} = {}",
|
|
||||||
#tq, #table_name, #tq,
|
|
||||||
#pk_db_name, ::sqlx_record::prelude::placeholder(1)
|
|
||||||
);
|
|
||||||
sqlx::query(&query).bind(#pk_field).execute(executor).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1516,41 +1482,51 @@ fn generate_soft_delete_impl(
|
||||||
let tq = table_quote();
|
let tq = table_quote();
|
||||||
|
|
||||||
let pk_field_name = primary_key.ident.to_string();
|
let pk_field_name = primary_key.ident.to_string();
|
||||||
let soft_delete_by_func = format_ident!("soft_delete_by_{}", pk_field_name);
|
let delete_by_func = format_ident!("delete_by_{}", pk_field_name);
|
||||||
|
let hard_delete_by_func = format_ident!("hard_delete_by_{}", pk_field_name);
|
||||||
let restore_by_func = format_ident!("restore_by_{}", pk_field_name);
|
let restore_by_func = format_ident!("restore_by_{}", pk_field_name);
|
||||||
|
|
||||||
// Determine semantics based on field name and attribute:
|
|
||||||
// - #[soft_delete] attribute: field should be FALSE when deleted (user convention)
|
|
||||||
// - `is_active` by name: FALSE when deleted, TRUE when active
|
|
||||||
// - `is_deleted`/`deleted` by name: TRUE when deleted, FALSE when active
|
|
||||||
let sd_field_name = sd_field.ident.to_string();
|
|
||||||
let is_inverted = sd_field.is_soft_delete || sd_field_name == "is_active";
|
|
||||||
|
|
||||||
let (delete_value, restore_value) = if is_inverted {
|
|
||||||
("FALSE", "TRUE")
|
|
||||||
} else {
|
|
||||||
("TRUE", "FALSE")
|
|
||||||
};
|
|
||||||
|
|
||||||
quote! {
|
quote! {
|
||||||
impl #impl_generics #name #ty_generics #where_clause {
|
impl #impl_generics #name #ty_generics #where_clause {
|
||||||
/// Soft delete - marks record as deleted without removing from database
|
/// Soft delete - sets the soft_delete field to true
|
||||||
pub async fn soft_delete<'a, E>(&self, executor: E) -> Result<(), sqlx::Error>
|
pub async fn delete<'a, E>(&self, executor: E) -> Result<(), sqlx::Error>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database = #db>,
|
E: sqlx::Executor<'a, Database = #db>,
|
||||||
{
|
{
|
||||||
Self::#soft_delete_by_func(executor, &self.#pk_field).await
|
Self::#delete_by_func(executor, &self.#pk_field).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Soft delete by primary key
|
/// Soft delete by primary key
|
||||||
pub async fn #soft_delete_by_func<'a, E>(executor: E, #pk_field: &#pk_type) -> Result<(), sqlx::Error>
|
pub async fn #delete_by_func<'a, E>(executor: E, #pk_field: &#pk_type) -> Result<(), sqlx::Error>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database = #db>,
|
E: sqlx::Executor<'a, Database = #db>,
|
||||||
{
|
{
|
||||||
let query = format!(
|
let query = format!(
|
||||||
"UPDATE {}{}{} SET {} = {} WHERE {} = {}",
|
"UPDATE {}{}{} SET {} = TRUE WHERE {} = {}",
|
||||||
|
#tq, #table_name, #tq,
|
||||||
|
#sd_db_name,
|
||||||
|
#pk_db_name, ::sqlx_record::prelude::placeholder(1)
|
||||||
|
);
|
||||||
|
sqlx::query(&query).bind(#pk_field).execute(executor).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hard delete - permanently removes the row from database
|
||||||
|
pub async fn hard_delete<'a, E>(&self, executor: E) -> Result<(), sqlx::Error>
|
||||||
|
where
|
||||||
|
E: sqlx::Executor<'a, Database = #db>,
|
||||||
|
{
|
||||||
|
Self::#hard_delete_by_func(executor, &self.#pk_field).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hard delete by primary key
|
||||||
|
pub async fn #hard_delete_by_func<'a, E>(executor: E, #pk_field: &#pk_type) -> Result<(), sqlx::Error>
|
||||||
|
where
|
||||||
|
E: sqlx::Executor<'a, Database = #db>,
|
||||||
|
{
|
||||||
|
let query = format!(
|
||||||
|
"DELETE FROM {}{}{} WHERE {} = {}",
|
||||||
#tq, #table_name, #tq,
|
#tq, #table_name, #tq,
|
||||||
#sd_db_name, #delete_value,
|
|
||||||
#pk_db_name, ::sqlx_record::prelude::placeholder(1)
|
#pk_db_name, ::sqlx_record::prelude::placeholder(1)
|
||||||
);
|
);
|
||||||
sqlx::query(&query).bind(#pk_field).execute(executor).await?;
|
sqlx::query(&query).bind(#pk_field).execute(executor).await?;
|
||||||
|
|
@ -1571,9 +1547,9 @@ fn generate_soft_delete_impl(
|
||||||
E: sqlx::Executor<'a, Database = #db>,
|
E: sqlx::Executor<'a, Database = #db>,
|
||||||
{
|
{
|
||||||
let query = format!(
|
let query = format!(
|
||||||
"UPDATE {}{}{} SET {} = {} WHERE {} = {}",
|
"UPDATE {}{}{} SET {} = FALSE WHERE {} = {}",
|
||||||
#tq, #table_name, #tq,
|
#tq, #table_name, #tq,
|
||||||
#sd_db_name, #restore_value,
|
#sd_db_name,
|
||||||
#pk_db_name, ::sqlx_record::prelude::placeholder(1)
|
#pk_db_name, ::sqlx_record::prelude::placeholder(1)
|
||||||
);
|
);
|
||||||
sqlx::query(&query).bind(#pk_field).execute(executor).await?;
|
sqlx::query(&query).bind(#pk_field).execute(executor).await?;
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
use sqlx::pool::PoolConnection;
|
use sqlx::pool::PoolConnection;
|
||||||
|
|
||||||
#[cfg(feature = "mysql")]
|
#[cfg(feature = "mysql")]
|
||||||
use sqlx::{MySql, MySqlConnection, MySqlPool, Transaction};
|
use sqlx::{MySql, MySqlPool};
|
||||||
|
|
||||||
#[cfg(feature = "postgres")]
|
#[cfg(feature = "postgres")]
|
||||||
use sqlx::{Postgres, PgConnection, PgPool, Transaction};
|
use sqlx::{Postgres, PgPool};
|
||||||
|
|
||||||
#[cfg(feature = "sqlite")]
|
#[cfg(feature = "sqlite")]
|
||||||
use sqlx::{Sqlite, SqliteConnection, SqlitePool, Transaction};
|
use sqlx::{Sqlite, SqlitePool};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// MySQL Implementation
|
// MySQL Implementation
|
||||||
|
|
@ -24,10 +24,6 @@ pub enum ConnProvider<'a> {
|
||||||
pool: MySqlPool,
|
pool: MySqlPool,
|
||||||
conn: Option<PoolConnection<MySql>>,
|
conn: Option<PoolConnection<MySql>>,
|
||||||
},
|
},
|
||||||
/// Stores a reference to a transaction
|
|
||||||
Transaction {
|
|
||||||
tx: &'a mut Transaction<'static, MySql>,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "mysql")]
|
#[cfg(feature = "mysql")]
|
||||||
|
|
@ -42,25 +38,18 @@ impl<'a> ConnProvider<'a> {
|
||||||
ConnProvider::Owned { pool, conn: None }
|
ConnProvider::Owned { pool, conn: None }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a ConnProvider from a borrowed transaction reference
|
|
||||||
pub fn from_tx(tx: &'a mut Transaction<'static, MySql>) -> Self {
|
|
||||||
ConnProvider::Transaction { tx }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a mutable reference to the underlying connection.
|
/// Get a mutable reference to the underlying connection.
|
||||||
/// For borrowed connections, returns the reference directly.
|
/// For borrowed connections, returns the reference directly.
|
||||||
/// For owned connections, lazily acquires from pool on first call.
|
/// For owned connections, lazily acquires from pool on first call.
|
||||||
/// For transactions, returns the transaction's underlying connection.
|
pub async fn get_conn(&mut self) -> Result<&mut PoolConnection<MySql>, sqlx::Error> {
|
||||||
pub async fn get_conn(&mut self) -> Result<&mut MySqlConnection, sqlx::Error> {
|
|
||||||
match self {
|
match self {
|
||||||
ConnProvider::Borrowed { conn } => Ok(&mut **conn),
|
ConnProvider::Borrowed { conn } => Ok(conn),
|
||||||
ConnProvider::Owned { pool, conn } => {
|
ConnProvider::Owned { pool, conn } => {
|
||||||
if conn.is_none() {
|
if conn.is_none() {
|
||||||
*conn = Some(pool.acquire().await?);
|
*conn = Some(pool.acquire().await?);
|
||||||
}
|
}
|
||||||
Ok(&mut **conn.as_mut().unwrap())
|
Ok(conn.as_mut().unwrap())
|
||||||
}
|
}
|
||||||
ConnProvider::Transaction { tx } => Ok(&mut **tx),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -80,10 +69,6 @@ pub enum ConnProvider<'a> {
|
||||||
pool: PgPool,
|
pool: PgPool,
|
||||||
conn: Option<PoolConnection<Postgres>>,
|
conn: Option<PoolConnection<Postgres>>,
|
||||||
},
|
},
|
||||||
/// Stores a reference to a transaction
|
|
||||||
Transaction {
|
|
||||||
tx: &'a mut Transaction<'static, Postgres>,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "postgres")]
|
#[cfg(feature = "postgres")]
|
||||||
|
|
@ -98,25 +83,18 @@ impl<'a> ConnProvider<'a> {
|
||||||
ConnProvider::Owned { pool, conn: None }
|
ConnProvider::Owned { pool, conn: None }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a ConnProvider from a borrowed transaction reference
|
|
||||||
pub fn from_tx(tx: &'a mut Transaction<'static, Postgres>) -> Self {
|
|
||||||
ConnProvider::Transaction { tx }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a mutable reference to the underlying connection.
|
/// Get a mutable reference to the underlying connection.
|
||||||
/// For borrowed connections, returns the reference directly.
|
/// For borrowed connections, returns the reference directly.
|
||||||
/// For owned connections, lazily acquires from pool on first call.
|
/// For owned connections, lazily acquires from pool on first call.
|
||||||
/// For transactions, returns the transaction's underlying connection.
|
pub async fn get_conn(&mut self) -> Result<&mut PoolConnection<Postgres>, sqlx::Error> {
|
||||||
pub async fn get_conn(&mut self) -> Result<&mut PgConnection, sqlx::Error> {
|
|
||||||
match self {
|
match self {
|
||||||
ConnProvider::Borrowed { conn } => Ok(&mut **conn),
|
ConnProvider::Borrowed { conn } => Ok(conn),
|
||||||
ConnProvider::Owned { pool, conn } => {
|
ConnProvider::Owned { pool, conn } => {
|
||||||
if conn.is_none() {
|
if conn.is_none() {
|
||||||
*conn = Some(pool.acquire().await?);
|
*conn = Some(pool.acquire().await?);
|
||||||
}
|
}
|
||||||
Ok(&mut **conn.as_mut().unwrap())
|
Ok(conn.as_mut().unwrap())
|
||||||
}
|
}
|
||||||
ConnProvider::Transaction { tx } => Ok(&mut **tx),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -136,10 +114,6 @@ pub enum ConnProvider<'a> {
|
||||||
pool: SqlitePool,
|
pool: SqlitePool,
|
||||||
conn: Option<PoolConnection<Sqlite>>,
|
conn: Option<PoolConnection<Sqlite>>,
|
||||||
},
|
},
|
||||||
/// Stores a reference to a transaction
|
|
||||||
Transaction {
|
|
||||||
tx: &'a mut Transaction<'static, Sqlite>,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "sqlite")]
|
#[cfg(feature = "sqlite")]
|
||||||
|
|
@ -154,25 +128,18 @@ impl<'a> ConnProvider<'a> {
|
||||||
ConnProvider::Owned { pool, conn: None }
|
ConnProvider::Owned { pool, conn: None }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a ConnProvider from a borrowed transaction reference
|
|
||||||
pub fn from_tx(tx: &'a mut Transaction<'static, Sqlite>) -> Self {
|
|
||||||
ConnProvider::Transaction { tx }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a mutable reference to the underlying connection.
|
/// Get a mutable reference to the underlying connection.
|
||||||
/// For borrowed connections, returns the reference directly.
|
/// For borrowed connections, returns the reference directly.
|
||||||
/// For owned connections, lazily acquires from pool on first call.
|
/// For owned connections, lazily acquires from pool on first call.
|
||||||
/// For transactions, returns the transaction's underlying connection.
|
pub async fn get_conn(&mut self) -> Result<&mut PoolConnection<Sqlite>, sqlx::Error> {
|
||||||
pub async fn get_conn(&mut self) -> Result<&mut SqliteConnection, sqlx::Error> {
|
|
||||||
match self {
|
match self {
|
||||||
ConnProvider::Borrowed { conn } => Ok(&mut **conn),
|
ConnProvider::Borrowed { conn } => Ok(conn),
|
||||||
ConnProvider::Owned { pool, conn } => {
|
ConnProvider::Owned { pool, conn } => {
|
||||||
if conn.is_none() {
|
if conn.is_none() {
|
||||||
*conn = Some(pool.acquire().await?);
|
*conn = Some(pool.acquire().await?);
|
||||||
}
|
}
|
||||||
Ok(&mut **conn.as_mut().unwrap())
|
Ok(conn.as_mut().unwrap())
|
||||||
}
|
}
|
||||||
ConnProvider::Transaction { tx } => Ok(&mut **tx),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
115
src/filter.rs
115
src/filter.rs
|
|
@ -111,121 +111,6 @@ pub fn placeholder(index: usize) -> String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the table quote character for the current database
|
|
||||||
#[inline]
|
|
||||||
pub fn table_quote() -> &'static str {
|
|
||||||
#[cfg(feature = "mysql")]
|
|
||||||
{ "`" }
|
|
||||||
#[cfg(feature = "postgres")]
|
|
||||||
{ "\"" }
|
|
||||||
#[cfg(feature = "sqlite")]
|
|
||||||
{ "\"" }
|
|
||||||
#[cfg(not(any(feature = "mysql", feature = "postgres", feature = "sqlite")))]
|
|
||||||
{ "`" }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builds an index hint clause (MySQL-specific, empty for other databases)
|
|
||||||
#[inline]
|
|
||||||
pub fn build_index_clause(index: Option<&str>) -> String {
|
|
||||||
#[cfg(feature = "mysql")]
|
|
||||||
{
|
|
||||||
index.map(|idx| format!("USE INDEX ({})", idx)).unwrap_or_default()
|
|
||||||
}
|
|
||||||
#[cfg(not(feature = "mysql"))]
|
|
||||||
{
|
|
||||||
let _ = index;
|
|
||||||
String::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builds a COUNT expression appropriate for the database backend
|
|
||||||
#[inline]
|
|
||||||
pub fn build_count_expr(field: &str) -> String {
|
|
||||||
#[cfg(feature = "postgres")]
|
|
||||||
{
|
|
||||||
format!("COUNT({})::BIGINT", field)
|
|
||||||
}
|
|
||||||
#[cfg(feature = "sqlite")]
|
|
||||||
{
|
|
||||||
format!("COUNT({})", field)
|
|
||||||
}
|
|
||||||
#[cfg(feature = "mysql")]
|
|
||||||
{
|
|
||||||
format!("CAST(COUNT({}) AS SIGNED)", field)
|
|
||||||
}
|
|
||||||
#[cfg(not(any(feature = "mysql", feature = "postgres", feature = "sqlite")))]
|
|
||||||
{
|
|
||||||
format!("COUNT({})", field)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builds an upsert statement for the current database backend
|
|
||||||
pub fn build_upsert_stmt(
|
|
||||||
table_name: &str,
|
|
||||||
all_fields: &[&str],
|
|
||||||
pk_field: &str,
|
|
||||||
non_pk_fields: &[&str],
|
|
||||||
placeholders: &str,
|
|
||||||
) -> String {
|
|
||||||
let tq = table_quote();
|
|
||||||
let fields_str = all_fields.join(", ");
|
|
||||||
|
|
||||||
#[cfg(feature = "mysql")]
|
|
||||||
{
|
|
||||||
let _ = pk_field; // Not used in MySQL ON DUPLICATE KEY syntax
|
|
||||||
let update_clause = non_pk_fields
|
|
||||||
.iter()
|
|
||||||
.map(|f| format!("{} = VALUES({})", f, f))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(", ");
|
|
||||||
format!(
|
|
||||||
"INSERT INTO {}{}{} ({}) VALUES ({}) ON DUPLICATE KEY UPDATE {}",
|
|
||||||
tq, table_name, tq, fields_str, placeholders, update_clause
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "postgres")]
|
|
||||||
{
|
|
||||||
let update_clause = non_pk_fields
|
|
||||||
.iter()
|
|
||||||
.map(|f| format!("{} = EXCLUDED.{}", f, f))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(", ");
|
|
||||||
format!(
|
|
||||||
"INSERT INTO {}{}{} ({}) VALUES ({}) ON CONFLICT ({}) DO UPDATE SET {}",
|
|
||||||
tq, table_name, tq, fields_str, placeholders, pk_field, update_clause
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "sqlite")]
|
|
||||||
{
|
|
||||||
let update_clause = non_pk_fields
|
|
||||||
.iter()
|
|
||||||
.map(|f| format!("{} = excluded.{}", f, f))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(", ");
|
|
||||||
format!(
|
|
||||||
"INSERT INTO {}{}{} ({}) VALUES ({}) ON CONFLICT({}) DO UPDATE SET {}",
|
|
||||||
tq, table_name, tq, fields_str, placeholders, pk_field, update_clause
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(any(feature = "mysql", feature = "postgres", feature = "sqlite")))]
|
|
||||||
{
|
|
||||||
let _ = pk_field; // Not used in MySQL ON DUPLICATE KEY syntax
|
|
||||||
// Fallback to MySQL syntax
|
|
||||||
let update_clause = non_pk_fields
|
|
||||||
.iter()
|
|
||||||
.map(|f| format!("{} = VALUES({})", f, f))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(", ");
|
|
||||||
format!(
|
|
||||||
"INSERT INTO {}{}{} ({}) VALUES ({}) ON DUPLICATE KEY UPDATE {}",
|
|
||||||
tq, table_name, tq, fields_str, placeholders, update_clause
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Filter<'_> {
|
impl Filter<'_> {
|
||||||
/// Returns the number of bind parameters this filter will use
|
/// Returns the number of bind parameters this filter will use
|
||||||
pub fn param_count(&self) -> usize {
|
pub fn param_count(&self) -> usize {
|
||||||
|
|
|
||||||
|
|
@ -181,7 +181,6 @@ pub mod prelude {
|
||||||
pub use crate::values;
|
pub use crate::values;
|
||||||
pub use crate::{new_uuid, lookup_table, lookup_options, transaction};
|
pub use crate::{new_uuid, lookup_table, lookup_options, transaction};
|
||||||
pub use crate::pagination::{Page, PageRequest};
|
pub use crate::pagination::{Page, PageRequest};
|
||||||
pub use crate::conn_provider::*;
|
|
||||||
|
|
||||||
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
|
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
|
||||||
pub use crate::conn_provider::ConnProvider;
|
pub use crate::conn_provider::ConnProvider;
|
||||||
|
|
|
||||||
104
src/value.rs
104
src/value.rs
|
|
@ -1,5 +1,5 @@
|
||||||
use sqlx::query::{Query, QueryAs, QueryScalar};
|
use sqlx::query::{Query, QueryAs, QueryScalar};
|
||||||
use sqlx::types::chrono::{NaiveDate, NaiveDateTime, NaiveTime};
|
use sqlx::types::chrono::{NaiveDate, NaiveDateTime};
|
||||||
use crate::filter::placeholder;
|
use crate::filter::placeholder;
|
||||||
|
|
||||||
// Database type alias based on enabled feature
|
// Database type alias based on enabled feature
|
||||||
|
|
@ -34,7 +34,6 @@ pub type Arguments_<'q> = sqlx::postgres::PgArguments;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Value {
|
pub enum Value {
|
||||||
Null,
|
|
||||||
Int8(i8),
|
Int8(i8),
|
||||||
Uint8(u8),
|
Uint8(u8),
|
||||||
Int16(i16),
|
Int16(i16),
|
||||||
|
|
@ -43,18 +42,12 @@ pub enum Value {
|
||||||
Uint32(u32),
|
Uint32(u32),
|
||||||
Int64(i64),
|
Int64(i64),
|
||||||
Uint64(u64),
|
Uint64(u64),
|
||||||
Float32(f32),
|
|
||||||
Float64(f64),
|
|
||||||
VecU8(Vec<u8>),
|
VecU8(Vec<u8>),
|
||||||
String(String),
|
String(String),
|
||||||
Bool(bool),
|
Bool(bool),
|
||||||
Uuid(uuid::Uuid),
|
Uuid(uuid::Uuid),
|
||||||
NaiveDate(NaiveDate),
|
NaiveDate(NaiveDate),
|
||||||
NaiveDateTime(NaiveDateTime),
|
NaiveDateTime(NaiveDateTime),
|
||||||
NaiveTime(NaiveTime),
|
|
||||||
Json(serde_json::Value),
|
|
||||||
#[cfg(feature = "decimal")]
|
|
||||||
Decimal(rust_decimal::Decimal),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Expression for column updates beyond simple value assignment.
|
/// Expression for column updates beyond simple value assignment.
|
||||||
|
|
@ -256,12 +249,10 @@ impl UpdateExpr {
|
||||||
pub type SqlValue = Value;
|
pub type SqlValue = Value;
|
||||||
|
|
||||||
// MySQL supports unsigned integers natively
|
// MySQL supports unsigned integers natively
|
||||||
// Note: UUID is bound as bytes for BINARY(16) column compatibility
|
|
||||||
#[cfg(feature = "mysql")]
|
#[cfg(feature = "mysql")]
|
||||||
macro_rules! bind_value {
|
macro_rules! bind_value {
|
||||||
($query:expr, $value: expr) => {{
|
($query:expr, $value: expr) => {{
|
||||||
let query = match $value {
|
let query = match $value {
|
||||||
Value::Null => $query.bind(None::<String>),
|
|
||||||
Value::Int8(v) => $query.bind(v),
|
Value::Int8(v) => $query.bind(v),
|
||||||
Value::Uint8(v) => $query.bind(v),
|
Value::Uint8(v) => $query.bind(v),
|
||||||
Value::Int16(v) => $query.bind(v),
|
Value::Int16(v) => $query.bind(v),
|
||||||
|
|
@ -270,18 +261,12 @@ macro_rules! bind_value {
|
||||||
Value::Uint32(v) => $query.bind(v),
|
Value::Uint32(v) => $query.bind(v),
|
||||||
Value::Int64(v) => $query.bind(v),
|
Value::Int64(v) => $query.bind(v),
|
||||||
Value::Uint64(v) => $query.bind(v),
|
Value::Uint64(v) => $query.bind(v),
|
||||||
Value::Float32(v) => $query.bind(v),
|
|
||||||
Value::Float64(v) => $query.bind(v),
|
|
||||||
Value::VecU8(v) => $query.bind(v),
|
Value::VecU8(v) => $query.bind(v),
|
||||||
Value::String(v) => $query.bind(v),
|
Value::String(v) => $query.bind(v),
|
||||||
Value::Bool(v) => $query.bind(v),
|
Value::Bool(v) => $query.bind(v),
|
||||||
Value::Uuid(v) => $query.bind(v),
|
Value::Uuid(v) => $query.bind(v),
|
||||||
Value::NaiveDate(v) => $query.bind(v),
|
Value::NaiveDate(v) => $query.bind(v),
|
||||||
Value::NaiveDateTime(v) => $query.bind(v),
|
Value::NaiveDateTime(v) => $query.bind(v),
|
||||||
Value::NaiveTime(v) => $query.bind(v),
|
|
||||||
Value::Json(v) => $query.bind(v),
|
|
||||||
#[cfg(feature = "decimal")]
|
|
||||||
Value::Decimal(v) => $query.bind(v),
|
|
||||||
};
|
};
|
||||||
query
|
query
|
||||||
}};
|
}};
|
||||||
|
|
@ -292,7 +277,6 @@ macro_rules! bind_value {
|
||||||
macro_rules! bind_value {
|
macro_rules! bind_value {
|
||||||
($query:expr, $value: expr) => {{
|
($query:expr, $value: expr) => {{
|
||||||
let query = match $value {
|
let query = match $value {
|
||||||
Value::Null => $query.bind(None::<String>),
|
|
||||||
Value::Int8(v) => $query.bind(v),
|
Value::Int8(v) => $query.bind(v),
|
||||||
Value::Uint8(v) => $query.bind(*v as i16),
|
Value::Uint8(v) => $query.bind(*v as i16),
|
||||||
Value::Int16(v) => $query.bind(v),
|
Value::Int16(v) => $query.bind(v),
|
||||||
|
|
@ -301,18 +285,12 @@ macro_rules! bind_value {
|
||||||
Value::Uint32(v) => $query.bind(*v as i64),
|
Value::Uint32(v) => $query.bind(*v as i64),
|
||||||
Value::Int64(v) => $query.bind(v),
|
Value::Int64(v) => $query.bind(v),
|
||||||
Value::Uint64(v) => $query.bind(*v as i64),
|
Value::Uint64(v) => $query.bind(*v as i64),
|
||||||
Value::Float32(v) => $query.bind(v),
|
|
||||||
Value::Float64(v) => $query.bind(v),
|
|
||||||
Value::VecU8(v) => $query.bind(v),
|
Value::VecU8(v) => $query.bind(v),
|
||||||
Value::String(v) => $query.bind(v),
|
Value::String(v) => $query.bind(v),
|
||||||
Value::Bool(v) => $query.bind(v),
|
Value::Bool(v) => $query.bind(v),
|
||||||
Value::Uuid(v) => $query.bind(v),
|
Value::Uuid(v) => $query.bind(v),
|
||||||
Value::NaiveDate(v) => $query.bind(v),
|
Value::NaiveDate(v) => $query.bind(v),
|
||||||
Value::NaiveDateTime(v) => $query.bind(v),
|
Value::NaiveDateTime(v) => $query.bind(v),
|
||||||
Value::NaiveTime(v) => $query.bind(v),
|
|
||||||
Value::Json(v) => $query.bind(v),
|
|
||||||
#[cfg(feature = "decimal")]
|
|
||||||
Value::Decimal(v) => $query.bind(v),
|
|
||||||
};
|
};
|
||||||
query
|
query
|
||||||
}};
|
}};
|
||||||
|
|
@ -331,13 +309,10 @@ pub fn bind_values<'q>(query: Query<'q, DB, Arguments_<'q>>, values: &'q [Value]
|
||||||
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
|
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
|
||||||
pub fn bind_value_owned<'q>(query: Query<'q, DB, Arguments_<'q>>, value: Value) -> Query<'q, DB, Arguments_<'q>> {
|
pub fn bind_value_owned<'q>(query: Query<'q, DB, Arguments_<'q>>, value: Value) -> Query<'q, DB, Arguments_<'q>> {
|
||||||
match value {
|
match value {
|
||||||
Value::Null => query.bind(None::<String>),
|
|
||||||
Value::Int8(v) => query.bind(v),
|
Value::Int8(v) => query.bind(v),
|
||||||
Value::Int16(v) => query.bind(v),
|
Value::Int16(v) => query.bind(v),
|
||||||
Value::Int32(v) => query.bind(v),
|
Value::Int32(v) => query.bind(v),
|
||||||
Value::Int64(v) => query.bind(v),
|
Value::Int64(v) => query.bind(v),
|
||||||
Value::Float32(v) => query.bind(v),
|
|
||||||
Value::Float64(v) => query.bind(v),
|
|
||||||
#[cfg(feature = "mysql")]
|
#[cfg(feature = "mysql")]
|
||||||
Value::Uint8(v) => query.bind(v),
|
Value::Uint8(v) => query.bind(v),
|
||||||
#[cfg(feature = "mysql")]
|
#[cfg(feature = "mysql")]
|
||||||
|
|
@ -360,10 +335,6 @@ pub fn bind_value_owned<'q>(query: Query<'q, DB, Arguments_<'q>>, value: Value)
|
||||||
Value::Uuid(v) => query.bind(v),
|
Value::Uuid(v) => query.bind(v),
|
||||||
Value::NaiveDate(v) => query.bind(v),
|
Value::NaiveDate(v) => query.bind(v),
|
||||||
Value::NaiveDateTime(v) => query.bind(v),
|
Value::NaiveDateTime(v) => query.bind(v),
|
||||||
Value::NaiveTime(v) => query.bind(v),
|
|
||||||
Value::Json(v) => query.bind(v),
|
|
||||||
#[cfg(feature = "decimal")]
|
|
||||||
Value::Decimal(v) => query.bind(v),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -559,79 +530,6 @@ impl From<&NaiveDateTime> for Value {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// New type implementations
|
|
||||||
impl From<f32> for Value {
|
|
||||||
fn from(value: f32) -> Self {
|
|
||||||
Value::Float32(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&f32> for Value {
|
|
||||||
fn from(value: &f32) -> Self {
|
|
||||||
Value::Float32(*value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<f64> for Value {
|
|
||||||
fn from(value: f64) -> Self {
|
|
||||||
Value::Float64(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&f64> for Value {
|
|
||||||
fn from(value: &f64) -> Self {
|
|
||||||
Value::Float64(*value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<NaiveTime> for Value {
|
|
||||||
fn from(value: NaiveTime) -> Self {
|
|
||||||
Value::NaiveTime(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&NaiveTime> for Value {
|
|
||||||
fn from(value: &NaiveTime) -> Self {
|
|
||||||
Value::NaiveTime(*value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<serde_json::Value> for Value {
|
|
||||||
fn from(value: serde_json::Value) -> Self {
|
|
||||||
Value::Json(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&serde_json::Value> for Value {
|
|
||||||
fn from(value: &serde_json::Value) -> Self {
|
|
||||||
Value::Json(value.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "decimal")]
|
|
||||||
impl From<rust_decimal::Decimal> for Value {
|
|
||||||
fn from(value: rust_decimal::Decimal) -> Self {
|
|
||||||
Value::Decimal(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "decimal")]
|
|
||||||
impl From<&rust_decimal::Decimal> for Value {
|
|
||||||
fn from(value: &rust_decimal::Decimal) -> Self {
|
|
||||||
Value::Decimal(*value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Option<T> implementations - convert None to Value::Null
|
|
||||||
impl<T: Into<Value>> From<Option<T>> for Value {
|
|
||||||
fn from(value: Option<T>) -> Self {
|
|
||||||
match value {
|
|
||||||
Some(v) => v.into(),
|
|
||||||
None => Value::Null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait BindValues<'q> {
|
pub trait BindValues<'q> {
|
||||||
type Output;
|
type Output;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue