Release v0.2.0 with UpdateExpr, ConnProvider, MCP server, and skills
New features: - UpdateExpr: Advanced updates with column arithmetic (Add, Sub, Mul, Div, Mod), CASE/WHEN conditionals, AddIf/SubIf, Coalesce, Greatest, Least, and raw SQL - ConnProvider: Flexible borrowed/owned connection management - lookup_table! and lookup_options! macros for type-safe lookup tables - new_uuid() for time-ordered UUIDs with better database indexing - MCP server (sqlx-record-expert) with documentation tools and resources - Claude Code skills for all features Improvements: - Fixed Postgres/SQLite unsigned integer binding (cast to signed) - Added From implementations for all integer types to Value - Added param_count() to Filter for expression parameter counting - Added bind_all_values() for proper expression binding order All three database backends (MySQL, PostgreSQL, SQLite) build and work correctly. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
e89041b9c6
commit
b960e235e6
|
|
@ -0,0 +1,276 @@
|
|||
# sqlx-record Audit Trail Skill
|
||||
|
||||
Guide to change tracking and audit functionality.
|
||||
|
||||
## Triggers
|
||||
- "audit trail", "change tracking"
|
||||
- "entity change", "change history"
|
||||
- "who changed", "audit log"
|
||||
|
||||
## EntityChange Model
|
||||
|
||||
```rust
|
||||
pub struct EntityChange {
|
||||
pub id: Uuid, // Change record ID
|
||||
pub entity_id: Uuid, // Target entity ID
|
||||
pub action: String, // Change type
|
||||
pub changed_at: i64, // Timestamp (milliseconds since epoch)
|
||||
pub actor_id: Uuid, // Who made the change
|
||||
pub session_id: Uuid, // Session context
|
||||
pub change_set_id: Uuid, // Transaction grouping
|
||||
pub new_value: Option<Value>, // JSON diff of changes
|
||||
}
|
||||
```
|
||||
|
||||
## Action Enum
|
||||
|
||||
```rust
|
||||
pub enum Action {
|
||||
Insert, // New entity created
|
||||
Update, // Entity modified
|
||||
Delete, // Soft delete
|
||||
Restore, // Restored from soft delete
|
||||
HardDelete, // Permanent deletion
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
// String conversion
|
||||
let action = Action::from("insert".to_string()); // Action::Insert
|
||||
println!("{}", Action::Update); // "update"
|
||||
```
|
||||
|
||||
## Repository Functions
|
||||
|
||||
```rust
|
||||
use sqlx_record::repositories::*;
|
||||
|
||||
// Create a change record
|
||||
create_entity_change(&pool, "entity_changes_users", &change).await?;
|
||||
|
||||
// Query by change ID
|
||||
let changes = get_entity_changes_by_id(&pool, table, &change_id).await?;
|
||||
|
||||
// Query by entity (all changes to an entity)
|
||||
let history = get_entity_changes_by_entity(&pool, table, &entity_id).await?;
|
||||
|
||||
// Query by session (all changes in a session)
|
||||
let changes = get_entity_changes_session(&pool, table, &session_id).await?;
|
||||
|
||||
// Query by actor (all changes by a user)
|
||||
let changes = get_entity_changes_actor(&pool, table, &actor_id).await?;
|
||||
|
||||
// Query by change set (atomic transaction)
|
||||
let changes = get_entity_changes_by_change_set(&pool, table, &change_set_id).await?;
|
||||
|
||||
// Combined query
|
||||
let changes = get_entity_changes_by_entity_and_actor(&pool, table, &entity_id, &actor_id).await?;
|
||||
```
|
||||
|
||||
## Diff Methods
|
||||
|
||||
### model_diff
|
||||
Compare UpdateForm with existing model:
|
||||
```rust
|
||||
let form = User::update_form().with_name("New Name").with_email("new@example.com");
|
||||
let existing = User::get_by_id(&pool, &id).await?.unwrap();
|
||||
|
||||
let diff = User::model_diff(&form, &existing);
|
||||
// {"name": {"old": "Old Name", "new": "New Name"}, "email": {"old": "old@example.com", "new": "new@example.com"}}
|
||||
```
|
||||
|
||||
### db_diff
|
||||
Compare UpdateForm with database values:
|
||||
```rust
|
||||
let form = User::update_form().with_name("New Name");
|
||||
let diff = User::db_diff(&form, &user_id, &pool).await?;
|
||||
```
|
||||
|
||||
### diff_modify
|
||||
Modify form to only include actual changes:
|
||||
```rust
|
||||
let mut form = User::update_form().with_name("Same Name").with_email("new@example.com");
|
||||
let existing = User::get_by_id(&pool, &id).await?.unwrap();
|
||||
|
||||
let diff = User::diff_modify(&mut form, &existing);
|
||||
// form now only contains email if name was already "Same Name"
|
||||
// diff contains only the actual changes
|
||||
```
|
||||
|
||||
### initial_diff
|
||||
Capture initial state for inserts:
|
||||
```rust
|
||||
let new_user = User { id: new_uuid(), name: "Alice".into(), ... };
|
||||
let diff = new_user.initial_diff();
|
||||
// {"id": "...", "name": "Alice", ...}
|
||||
```
|
||||
|
||||
### to_update_form
|
||||
Convert entity to UpdateForm:
|
||||
```rust
|
||||
let user = User::get_by_id(&pool, &id).await?.unwrap();
|
||||
let form = user.to_update_form();
|
||||
// form has all fields set to current values
|
||||
```
|
||||
|
||||
## Audit Table Setup
|
||||
|
||||
Use sqlx-record-ctl CLI:
|
||||
```bash
|
||||
sqlx-record-ctl --schema-name mydb --db-url "mysql://user:pass@localhost/mydb"
|
||||
```
|
||||
|
||||
Or create manually:
|
||||
```sql
|
||||
-- Metadata table (required)
|
||||
CREATE TABLE entity_changes_metadata (
|
||||
table_name VARCHAR(255) PRIMARY KEY,
|
||||
is_auditable BOOLEAN NOT NULL DEFAULT TRUE
|
||||
);
|
||||
|
||||
INSERT INTO entity_changes_metadata VALUES ('users', TRUE);
|
||||
|
||||
-- Audit table
|
||||
CREATE TABLE entity_changes_users (
|
||||
id BINARY(16) PRIMARY KEY,
|
||||
entity_id BINARY(16) NOT NULL,
|
||||
action ENUM('insert','update','delete','restore','hard-delete'),
|
||||
changed_at BIGINT,
|
||||
actor_id BINARY(16),
|
||||
session_id BINARY(16),
|
||||
change_set_id BINARY(16),
|
||||
new_value JSON
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX idx_users_entity_id ON entity_changes_users (entity_id);
|
||||
CREATE INDEX idx_users_actor_id ON entity_changes_users (actor_id);
|
||||
CREATE INDEX idx_users_session_id ON entity_changes_users (session_id);
|
||||
CREATE INDEX idx_users_change_set_id ON entity_changes_users (change_set_id);
|
||||
CREATE INDEX idx_users_entity_id_actor_id ON entity_changes_users (entity_id, actor_id);
|
||||
```
|
||||
|
||||
## Complete Audit Integration
|
||||
|
||||
```rust
|
||||
use sqlx_record::prelude::*;
|
||||
use sqlx_record::models::{EntityChange, Action};
|
||||
use sqlx_record::repositories::create_entity_change;
|
||||
|
||||
async fn create_user_with_audit(
|
||||
pool: &Pool,
|
||||
user: User,
|
||||
actor_id: &Uuid,
|
||||
session_id: &Uuid,
|
||||
change_set_id: &Uuid,
|
||||
) -> Result<Uuid, Error> {
|
||||
// Capture initial state
|
||||
let diff = user.initial_diff();
|
||||
|
||||
// Insert entity
|
||||
let user_id = user.insert(pool).await?;
|
||||
|
||||
// Record change
|
||||
let change = EntityChange {
|
||||
id: new_uuid(),
|
||||
entity_id: user_id,
|
||||
action: Action::Insert.to_string(),
|
||||
changed_at: chrono::Utc::now().timestamp_millis(),
|
||||
actor_id: *actor_id,
|
||||
session_id: *session_id,
|
||||
change_set_id: *change_set_id,
|
||||
new_value: Some(serde_json::to_value(diff).unwrap()),
|
||||
};
|
||||
|
||||
create_entity_change(pool, &User::entity_changes_table_name(), &change).await?;
|
||||
|
||||
Ok(user_id)
|
||||
}
|
||||
|
||||
async fn update_user_with_audit(
|
||||
pool: &Pool,
|
||||
user_id: &Uuid,
|
||||
form: UserUpdateForm,
|
||||
actor_id: &Uuid,
|
||||
session_id: &Uuid,
|
||||
change_set_id: &Uuid,
|
||||
) -> Result<(), Error> {
|
||||
// Get current state
|
||||
let existing = User::get_by_id(pool, user_id).await?.unwrap();
|
||||
|
||||
// Calculate diff
|
||||
let diff = User::model_diff(&form, &existing);
|
||||
|
||||
// Skip if no changes
|
||||
if diff.as_object().map(|o| o.is_empty()).unwrap_or(true) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Update entity
|
||||
User::update_by_id(pool, user_id, form).await?;
|
||||
|
||||
// Record change
|
||||
let change = EntityChange {
|
||||
id: new_uuid(),
|
||||
entity_id: *user_id,
|
||||
action: Action::Update.to_string(),
|
||||
changed_at: chrono::Utc::now().timestamp_millis(),
|
||||
actor_id: *actor_id,
|
||||
session_id: *session_id,
|
||||
change_set_id: *change_set_id,
|
||||
new_value: Some(diff),
|
||||
};
|
||||
|
||||
create_entity_change(pool, &User::entity_changes_table_name(), &change).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Query history
|
||||
async fn get_user_history(pool: &Pool, user_id: &Uuid) -> Result<Vec<EntityChange>, Error> {
|
||||
get_entity_changes_by_entity(pool, "entity_changes_users", user_id).await
|
||||
}
|
||||
|
||||
// Query by actor
|
||||
async fn get_changes_by_user(pool: &Pool, actor_id: &Uuid) -> Result<Vec<EntityChange>, Error> {
|
||||
get_entity_changes_actor(pool, "entity_changes_users", actor_id).await
|
||||
}
|
||||
```
|
||||
|
||||
## Change Set Pattern
|
||||
|
||||
Group related changes into atomic transactions:
|
||||
|
||||
```rust
|
||||
async fn transfer_ownership(
|
||||
pool: &Pool,
|
||||
item_id: &Uuid,
|
||||
from_user: &Uuid,
|
||||
to_user: &Uuid,
|
||||
actor_id: &Uuid,
|
||||
session_id: &Uuid,
|
||||
) -> Result<(), Error> {
|
||||
// Single change set for the transaction
|
||||
let change_set_id = new_uuid();
|
||||
|
||||
// Update item ownership
|
||||
update_with_audit(pool, item_id,
|
||||
Item::update_form().with_owner_id(*to_user),
|
||||
actor_id, session_id, &change_set_id
|
||||
).await?;
|
||||
|
||||
// Update from_user stats
|
||||
update_with_audit(pool, from_user,
|
||||
UserStats::update_form().with_item_count_delta(-1),
|
||||
actor_id, session_id, &change_set_id
|
||||
).await?;
|
||||
|
||||
// Update to_user stats
|
||||
update_with_audit(pool, to_user,
|
||||
UserStats::update_form().with_item_count_delta(1),
|
||||
actor_id, session_id, &change_set_id
|
||||
).await?;
|
||||
|
||||
// All three changes can be queried together via change_set_id
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
# sqlx-record ConnProvider Skill
|
||||
|
||||
Guide to flexible connection management.
|
||||
|
||||
## Triggers
|
||||
- "connection provider", "conn provider"
|
||||
- "borrow connection", "pool connection"
|
||||
- "lazy connection", "connection management"
|
||||
|
||||
## Overview
|
||||
|
||||
`ConnProvider` enables flexible connection handling:
|
||||
- **Borrowed**: Use an existing connection reference
|
||||
- **Owned**: Lazily acquire from pool on first use
|
||||
|
||||
## Enum Variants
|
||||
|
||||
```rust
|
||||
pub enum ConnProvider<'a> {
|
||||
/// Reference to existing connection
|
||||
Borrowed {
|
||||
conn: &'a mut PoolConnection<DB>,
|
||||
},
|
||||
/// Lazy acquisition from pool
|
||||
Owned {
|
||||
pool: Pool,
|
||||
conn: Option<PoolConnection<DB>>,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Constructors
|
||||
|
||||
### from_ref
|
||||
Use an existing connection:
|
||||
```rust
|
||||
let mut conn = pool.acquire().await?;
|
||||
let mut provider = ConnProvider::from_ref(&mut conn);
|
||||
```
|
||||
|
||||
### from_pool
|
||||
Lazy acquisition from pool:
|
||||
```rust
|
||||
let mut provider = ConnProvider::from_pool(pool.clone());
|
||||
// Connection acquired on first get_conn() call
|
||||
```
|
||||
|
||||
## Getting the Connection
|
||||
|
||||
```rust
|
||||
let conn = provider.get_conn().await?;
|
||||
// Returns &mut PoolConnection<DB>
|
||||
```
|
||||
|
||||
- **Borrowed**: Returns reference immediately
|
||||
- **Owned**: Acquires on first call, returns same connection on subsequent calls
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Reuse Existing Connection
|
||||
```rust
|
||||
async fn process_batch(conn: &mut PoolConnection<MySql>) -> Result<()> {
|
||||
let mut provider = ConnProvider::from_ref(conn);
|
||||
|
||||
do_work_a(&mut provider).await?;
|
||||
do_work_b(&mut provider).await?; // Same connection
|
||||
do_work_c(&mut provider).await?; // Same connection
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Lazy Pool Connection
|
||||
```rust
|
||||
async fn maybe_needs_db(pool: MySqlPool, condition: bool) -> Result<()> {
|
||||
let mut provider = ConnProvider::from_pool(pool);
|
||||
|
||||
if condition {
|
||||
// Connection acquired here (first use)
|
||||
let conn = provider.get_conn().await?;
|
||||
sqlx::query("SELECT 1").execute(&mut **conn).await?;
|
||||
}
|
||||
// If condition is false, no connection was ever acquired
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Uniform Interface
|
||||
```rust
|
||||
async fn do_database_work(provider: &mut ConnProvider<'_>) -> Result<()> {
|
||||
let conn = provider.get_conn().await?;
|
||||
|
||||
// Works regardless of Borrowed or Owned
|
||||
sqlx::query("INSERT INTO logs (msg) VALUES (?)")
|
||||
.bind("operation completed")
|
||||
.execute(&mut **conn)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Call with borrowed
|
||||
let mut conn = pool.acquire().await?;
|
||||
do_database_work(&mut ConnProvider::from_ref(&mut conn)).await?;
|
||||
|
||||
// Call with pool
|
||||
do_database_work(&mut ConnProvider::from_pool(pool)).await?;
|
||||
```
|
||||
|
||||
### Transaction-like Patterns
|
||||
```rust
|
||||
async fn multi_step_operation(pool: MySqlPool) -> Result<()> {
|
||||
let mut provider = ConnProvider::from_pool(pool);
|
||||
|
||||
// All operations use same connection
|
||||
step_1(&mut provider).await?;
|
||||
step_2(&mut provider).await?;
|
||||
step_3(&mut provider).await?;
|
||||
|
||||
// Connection returned to pool when provider drops
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## Database-Specific Types
|
||||
|
||||
The concrete types depend on the enabled feature:
|
||||
|
||||
| Feature | Pool Type | Connection Type |
|
||||
|---------|-----------|-----------------|
|
||||
| `mysql` | `MySqlPool` | `PoolConnection<MySql>` |
|
||||
| `postgres` | `PgPool` | `PoolConnection<Postgres>` |
|
||||
| `sqlite` | `SqlitePool` | `PoolConnection<Sqlite>` |
|
||||
|
||||
## Example: Service Layer
|
||||
|
||||
```rust
|
||||
use sqlx_record::prelude::*;
|
||||
|
||||
struct UserService;
|
||||
|
||||
impl UserService {
|
||||
async fn create_with_profile(
|
||||
provider: &mut ConnProvider<'_>,
|
||||
name: &str,
|
||||
bio: &str,
|
||||
) -> Result<Uuid, Error> {
|
||||
let conn = provider.get_conn().await?;
|
||||
|
||||
// Create user
|
||||
let user_id = new_uuid();
|
||||
sqlx::query("INSERT INTO users (id, name) VALUES (?, ?)")
|
||||
.bind(user_id)
|
||||
.bind(name)
|
||||
.execute(&mut **conn)
|
||||
.await?;
|
||||
|
||||
// Create profile (same connection)
|
||||
sqlx::query("INSERT INTO profiles (user_id, bio) VALUES (?, ?)")
|
||||
.bind(user_id)
|
||||
.bind(bio)
|
||||
.execute(&mut **conn)
|
||||
.await?;
|
||||
|
||||
Ok(user_id)
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
let mut provider = ConnProvider::from_pool(pool);
|
||||
let user_id = UserService::create_with_profile(&mut provider, "Alice", "Hello!").await?;
|
||||
```
|
||||
|
||||
## Connection Lifecycle
|
||||
|
||||
```
|
||||
from_pool(pool) from_ref(&mut conn)
|
||||
│ │
|
||||
▼ ▼
|
||||
Owned { Borrowed {
|
||||
pool, conn: &mut PoolConnection
|
||||
conn: None }
|
||||
} │
|
||||
│ │
|
||||
│ get_conn() │ get_conn()
|
||||
▼ ▼
|
||||
pool.acquire() return conn
|
||||
│ │
|
||||
▼ │
|
||||
Owned { │
|
||||
pool, │
|
||||
conn: Some(acquired) │
|
||||
} │
|
||||
│ │
|
||||
│ get_conn() (subsequent) │
|
||||
▼ │
|
||||
return &mut acquired │
|
||||
│ │
|
||||
▼ ▼
|
||||
Drop: conn returned Drop: nothing (borrowed)
|
||||
```
|
||||
|
|
@ -0,0 +1,238 @@
|
|||
# sqlx-record Entity Skill
|
||||
|
||||
Detailed guidance for #[derive(Entity)] macro.
|
||||
|
||||
## Triggers
|
||||
- "derive entity", "entity macro"
|
||||
- "generate crud", "crud methods"
|
||||
- "primary key", "version field"
|
||||
- "table name", "rename field"
|
||||
|
||||
## Struct Attributes
|
||||
|
||||
### #[table_name]
|
||||
```rust
|
||||
#[derive(Entity, FromRow)]
|
||||
#[table_name = "users"] // or #[table_name("users")]
|
||||
struct User { ... }
|
||||
```
|
||||
- Optional: defaults to snake_case of struct name
|
||||
- `User` -> `users`, `OrderItem` -> `order_items`
|
||||
|
||||
## Field Attributes
|
||||
|
||||
### #[primary_key]
|
||||
```rust
|
||||
#[primary_key]
|
||||
id: Uuid,
|
||||
```
|
||||
- Required on one field
|
||||
- Generates `get_by_{pk}`, `update_by_{pk}` methods
|
||||
- Supports: Uuid, String, i32, i64, etc.
|
||||
|
||||
### #[rename]
|
||||
```rust
|
||||
#[rename("user_name")] // or #[rename = "user_name"]
|
||||
name: String,
|
||||
```
|
||||
- Maps struct field to different database column
|
||||
- Use when DB column doesn't match Rust naming
|
||||
|
||||
### #[version]
|
||||
```rust
|
||||
#[version]
|
||||
version: u32,
|
||||
```
|
||||
- Auto-increments on every update
|
||||
- Wraps on overflow (u32::MAX -> 0)
|
||||
- Generates `get_version()`, `get_versions()` methods
|
||||
- Supports: u32, u64, i32, i64
|
||||
|
||||
### #[field_type]
|
||||
```rust
|
||||
#[field_type("BIGINT")] // or #[field_type = "BIGINT"]
|
||||
large_count: i64,
|
||||
```
|
||||
- SQLx type hint for compile-time validation
|
||||
- Adds type annotation in SELECT: `field as "field: TYPE"`
|
||||
|
||||
## Generated Methods
|
||||
|
||||
### Insert
|
||||
```rust
|
||||
pub async fn insert<E>(&self, executor: E) -> Result<PkType, sqlx::Error>
|
||||
```
|
||||
|
||||
### Get Methods
|
||||
```rust
|
||||
// By single primary key
|
||||
pub async fn get_by_id(executor, id: &Uuid) -> Result<Option<Self>, Error>
|
||||
|
||||
// By multiple primary keys
|
||||
pub async fn get_by_ids(executor, ids: &[Uuid]) -> Result<Vec<Self>, Error>
|
||||
|
||||
// Generic primary key access
|
||||
pub async fn get_by_primary_key(executor, pk: &PkType) -> Result<Option<Self>, Error>
|
||||
```
|
||||
|
||||
### Find Methods
|
||||
```rust
|
||||
// Basic find
|
||||
pub async fn find(executor, filters: Vec<Filter>, index: Option<&str>) -> Result<Vec<Self>, Error>
|
||||
|
||||
// Find first match
|
||||
pub async fn find_one(executor, filters: Vec<Filter>, index: Option<&str>) -> Result<Option<Self>, Error>
|
||||
|
||||
// With ordering
|
||||
pub async fn find_ordered(
|
||||
executor,
|
||||
filters: Vec<Filter>,
|
||||
index: Option<&str>,
|
||||
order_by: Vec<(&str, bool)> // (field, is_ascending)
|
||||
) -> Result<Vec<Self>, Error>
|
||||
|
||||
// With ordering and pagination
|
||||
pub async fn find_ordered_with_limit(
|
||||
executor,
|
||||
filters: Vec<Filter>,
|
||||
index: Option<&str>,
|
||||
order_by: Vec<(&str, bool)>,
|
||||
offset_limit: Option<(u32, u32)> // (offset, limit)
|
||||
) -> Result<Vec<Self>, Error>
|
||||
|
||||
// Count matching
|
||||
pub async fn count(executor, filters: Vec<Filter>, index: Option<&str>) -> Result<u64, Error>
|
||||
```
|
||||
|
||||
### Update Methods
|
||||
```rust
|
||||
// Update instance
|
||||
pub async fn update(&self, executor, form: UpdateForm) -> Result<(), Error>
|
||||
|
||||
// Update by primary key
|
||||
pub async fn update_by_id(executor, id: &Uuid, form: UpdateForm) -> Result<(), Error>
|
||||
|
||||
// Update multiple
|
||||
pub async fn update_by_ids(executor, ids: &[Uuid], form: UpdateForm) -> Result<(), Error>
|
||||
|
||||
// Create update form
|
||||
pub fn update_form() -> UpdateForm
|
||||
```
|
||||
|
||||
### Diff Methods
|
||||
```rust
|
||||
// Compare form with model
|
||||
pub fn model_diff(form: &UpdateForm, model: &Self) -> serde_json::Value
|
||||
|
||||
// Compare form with database
|
||||
pub async fn db_diff(form: &UpdateForm, pk: &PkType, executor) -> Result<Value, Error>
|
||||
|
||||
// Modify form to only include changes
|
||||
pub fn diff_modify(form: &mut UpdateForm, model: &Self) -> serde_json::Value
|
||||
|
||||
// Convert entity to update form
|
||||
pub fn to_update_form(&self) -> UpdateForm
|
||||
|
||||
// Get initial state as JSON
|
||||
pub fn initial_diff(&self) -> serde_json::Value
|
||||
```
|
||||
|
||||
### Version Methods (if #[version] exists)
|
||||
```rust
|
||||
pub async fn get_version(executor, pk: &PkType) -> Result<Option<VersionType>, Error>
|
||||
pub async fn get_versions(executor, pks: &[PkType]) -> Result<HashMap<PkType, VersionType>, Error>
|
||||
```
|
||||
|
||||
### Metadata Methods
|
||||
```rust
|
||||
pub const fn table_name() -> &'static str
|
||||
pub fn entity_key(pk: &PkType) -> String // "/entities/{table}/{pk}"
|
||||
pub fn entity_changes_table_name() -> String // "entity_changes_{table}"
|
||||
pub const fn primary_key_field() -> &'static str
|
||||
pub const fn primary_key_db_field() -> &'static str
|
||||
pub fn primary_key(&self) -> &PkType
|
||||
pub fn select_fields() -> Vec<&'static str>
|
||||
```
|
||||
|
||||
## UpdateForm
|
||||
|
||||
Generated struct `{Entity}UpdateForm` with all non-PK fields as Option<T>.
|
||||
|
||||
```rust
|
||||
// Builder pattern
|
||||
let form = User::update_form()
|
||||
.with_name("Alice")
|
||||
.with_email("alice@example.com");
|
||||
|
||||
// Setter pattern
|
||||
let mut form = User::update_form();
|
||||
form.set_name("Alice");
|
||||
|
||||
// Execute
|
||||
User::update_by_id(&pool, &id, form).await?;
|
||||
```
|
||||
|
||||
Only set fields are updated - others remain unchanged.
|
||||
|
||||
## Complete Example
|
||||
|
||||
```rust
|
||||
use sqlx_record::prelude::*;
|
||||
use sqlx::FromRow;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Entity, FromRow, Debug, Clone)]
|
||||
#[table_name = "products"]
|
||||
pub struct Product {
|
||||
#[primary_key]
|
||||
pub id: Uuid,
|
||||
|
||||
#[rename("product_name")]
|
||||
pub name: String,
|
||||
|
||||
pub price_cents: i64,
|
||||
|
||||
pub category_id: Uuid,
|
||||
|
||||
pub is_active: bool,
|
||||
|
||||
#[version]
|
||||
pub version: u32,
|
||||
|
||||
#[field_type("TEXT")]
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
// Usage
|
||||
async fn example(pool: &Pool) -> Result<(), Error> {
|
||||
// Create
|
||||
let product = Product {
|
||||
id: new_uuid(),
|
||||
name: "Widget".into(),
|
||||
price_cents: 999,
|
||||
category_id: category_id,
|
||||
is_active: true,
|
||||
version: 0,
|
||||
description: Some("A great widget".into()),
|
||||
};
|
||||
product.insert(pool).await?;
|
||||
|
||||
// Read
|
||||
let product = Product::get_by_id(pool, &product.id).await?.unwrap();
|
||||
|
||||
// Update
|
||||
Product::update_by_id(pool, &product.id,
|
||||
Product::update_form()
|
||||
.with_price_cents(1299)
|
||||
.with_is_active(false)
|
||||
).await?;
|
||||
|
||||
// Find active products in category
|
||||
let products = Product::find(pool,
|
||||
filters![("is_active", true), ("category_id", category_id)],
|
||||
None
|
||||
).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,261 @@
|
|||
# sqlx-record Filters Skill
|
||||
|
||||
Comprehensive guide to the Filter system.
|
||||
|
||||
## Triggers
|
||||
- "filter query", "where clause"
|
||||
- "filter macro", "filters!"
|
||||
- "query builder", "search criteria"
|
||||
|
||||
## Filter Enum
|
||||
|
||||
```rust
|
||||
pub enum Filter<'a> {
|
||||
// Comparison
|
||||
Equal(&'a str, Value),
|
||||
NotEqual(&'a str, Value),
|
||||
GreaterThan(&'a str, Value),
|
||||
GreaterThanOrEqual(&'a str, Value),
|
||||
LessThan(&'a str, Value),
|
||||
LessThanOrEqual(&'a str, Value),
|
||||
|
||||
// Pattern matching
|
||||
Like(&'a str, Value),
|
||||
ILike(&'a str, Value), // Case-insensitive
|
||||
NotLike(&'a str, Value),
|
||||
|
||||
// Set membership
|
||||
In(&'a str, Vec<Value>),
|
||||
NotIn(&'a str, Vec<Value>),
|
||||
|
||||
// Null checks
|
||||
IsNull(&'a str),
|
||||
IsNotNull(&'a str),
|
||||
|
||||
// Composition
|
||||
And(Vec<Filter<'a>>),
|
||||
Or(Vec<Filter<'a>>),
|
||||
}
|
||||
```
|
||||
|
||||
## Macros
|
||||
|
||||
### filters!
|
||||
```rust
|
||||
// Empty
|
||||
filters![]
|
||||
|
||||
// Single condition
|
||||
filters![("is_active", true)]
|
||||
|
||||
// Multiple conditions (implicit AND)
|
||||
filters![("is_active", true), ("role", "admin")]
|
||||
```
|
||||
|
||||
### filter_and!
|
||||
```rust
|
||||
// Explicit AND
|
||||
filter_and![("age", 18), ("verified", true)]
|
||||
```
|
||||
|
||||
### filter_or!
|
||||
```rust
|
||||
// OR conditions
|
||||
filter_or![("status", "active"), ("status", "pending")]
|
||||
```
|
||||
|
||||
## Operator Trait (FilterOps)
|
||||
|
||||
String references implement FilterOps for fluent syntax:
|
||||
|
||||
```rust
|
||||
"age".gt(18) // Filter::GreaterThan("age", 18.into())
|
||||
"age".ge(18) // Filter::GreaterThanOrEqual
|
||||
"age".lt(65) // Filter::LessThan
|
||||
"age".le(65) // Filter::LessThanOrEqual
|
||||
"name".eq("Bob") // Filter::Equal
|
||||
"name".ne("Bob") // Filter::NotEqual
|
||||
```
|
||||
|
||||
## Direct Filter Construction
|
||||
|
||||
```rust
|
||||
// Pattern matching
|
||||
Filter::Like("name", "%alice%".into())
|
||||
Filter::ILike("email", "%@GMAIL.COM".into()) // Case-insensitive
|
||||
Filter::NotLike("name", "test%".into())
|
||||
|
||||
// Set membership
|
||||
Filter::In("status", vec!["active".into(), "pending".into()])
|
||||
Filter::NotIn("role", vec!["banned".into(), "suspended".into()])
|
||||
|
||||
// Null checks
|
||||
Filter::IsNull("deleted_at")
|
||||
Filter::IsNotNull("email_verified_at")
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
### Nested AND
|
||||
```rust
|
||||
Filter::And(vec![
|
||||
"age".ge(18),
|
||||
"age".le(65),
|
||||
Filter::IsNotNull("email"),
|
||||
])
|
||||
```
|
||||
|
||||
### Nested OR
|
||||
```rust
|
||||
Filter::Or(vec![
|
||||
Filter::Equal("status", "active".into()),
|
||||
Filter::Equal("status", "pending".into()),
|
||||
])
|
||||
```
|
||||
|
||||
### Complex Queries
|
||||
```rust
|
||||
// (age >= 18 AND verified = true) OR role = 'admin'
|
||||
let filters = vec![
|
||||
Filter::Or(vec![
|
||||
Filter::And(vec![
|
||||
"age".ge(18),
|
||||
Filter::Equal("verified", true.into()),
|
||||
]),
|
||||
Filter::Equal("role", "admin".into()),
|
||||
])
|
||||
];
|
||||
```
|
||||
|
||||
## Usage with Entity Methods
|
||||
|
||||
### find()
|
||||
```rust
|
||||
let users = User::find(&pool, filters![("is_active", true)], None).await?;
|
||||
```
|
||||
|
||||
### find_one()
|
||||
```rust
|
||||
let user = User::find_one(&pool, filters![("email", email)], None).await?;
|
||||
```
|
||||
|
||||
### find_ordered()
|
||||
```rust
|
||||
let users = User::find_ordered(
|
||||
&pool,
|
||||
filters![("is_active", true)],
|
||||
None,
|
||||
vec![("created_at", false), ("name", true)] // DESC, ASC
|
||||
).await?;
|
||||
```
|
||||
|
||||
### find_ordered_with_limit()
|
||||
```rust
|
||||
let page = User::find_ordered_with_limit(
|
||||
&pool,
|
||||
filters![("role", "admin")],
|
||||
None,
|
||||
vec![("created_at", false)],
|
||||
Some((20, 10)) // OFFSET 20, LIMIT 10 (page 3)
|
||||
).await?;
|
||||
```
|
||||
|
||||
### count()
|
||||
```rust
|
||||
let active_count = User::count(&pool, filters![("is_active", true)], None).await?;
|
||||
```
|
||||
|
||||
## Index Hints (MySQL only)
|
||||
|
||||
```rust
|
||||
// MySQL: SELECT ... FROM users USE INDEX(idx_users_email) WHERE ...
|
||||
let users = User::find(&pool, filters, Some("idx_users_email")).await?;
|
||||
|
||||
// PostgreSQL/SQLite: Index hint ignored
|
||||
```
|
||||
|
||||
## Building WHERE Clauses Manually
|
||||
|
||||
```rust
|
||||
let filters = filters![("status", "active"), ("role", "admin")];
|
||||
|
||||
// Build clause starting at placeholder index 1
|
||||
let (clause, values) = Filter::build_where_clause(&filters);
|
||||
// MySQL/SQLite: "status = ? AND role = ?"
|
||||
// PostgreSQL: "status = $1 AND role = $2"
|
||||
|
||||
// Build with custom offset (e.g., after binding other values)
|
||||
let (clause, values) = Filter::build_where_clause_with_offset(&filters, 3);
|
||||
// PostgreSQL: "status = $3 AND role = $4"
|
||||
```
|
||||
|
||||
## ILIKE Handling
|
||||
|
||||
PostgreSQL has native ILIKE. For MySQL/SQLite, it's emulated:
|
||||
|
||||
```rust
|
||||
// PostgreSQL: field ILIKE value
|
||||
// MySQL/SQLite: LOWER(field) LIKE LOWER(value)
|
||||
Filter::ILike("email", "%@gmail.com".into())
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pagination
|
||||
```rust
|
||||
async fn get_page(pool: &Pool, page: u32, per_page: u32, filters: Vec<Filter<'_>>) -> Result<Vec<User>> {
|
||||
let offset = page * per_page;
|
||||
User::find_ordered_with_limit(
|
||||
pool,
|
||||
filters,
|
||||
None,
|
||||
vec![("id", true)], // Stable ordering
|
||||
Some((offset, per_page))
|
||||
).await
|
||||
}
|
||||
```
|
||||
|
||||
### Search with Multiple Fields
|
||||
```rust
|
||||
fn search_users(query: &str) -> Vec<Filter<'static>> {
|
||||
let pattern = format!("%{}%", query);
|
||||
vec![filter_or![
|
||||
Filter::ILike("name", pattern.clone().into()),
|
||||
Filter::ILike("email", pattern.clone().into()),
|
||||
Filter::ILike("username", pattern.into()),
|
||||
]]
|
||||
}
|
||||
```
|
||||
|
||||
### Date Range
|
||||
```rust
|
||||
fn date_range(start: NaiveDate, end: NaiveDate) -> Vec<Filter<'static>> {
|
||||
vec![
|
||||
"created_at".ge(start),
|
||||
"created_at".le(end),
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Optional Filters
|
||||
```rust
|
||||
fn build_filters(
|
||||
status: Option<&str>,
|
||||
min_age: Option<i32>,
|
||||
role: Option<&str>,
|
||||
) -> Vec<Filter<'static>> {
|
||||
let mut filters = vec![];
|
||||
|
||||
if let Some(s) = status {
|
||||
filters.push(Filter::Equal("status", s.into()));
|
||||
}
|
||||
if let Some(age) = min_age {
|
||||
filters.push("age".ge(age));
|
||||
}
|
||||
if let Some(r) = role {
|
||||
filters.push(Filter::Equal("role", r.into()));
|
||||
}
|
||||
|
||||
filters
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,279 @@
|
|||
# sqlx-record Lookup Tables Skill
|
||||
|
||||
Guide to lookup_table! and lookup_options! macros.
|
||||
|
||||
## Triggers
|
||||
- "lookup table", "lookup options"
|
||||
- "code enum", "status enum"
|
||||
- "type-safe codes", "constants"
|
||||
|
||||
## lookup_table! Macro
|
||||
|
||||
Creates a database-backed lookup entity with type-safe code enum.
|
||||
|
||||
### Syntax
|
||||
```rust
|
||||
lookup_table!(Name, "code1", "code2", "code3");
|
||||
```
|
||||
|
||||
### Generated Code
|
||||
```rust
|
||||
// Entity struct with CRUD via #[derive(Entity)]
|
||||
#[derive(Entity, FromRow)]
|
||||
pub struct Name {
|
||||
#[primary_key]
|
||||
pub code: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub is_active: bool,
|
||||
}
|
||||
|
||||
// Type-safe enum
|
||||
pub enum NameCode {
|
||||
Code1,
|
||||
Code2,
|
||||
Code3,
|
||||
}
|
||||
|
||||
// String constants
|
||||
impl Name {
|
||||
pub const CODE1: &'static str = "code1";
|
||||
pub const CODE2: &'static str = "code2";
|
||||
pub const CODE3: &'static str = "code3";
|
||||
}
|
||||
|
||||
// Display impl (enum -> string)
|
||||
impl Display for NameCode { ... }
|
||||
|
||||
// TryFrom impl (string -> enum)
|
||||
impl TryFrom<&str> for NameCode { ... }
|
||||
```
|
||||
|
||||
### Example
|
||||
```rust
|
||||
lookup_table!(OrderStatus,
|
||||
"pending",
|
||||
"processing",
|
||||
"shipped",
|
||||
"delivered",
|
||||
"cancelled"
|
||||
);
|
||||
|
||||
// Usage
|
||||
let status = OrderStatus::PENDING; // "pending"
|
||||
let code = OrderStatusCode::Pending;
|
||||
println!("{}", code); // "pending"
|
||||
|
||||
// Parse from string
|
||||
let code = OrderStatusCode::try_from("shipped")?;
|
||||
|
||||
// Query the lookup table
|
||||
let statuses = OrderStatus::find(&pool, filters![("is_active", true)], None).await?;
|
||||
|
||||
// Get specific status
|
||||
let status = OrderStatus::get_by_code(&pool, OrderStatus::PENDING).await?;
|
||||
```
|
||||
|
||||
### Database Schema
|
||||
```sql
|
||||
CREATE TABLE order_status (
|
||||
code VARCHAR(255) PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE
|
||||
);
|
||||
|
||||
INSERT INTO order_status VALUES
|
||||
('pending', 'Pending', 'Order awaiting processing', TRUE),
|
||||
('processing', 'Processing', 'Order being prepared', TRUE),
|
||||
('shipped', 'Shipped', 'Order in transit', TRUE),
|
||||
('delivered', 'Delivered', 'Order completed', TRUE),
|
||||
('cancelled', 'Cancelled', 'Order cancelled', TRUE);
|
||||
```
|
||||
|
||||
## lookup_options! Macro
|
||||
|
||||
Creates code enum without database entity (for embedded options).
|
||||
|
||||
### Syntax
|
||||
```rust
|
||||
lookup_options!(Name, "code1", "code2", "code3");
|
||||
```
|
||||
|
||||
### Generated Code
|
||||
```rust
|
||||
// Type-safe enum
|
||||
pub enum NameCode {
|
||||
Code1,
|
||||
Code2,
|
||||
Code3,
|
||||
}
|
||||
|
||||
// Unit struct for constants
|
||||
pub struct Name;
|
||||
|
||||
impl Name {
|
||||
pub const CODE1: &'static str = "code1";
|
||||
pub const CODE2: &'static str = "code2";
|
||||
pub const CODE3: &'static str = "code3";
|
||||
}
|
||||
|
||||
// Display and TryFrom implementations
|
||||
```
|
||||
|
||||
### Example
|
||||
```rust
|
||||
lookup_options!(PaymentMethod,
|
||||
"credit-card",
|
||||
"debit-card",
|
||||
"paypal",
|
||||
"bank-transfer",
|
||||
"crypto"
|
||||
);
|
||||
|
||||
// Usage
|
||||
let method = PaymentMethod::CREDIT_CARD; // "credit-card"
|
||||
|
||||
// In entity
|
||||
#[derive(Entity)]
|
||||
struct Order {
|
||||
#[primary_key]
|
||||
id: Uuid,
|
||||
payment_method: String, // Stores the code string
|
||||
}
|
||||
|
||||
// Query with constant
|
||||
let orders = Order::find(&pool,
|
||||
filters![("payment_method", PaymentMethod::PAYPAL)],
|
||||
None
|
||||
).await?;
|
||||
|
||||
// Type-safe matching
|
||||
match PaymentMethodCode::try_from(order.payment_method.as_str())? {
|
||||
PaymentMethodCode::CreditCard => process_credit_card(),
|
||||
PaymentMethodCode::DebitCard => process_debit_card(),
|
||||
PaymentMethodCode::Paypal => process_paypal(),
|
||||
PaymentMethodCode::BankTransfer => process_bank_transfer(),
|
||||
PaymentMethodCode::Crypto => process_crypto(),
|
||||
}
|
||||
```
|
||||
|
||||
## Code Naming Conventions
|
||||
|
||||
Codes are converted to enum variants using camelCase:
|
||||
|
||||
| Code String | Enum Variant | Constant |
|
||||
|-------------|--------------|----------|
|
||||
| `"active"` | `Active` | `ACTIVE` |
|
||||
| `"pro-rata"` | `ProRata` | `PRO_RATA` |
|
||||
| `"full-24-hours"` | `Full24Hours` | `FULL_24_HOURS` |
|
||||
| `"activity_added"` | `ActivityAdded` | `ACTIVITY_ADDED` |
|
||||
| `"no-refunds"` | `NoRefunds` | `NO_REFUNDS` |
|
||||
|
||||
## When to Use Each
|
||||
|
||||
### Use lookup_table! when:
|
||||
- Lookup values are stored in database
|
||||
- Values may change at runtime
|
||||
- Need to query/manage lookup values
|
||||
- Want audit trail on lookup changes
|
||||
|
||||
### Use lookup_options! when:
|
||||
- Codes are compile-time constants
|
||||
- No database table needed
|
||||
- Codes are embedded in other entities
|
||||
- Values won't change without code deployment
|
||||
|
||||
## Real-World Examples
|
||||
|
||||
### Status Workflows
|
||||
```rust
|
||||
lookup_table!(TaskStatus,
|
||||
"todo",
|
||||
"in-progress",
|
||||
"review",
|
||||
"done",
|
||||
"archived"
|
||||
);
|
||||
|
||||
impl Task {
|
||||
pub fn can_transition(&self, to: TaskStatusCode) -> bool {
|
||||
match (TaskStatusCode::try_from(self.status.as_str()), to) {
|
||||
(Ok(TaskStatusCode::Todo), TaskStatusCode::InProgress) => true,
|
||||
(Ok(TaskStatusCode::InProgress), TaskStatusCode::Review) => true,
|
||||
(Ok(TaskStatusCode::Review), TaskStatusCode::Done) => true,
|
||||
(Ok(TaskStatusCode::Review), TaskStatusCode::InProgress) => true,
|
||||
(Ok(_), TaskStatusCode::Archived) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Options
|
||||
```rust
|
||||
lookup_options!(LogLevel, "debug", "info", "warn", "error");
|
||||
lookup_options!(Theme, "light", "dark", "system");
|
||||
lookup_options!(Language, "en", "es", "fr", "de", "ja");
|
||||
|
||||
#[derive(Entity)]
|
||||
struct UserPreferences {
|
||||
#[primary_key]
|
||||
user_id: Uuid,
|
||||
log_level: String,
|
||||
theme: String,
|
||||
language: String,
|
||||
}
|
||||
|
||||
// Usage
|
||||
let prefs = UserPreferences {
|
||||
user_id: user_id,
|
||||
log_level: LogLevel::INFO.into(),
|
||||
theme: Theme::DARK.into(),
|
||||
language: Language::EN.into(),
|
||||
};
|
||||
```
|
||||
|
||||
### Domain-Specific Types
|
||||
```rust
|
||||
lookup_options!(FastingPattern,
|
||||
"time-restricted",
|
||||
"omad",
|
||||
"alternate-day",
|
||||
"five-two",
|
||||
"extended",
|
||||
"custom"
|
||||
);
|
||||
|
||||
lookup_options!(ProgramType,
|
||||
"self-paced",
|
||||
"live",
|
||||
"challenge",
|
||||
"maintenance"
|
||||
);
|
||||
|
||||
lookup_options!(SubscriptionTier,
|
||||
"free",
|
||||
"basic",
|
||||
"premium",
|
||||
"unlimited"
|
||||
);
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```rust
|
||||
// TryFrom returns Result
|
||||
match UserStatusCode::try_from(unknown_string) {
|
||||
Ok(code) => handle_status(code),
|
||||
Err(msg) => {
|
||||
// msg: "Unknown userstatus code: invalid_value"
|
||||
log::warn!("{}", msg);
|
||||
handle_unknown_status()
|
||||
}
|
||||
}
|
||||
|
||||
// Or use unwrap_or for default
|
||||
let code = UserStatusCode::try_from(status_str)
|
||||
.unwrap_or(UserStatusCode::Pending);
|
||||
```
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
# sqlx-record Skill
|
||||
|
||||
Expert guidance for using the sqlx-record Rust library.
|
||||
|
||||
## Triggers
|
||||
- "create entity", "define entity", "entity struct"
|
||||
- "sqlx record", "sqlx-record"
|
||||
- "crud operations", "database entity"
|
||||
- "audit trail", "change tracking"
|
||||
- "lookup table", "lookup options"
|
||||
|
||||
## Overview
|
||||
|
||||
sqlx-record provides derive macros for automatic CRUD operations and audit trails for SQL entities. It supports MySQL, PostgreSQL, and SQLite.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Entity Definition
|
||||
```rust
|
||||
use sqlx_record::prelude::*;
|
||||
use sqlx::FromRow;
|
||||
|
||||
#[derive(Entity, FromRow)]
|
||||
#[table_name = "users"]
|
||||
struct User {
|
||||
#[primary_key]
|
||||
id: Uuid,
|
||||
|
||||
#[rename("user_name")] // Maps to different DB column
|
||||
name: String,
|
||||
|
||||
#[version] // Auto-increment on updates
|
||||
version: u32,
|
||||
|
||||
#[field_type("TEXT")] // SQLx type hint
|
||||
bio: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
### CRUD Operations
|
||||
```rust
|
||||
// Insert
|
||||
let user = User { id: new_uuid(), name: "Alice".into(), version: 0 };
|
||||
user.insert(&pool).await?;
|
||||
|
||||
// Get
|
||||
let user = User::get_by_id(&pool, &id).await?;
|
||||
let users = User::get_by_ids(&pool, &ids).await?;
|
||||
|
||||
// Find with filters
|
||||
let users = User::find(&pool, filters![("is_active", true)], None).await?;
|
||||
let user = User::find_one(&pool, filters![("email", email)], None).await?;
|
||||
|
||||
// Find with ordering and pagination
|
||||
let page = User::find_ordered_with_limit(
|
||||
&pool,
|
||||
filters![("role", "admin")],
|
||||
None,
|
||||
vec![("created_at", false)], // DESC
|
||||
Some((0, 10)) // offset, limit
|
||||
).await?;
|
||||
|
||||
// Count
|
||||
let count = User::count(&pool, filters![("is_active", true)], None).await?;
|
||||
|
||||
// Update
|
||||
User::update_by_id(&pool, &id, User::update_form().with_name("Bob")).await?;
|
||||
```
|
||||
|
||||
### Filter System
|
||||
```rust
|
||||
// Simple equality
|
||||
filters![("field", value)]
|
||||
|
||||
// Multiple conditions (AND)
|
||||
filters![("active", true), ("role", "admin")]
|
||||
|
||||
// OR conditions
|
||||
filter_or![("status", "active"), ("status", "pending")]
|
||||
|
||||
// Operators
|
||||
"age".gt(18) // >
|
||||
"age".ge(18) // >=
|
||||
"age".lt(65) // <
|
||||
"age".le(65) // <=
|
||||
"name".eq("Bob") // =
|
||||
"name".ne("Bob") // !=
|
||||
|
||||
// Other filters
|
||||
Filter::Like("name", "%alice%".into())
|
||||
Filter::In("status", vec!["active".into(), "pending".into()])
|
||||
Filter::IsNull("deleted_at")
|
||||
Filter::IsNotNull("email")
|
||||
```
|
||||
|
||||
### Lookup Tables
|
||||
```rust
|
||||
// With database entity
|
||||
lookup_table!(OrderStatus, "pending", "shipped", "delivered");
|
||||
// Generates: struct OrderStatus, enum OrderStatusCode, constants
|
||||
|
||||
// Without database entity
|
||||
lookup_options!(PaymentMethod, "credit-card", "paypal", "bank-transfer");
|
||||
// Generates: enum PaymentMethodCode, struct PaymentMethod (constants only)
|
||||
|
||||
// Usage
|
||||
let status = OrderStatus::PENDING; // "pending"
|
||||
let code = OrderStatusCode::try_from("pending")?; // OrderStatusCode::Pending
|
||||
```
|
||||
|
||||
### Time-Ordered UUIDs
|
||||
```rust
|
||||
let id = new_uuid(); // Timestamp prefix for better indexing
|
||||
```
|
||||
|
||||
## Feature Flags
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
sqlx-record = { version = "0.2", features = ["mysql", "derive"] }
|
||||
# Database: "mysql", "postgres", or "sqlite" (pick one)
|
||||
# Optional: "derive", "static-validation"
|
||||
```
|
||||
|
||||
## Advanced Updates (UpdateExpr)
|
||||
|
||||
```rust
|
||||
use sqlx_record::prelude::UpdateExpr;
|
||||
|
||||
// Arithmetic: score = score + 10
|
||||
User::update_by_id(&pool, &id,
|
||||
User::update_form().eval_score(UpdateExpr::Add(10.into()))
|
||||
).await?;
|
||||
|
||||
// CASE/WHEN
|
||||
User::update_by_id(&pool, &id,
|
||||
User::update_form().eval_tier(UpdateExpr::Case {
|
||||
branches: vec![("score".gt(100), "gold".into())],
|
||||
default: "bronze".into(),
|
||||
})
|
||||
).await?;
|
||||
|
||||
// Raw SQL escape hatch
|
||||
User::update_by_id(&pool, &id,
|
||||
User::update_form().raw("computed", "COALESCE(a, 0) + b")
|
||||
).await?;
|
||||
```
|
||||
|
||||
## ConnProvider (Flexible Connections)
|
||||
|
||||
```rust
|
||||
use sqlx_record::ConnProvider;
|
||||
|
||||
// Borrowed or owned pool connections
|
||||
let conn = ConnProvider::Borrowed(&pool);
|
||||
let users = User::find(&*conn, filters![], None).await?;
|
||||
```
|
||||
|
||||
## Database Differences
|
||||
|
||||
| Feature | MySQL | PostgreSQL | SQLite |
|
||||
|---------|-------|------------|--------|
|
||||
| Placeholder | `?` | `$1, $2` | `?` |
|
||||
| Table quote | `` ` `` | `"` | `"` |
|
||||
| Index hints | Supported | N/A | N/A |
|
||||
|
|
@ -0,0 +1,242 @@
|
|||
# sqlx-record UpdateExpr Skill
|
||||
|
||||
Guide to advanced update operations with eval_* methods.
|
||||
|
||||
## Triggers
|
||||
- "update expression", "update expr"
|
||||
- "increment field", "decrement field"
|
||||
- "case when update", "conditional update"
|
||||
- "arithmetic update", "column arithmetic"
|
||||
|
||||
## Overview
|
||||
|
||||
`UpdateExpr` enables complex update operations beyond simple value assignment:
|
||||
- Column arithmetic (`count = count + 1`)
|
||||
- CASE/WHEN conditional updates
|
||||
- Conditional increments/decrements
|
||||
- Raw SQL escape hatch
|
||||
|
||||
## UpdateExpr Enum
|
||||
|
||||
```rust
|
||||
pub enum UpdateExpr {
|
||||
Set(Value), // column = value
|
||||
Add(Value), // column = column + value
|
||||
Sub(Value), // column = column - value
|
||||
Mul(Value), // column = column * value
|
||||
Div(Value), // column = column / value
|
||||
Mod(Value), // column = column % value
|
||||
Case {
|
||||
branches: Vec<(Filter<'static>, Value)>,
|
||||
default: Value,
|
||||
},
|
||||
AddIf { condition: Filter<'static>, value: Value },
|
||||
SubIf { condition: Filter<'static>, value: Value },
|
||||
Coalesce(Value), // COALESCE(column, value)
|
||||
Greatest(Value), // GREATEST(column, value)
|
||||
Least(Value), // LEAST(column, value)
|
||||
Raw { sql: String, values: Vec<Value> },
|
||||
}
|
||||
```
|
||||
|
||||
## Generated Methods
|
||||
|
||||
For each non-binary field, an `eval_{field}` method is generated:
|
||||
|
||||
```rust
|
||||
// Generated for: count: i32
|
||||
pub fn eval_count(mut self, expr: UpdateExpr) -> Self
|
||||
|
||||
// Generated for: score: i64
|
||||
pub fn eval_score(mut self, expr: UpdateExpr) -> Self
|
||||
|
||||
// Generated for: status: String
|
||||
pub fn eval_status(mut self, expr: UpdateExpr) -> Self
|
||||
```
|
||||
|
||||
Binary fields (`Vec<u8>`) do not get `eval_*` methods.
|
||||
|
||||
## Precedence
|
||||
|
||||
`eval_*` methods take precedence over `with_*` if both are set for the same field:
|
||||
|
||||
```rust
|
||||
let form = User::update_form()
|
||||
.with_count(100) // This is ignored
|
||||
.eval_count(UpdateExpr::Add(1.into())); // This is used
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Simple Arithmetic
|
||||
```rust
|
||||
// Increment
|
||||
let form = User::update_form()
|
||||
.eval_count(UpdateExpr::Add(1.into())); // count = count + 1
|
||||
|
||||
// Decrement
|
||||
let form = User::update_form()
|
||||
.eval_balance(UpdateExpr::Sub(50.into())); // balance = balance - 50
|
||||
|
||||
// Multiply
|
||||
let form = User::update_form()
|
||||
.eval_score(UpdateExpr::Mul(2.into())); // score = score * 2
|
||||
```
|
||||
|
||||
### CASE/WHEN Conditional
|
||||
```rust
|
||||
// Update status based on score
|
||||
let form = User::update_form()
|
||||
.eval_tier(UpdateExpr::Case {
|
||||
branches: vec![
|
||||
("score".gt(1000), "platinum".into()),
|
||||
("score".gt(500), "gold".into()),
|
||||
("score".gt(100), "silver".into()),
|
||||
],
|
||||
default: "bronze".into(),
|
||||
});
|
||||
// SQL: tier = CASE
|
||||
// WHEN score > ? THEN ?
|
||||
// WHEN score > ? THEN ?
|
||||
// WHEN score > ? THEN ?
|
||||
// ELSE ? END
|
||||
```
|
||||
|
||||
### Conditional Increment
|
||||
```rust
|
||||
// Add bonus only for premium users
|
||||
let form = User::update_form()
|
||||
.eval_balance(UpdateExpr::AddIf {
|
||||
condition: "is_premium".eq(true),
|
||||
value: 100.into(),
|
||||
});
|
||||
// SQL: balance = CASE WHEN is_premium = ? THEN balance + ? ELSE balance END
|
||||
```
|
||||
|
||||
### Using Filters with Case
|
||||
```rust
|
||||
use sqlx_record::prelude::*;
|
||||
|
||||
// Complex condition with AND
|
||||
let form = User::update_form()
|
||||
.eval_discount(UpdateExpr::Case {
|
||||
branches: vec![
|
||||
(Filter::And(vec![
|
||||
"orders".gt(10),
|
||||
"is_vip".eq(true),
|
||||
]), 20.into()),
|
||||
("orders".gt(5), 10.into()),
|
||||
],
|
||||
default: 0.into(),
|
||||
});
|
||||
```
|
||||
|
||||
### Coalesce (NULL handling)
|
||||
```rust
|
||||
// Set to value if NULL
|
||||
let form = User::update_form()
|
||||
.eval_nickname(UpdateExpr::Coalesce("Anonymous".into()));
|
||||
// SQL: nickname = COALESCE(nickname, ?)
|
||||
```
|
||||
|
||||
### Greatest/Least
|
||||
```rust
|
||||
// Ensure minimum value (clamp)
|
||||
let form = User::update_form()
|
||||
.eval_balance(UpdateExpr::Greatest(0.into())); // balance = GREATEST(balance, 0)
|
||||
|
||||
// Ensure maximum value (cap)
|
||||
let form = User::update_form()
|
||||
.eval_score(UpdateExpr::Least(100.into())); // score = LEAST(score, 100)
|
||||
```
|
||||
|
||||
### Raw SQL Escape Hatch
|
||||
```rust
|
||||
// Simple expression without parameters
|
||||
let form = User::update_form()
|
||||
.raw("computed", "COALESCE(a, 0) + COALESCE(b, 0)");
|
||||
|
||||
// Expression with bind parameters
|
||||
let form = User::update_form()
|
||||
.raw_with_values("adjusted", "ROUND(price * ? * (1 - discount / 100))", values![1.1]);
|
||||
|
||||
// Multiple placeholders
|
||||
let form = User::update_form()
|
||||
.raw_with_values("stats", "JSON_SET(stats, '$.views', JSON_EXTRACT(stats, '$.views') + ?)", values![1]);
|
||||
```
|
||||
|
||||
## Combining with Simple Updates
|
||||
|
||||
```rust
|
||||
let form = User::update_form()
|
||||
.with_name("Alice") // Simple value update
|
||||
.with_email("alice@example.com") // Simple value update
|
||||
.eval_login_count(UpdateExpr::Add(1.into())) // Arithmetic
|
||||
.eval_last_login(UpdateExpr::Set( // Expression set
|
||||
Value::NaiveDateTime(Utc::now().naive_utc())
|
||||
));
|
||||
|
||||
User::update_by_id(&pool, &user_id, form).await?;
|
||||
```
|
||||
|
||||
## Full Update Flow
|
||||
|
||||
```rust
|
||||
use sqlx_record::prelude::*;
|
||||
|
||||
#[derive(Entity, FromRow)]
|
||||
#[table_name = "game_scores"]
|
||||
struct GameScore {
|
||||
#[primary_key]
|
||||
id: Uuid,
|
||||
player_id: Uuid,
|
||||
score: i64,
|
||||
high_score: i64,
|
||||
games_played: i32,
|
||||
tier: String,
|
||||
}
|
||||
|
||||
async fn record_game(pool: &Pool, id: &Uuid, new_score: i64) -> Result<(), Error> {
|
||||
let form = GameScore::update_form()
|
||||
// Increment games played
|
||||
.eval_games_played(UpdateExpr::Add(1.into()))
|
||||
// Update high score if this score is higher
|
||||
.eval_high_score(UpdateExpr::Greatest(new_score.into()))
|
||||
// Set current score
|
||||
.with_score(new_score)
|
||||
// Update tier based on high score
|
||||
.eval_tier(UpdateExpr::Case {
|
||||
branches: vec![
|
||||
("high_score".gt(10000), "master".into()),
|
||||
("high_score".gt(5000), "expert".into()),
|
||||
("high_score".gt(1000), "advanced".into()),
|
||||
],
|
||||
default: "beginner".into(),
|
||||
});
|
||||
|
||||
GameScore::update_by_id(pool, id, form).await
|
||||
}
|
||||
```
|
||||
|
||||
## SQL Generation
|
||||
|
||||
The `update_stmt_with_values()` method generates SQL and collects bind values:
|
||||
|
||||
```rust
|
||||
let form = User::update_form()
|
||||
.with_name("Alice")
|
||||
.eval_count(UpdateExpr::Add(5.into()));
|
||||
|
||||
let (sql, values) = form.update_stmt_with_values();
|
||||
// sql: "name = ?, count = count + ?"
|
||||
// values: [Value::String("Alice"), Value::Int32(5)]
|
||||
```
|
||||
|
||||
## Database Compatibility
|
||||
|
||||
All UpdateExpr variants generate standard SQL that works across:
|
||||
- MySQL
|
||||
- PostgreSQL
|
||||
- SQLite
|
||||
|
||||
Note: `Greatest` and `Least` use SQL functions that are available in all three databases.
|
||||
|
|
@ -0,0 +1,257 @@
|
|||
# sqlx-record Values Skill
|
||||
|
||||
Guide to Value types and database binding.
|
||||
|
||||
## Triggers
|
||||
- "value type", "sql value"
|
||||
- "bind value", "query parameter"
|
||||
- "type conversion"
|
||||
|
||||
## Value Enum
|
||||
|
||||
```rust
|
||||
pub enum Value {
|
||||
// Integers
|
||||
Int8(i8),
|
||||
Uint8(u8),
|
||||
Int16(i16),
|
||||
Uint16(u16),
|
||||
Int32(i32),
|
||||
Uint32(u32),
|
||||
Int64(i64),
|
||||
Uint64(u64),
|
||||
|
||||
// Other primitives
|
||||
String(String),
|
||||
Bool(bool),
|
||||
VecU8(Vec<u8>),
|
||||
|
||||
// Special types
|
||||
Uuid(uuid::Uuid),
|
||||
NaiveDate(NaiveDate),
|
||||
NaiveDateTime(NaiveDateTime),
|
||||
}
|
||||
```
|
||||
|
||||
## Auto-Conversions (From trait)
|
||||
|
||||
```rust
|
||||
// Strings
|
||||
Value::from("hello") // String
|
||||
Value::from("hello".to_string()) // String
|
||||
|
||||
// Integers
|
||||
Value::from(42i32) // Int32
|
||||
Value::from(42i64) // Int64
|
||||
Value::from(&42i32) // Int32 (from reference)
|
||||
Value::from(&42i64) // Int64 (from reference)
|
||||
|
||||
// Boolean
|
||||
Value::from(true) // Bool
|
||||
Value::from(&true) // Bool (from reference)
|
||||
|
||||
// UUID
|
||||
Value::from(uuid::Uuid::new_v4()) // Uuid
|
||||
Value::from(&some_uuid) // Uuid (from reference)
|
||||
|
||||
// Dates
|
||||
Value::from(NaiveDate::from_ymd(2024, 1, 15)) // NaiveDate
|
||||
Value::from(NaiveDateTime::new(...)) // NaiveDateTime
|
||||
```
|
||||
|
||||
## values! Macro
|
||||
|
||||
```rust
|
||||
// Empty
|
||||
values![]
|
||||
|
||||
// Single value (auto-converts)
|
||||
values!["hello"]
|
||||
values![42]
|
||||
values![true]
|
||||
|
||||
// Multiple values
|
||||
values!["name", 30, true, uuid]
|
||||
|
||||
// With explicit types
|
||||
values![
|
||||
Value::String("test".into()),
|
||||
Value::Int32(42),
|
||||
Value::Bool(true)
|
||||
]
|
||||
```
|
||||
|
||||
## Database-Specific Handling
|
||||
|
||||
### Unsigned Integers
|
||||
|
||||
| Type | MySQL | PostgreSQL | SQLite |
|
||||
|------|-------|------------|--------|
|
||||
| Uint8 | Native u8 | Cast to i16 | Cast to i16 |
|
||||
| Uint16 | Native u16 | Cast to i32 | Cast to i32 |
|
||||
| Uint32 | Native u32 | Cast to i64 | Cast to i64 |
|
||||
| Uint64 | Native u64 | Cast to i64* | Cast to i64* |
|
||||
|
||||
*Note: Uint64 values > i64::MAX will overflow when cast.
|
||||
|
||||
### UUID Storage
|
||||
|
||||
| Database | Type | Notes |
|
||||
|----------|------|-------|
|
||||
| MySQL | BINARY(16) | Stored as bytes |
|
||||
| PostgreSQL | UUID | Native type |
|
||||
| SQLite | BLOB | Stored as bytes |
|
||||
|
||||
### JSON Storage
|
||||
|
||||
| Database | Type |
|
||||
|----------|------|
|
||||
| MySQL | JSON |
|
||||
| PostgreSQL | JSONB |
|
||||
| SQLite | TEXT |
|
||||
|
||||
## Bind Functions
|
||||
|
||||
### bind_values
|
||||
Bind values to a raw Query:
|
||||
```rust
|
||||
use sqlx_record::prelude::*;
|
||||
|
||||
let query = sqlx::query("SELECT * FROM users WHERE name = ? AND age > ?");
|
||||
let query = bind_values(query, &values!["Alice", 18]);
|
||||
let rows = query.fetch_all(&pool).await?;
|
||||
```
|
||||
|
||||
### bind_as_values
|
||||
Bind values to a typed QueryAs:
|
||||
```rust
|
||||
let query = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = ?");
|
||||
let query = bind_as_values(query, &values![user_id]);
|
||||
let user = query.fetch_optional(&pool).await?;
|
||||
```
|
||||
|
||||
### bind_scalar_values
|
||||
Bind values to a scalar QueryScalar:
|
||||
```rust
|
||||
let query = sqlx::query_scalar("SELECT COUNT(*) FROM users WHERE active = ?");
|
||||
let query = bind_scalar_values(query, &values![true]);
|
||||
let count: i64 = query.fetch_one(&pool).await?;
|
||||
```
|
||||
|
||||
## BindValues Trait
|
||||
|
||||
Extension trait for fluent binding:
|
||||
```rust
|
||||
use sqlx_record::prelude::BindValues;
|
||||
|
||||
let users = sqlx::query_as::<_, User>("SELECT * FROM users WHERE role = ?")
|
||||
.bind_values(&values!["admin"])
|
||||
.fetch_all(&pool)
|
||||
.await?;
|
||||
```
|
||||
|
||||
## Placeholder Function
|
||||
|
||||
Database-specific placeholder generation:
|
||||
```rust
|
||||
use sqlx_record::prelude::placeholder;
|
||||
|
||||
let ph1 = placeholder(1);
|
||||
let ph2 = placeholder(2);
|
||||
|
||||
// MySQL/SQLite: "?", "?"
|
||||
// PostgreSQL: "$1", "$2"
|
||||
|
||||
let sql = format!("SELECT * FROM users WHERE id = {} AND role = {}", ph1, ph2);
|
||||
```
|
||||
|
||||
## With Filters
|
||||
|
||||
Filters automatically use Value internally:
|
||||
```rust
|
||||
// These are equivalent:
|
||||
filters![("name", "Alice")]
|
||||
filters![("name", Value::String("Alice".into()))]
|
||||
|
||||
// Values are extracted for binding:
|
||||
let (where_clause, values) = Filter::build_where_clause(&filters);
|
||||
// values: Vec<Value>
|
||||
```
|
||||
|
||||
## Custom Queries with Values
|
||||
|
||||
```rust
|
||||
use sqlx_record::prelude::*;
|
||||
|
||||
async fn complex_query(pool: &Pool, status: &str, min_age: i32) -> Result<Vec<User>> {
|
||||
let values = values![status, min_age];
|
||||
|
||||
let sql = format!(
|
||||
"SELECT * FROM users WHERE status = {} AND age >= {} ORDER BY name",
|
||||
placeholder(1),
|
||||
placeholder(2)
|
||||
);
|
||||
|
||||
let query = sqlx::query_as::<_, User>(&sql);
|
||||
let query = bind_as_values(query, &values);
|
||||
|
||||
query.fetch_all(pool).await
|
||||
}
|
||||
```
|
||||
|
||||
## Updater Enum
|
||||
|
||||
For more complex update patterns:
|
||||
```rust
|
||||
pub enum Updater<'a> {
|
||||
Set(&'a str, Value), // SET field = value
|
||||
Increment(&'a str, Value), // SET field = field + value
|
||||
Decrement(&'a str, Value), // SET field = field - value
|
||||
}
|
||||
```
|
||||
|
||||
## Type Helpers
|
||||
|
||||
### query_fields
|
||||
Extract field names from aliased field list:
|
||||
```rust
|
||||
let fields = vec!["id", "name as user_name", "email"];
|
||||
let result = query_fields(fields);
|
||||
// "id, name, email"
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Dynamic WHERE Clause
|
||||
```rust
|
||||
fn build_query(filters: &[(&str, Value)]) -> (String, Vec<Value>) {
|
||||
let mut conditions = Vec::new();
|
||||
let mut values = Vec::new();
|
||||
|
||||
for (i, (field, value)) in filters.iter().enumerate() {
|
||||
conditions.push(format!("{} = {}", field, placeholder(i + 1)));
|
||||
values.push(value.clone());
|
||||
}
|
||||
|
||||
let where_clause = if conditions.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("WHERE {}", conditions.join(" AND "))
|
||||
};
|
||||
|
||||
(where_clause, values)
|
||||
}
|
||||
```
|
||||
|
||||
### Batch Insert Values
|
||||
```rust
|
||||
fn batch_values(users: &[User]) -> Vec<Value> {
|
||||
users.iter().flat_map(|u| {
|
||||
vec![
|
||||
Value::Uuid(u.id),
|
||||
Value::String(u.name.clone()),
|
||||
Value::String(u.email.clone()),
|
||||
]
|
||||
}).collect()
|
||||
}
|
||||
```
|
||||
137
CLAUDE.md
137
CLAUDE.md
|
|
@ -14,7 +14,7 @@ This file provides guidance to Claude Code when working with this repository.
|
|||
```
|
||||
sqlx-record/
|
||||
├── src/ # Core library
|
||||
│ ├── lib.rs # Public API exports and prelude
|
||||
│ ├── lib.rs # Public API exports, prelude, lookup macros, new_uuid
|
||||
│ ├── models.rs # EntityChange struct, Action enum
|
||||
│ ├── repositories.rs # Database query functions for entity changes
|
||||
│ ├── value.rs # Type-safe Value enum, bind functions
|
||||
|
|
@ -26,6 +26,15 @@ sqlx-record/
|
|||
│ └── string_utils.rs # Pluralization helpers
|
||||
├── sqlx-record-ctl/ # CLI tool for audit table management
|
||||
│ └── src/main.rs
|
||||
├── mcp/ # MCP server for documentation/code generation
|
||||
│ └── src/main.rs # sqlx-record-expert executable
|
||||
├── .claude/skills/ # Claude Code skills documentation
|
||||
│ ├── sqlx-record.md # Overview and quick reference
|
||||
│ ├── sqlx-entity.md # #[derive(Entity)] detailed guide
|
||||
│ ├── sqlx-filters.md # Filter system guide
|
||||
│ ├── sqlx-audit.md # Audit trail guide
|
||||
│ ├── sqlx-lookup.md # Lookup tables guide
|
||||
│ └── sqlx-values.md # Value types guide
|
||||
└── Cargo.toml # Workspace root
|
||||
```
|
||||
|
||||
|
|
@ -50,6 +59,9 @@ cargo build --features "mysql,derive"
|
|||
# Build CLI tool
|
||||
cargo build -p sqlx-record-ctl --features mysql
|
||||
|
||||
# Build MCP server
|
||||
cargo build -p sqlx-record-mcp
|
||||
|
||||
# Test
|
||||
cargo test --features mysql
|
||||
|
||||
|
|
@ -57,6 +69,125 @@ cargo test --features mysql
|
|||
make tag
|
||||
```
|
||||
|
||||
## MCP Server (sqlx-record-expert)
|
||||
|
||||
The `mcp/` directory contains an MCP server providing:
|
||||
|
||||
**Tools:**
|
||||
- `generate_entity` - Generate Entity struct code
|
||||
- `generate_filter` - Generate filter expressions
|
||||
- `generate_lookup` - Generate lookup_table!/lookup_options! code
|
||||
- `explain_feature` - Get detailed documentation
|
||||
|
||||
**Resources:**
|
||||
- `sqlx-record://docs/overview` - Library overview
|
||||
- `sqlx-record://docs/derive` - Derive macro docs
|
||||
- `sqlx-record://docs/filters` - Filter system docs
|
||||
- `sqlx-record://docs/values` - Value types docs
|
||||
- `sqlx-record://docs/lookup` - Lookup tables docs
|
||||
- `sqlx-record://docs/audit` - Audit trail docs
|
||||
- `sqlx-record://docs/examples` - Complete examples
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
cargo build -p sqlx-record-mcp
|
||||
./target/debug/sqlx-record-expert
|
||||
```
|
||||
|
||||
## Lookup Macros
|
||||
|
||||
### lookup_table!
|
||||
Creates database-backed lookup entity with code enum:
|
||||
```rust
|
||||
lookup_table!(OrderStatus, "pending", "shipped", "delivered");
|
||||
// Generates: struct OrderStatus, enum OrderStatusCode, constants
|
||||
```
|
||||
|
||||
### lookup_options!
|
||||
Creates code enum without database entity:
|
||||
```rust
|
||||
lookup_options!(PaymentMethod, "credit-card", "paypal", "bank-transfer");
|
||||
// Generates: enum PaymentMethodCode, struct PaymentMethod (constants only)
|
||||
```
|
||||
|
||||
## Time-Ordered UUIDs
|
||||
|
||||
```rust
|
||||
use sqlx_record::new_uuid;
|
||||
let id = new_uuid(); // Timestamp prefix (8 bytes) + random (8 bytes)
|
||||
```
|
||||
|
||||
## Connection Provider
|
||||
|
||||
Flexible connection management - borrow existing or lazily acquire from pool:
|
||||
|
||||
```rust
|
||||
use sqlx_record::prelude::ConnProvider;
|
||||
|
||||
// From borrowed connection
|
||||
let mut provider = ConnProvider::from_ref(&mut conn);
|
||||
|
||||
// From pool (lazy acquisition)
|
||||
let mut provider = ConnProvider::from_pool(pool.clone());
|
||||
|
||||
// Get connection (acquires on first call for Owned variant)
|
||||
let conn = provider.get_conn().await?;
|
||||
```
|
||||
|
||||
## UpdateExpr - Advanced Update Operations
|
||||
|
||||
Beyond simple value updates, use `eval_*` methods for arithmetic and conditional updates:
|
||||
|
||||
```rust
|
||||
use sqlx_record::prelude::*;
|
||||
|
||||
// Arithmetic operations
|
||||
let form = User::update_form()
|
||||
.with_name("Alice") // Simple value
|
||||
.eval_count(UpdateExpr::Add(1.into())) // count = count + 1
|
||||
.eval_score(UpdateExpr::Sub(10.into())); // score = score - 10
|
||||
|
||||
// CASE/WHEN conditional
|
||||
let form = User::update_form()
|
||||
.eval_status(UpdateExpr::Case {
|
||||
branches: vec![
|
||||
("score".gt(100), "vip".into()),
|
||||
("score".gt(50), "premium".into()),
|
||||
],
|
||||
default: "standard".into(),
|
||||
});
|
||||
|
||||
// Conditional increment
|
||||
let form = User::update_form()
|
||||
.eval_balance(UpdateExpr::AddIf {
|
||||
condition: "is_premium".eq(true),
|
||||
value: 100.into(),
|
||||
});
|
||||
|
||||
// Raw SQL escape hatch
|
||||
let form = User::update_form()
|
||||
.raw("computed", "COALESCE(a, 0) + COALESCE(b, 0)")
|
||||
.raw_with_values("adjusted", "value * ? + ?", values![1.5, 10]);
|
||||
```
|
||||
|
||||
### UpdateExpr Variants
|
||||
|
||||
| Variant | SQL Generated |
|
||||
|---------|--------------|
|
||||
| `Set(value)` | `column = ?` |
|
||||
| `Add(value)` | `column = column + ?` |
|
||||
| `Sub(value)` | `column = column - ?` |
|
||||
| `Mul(value)` | `column = column * ?` |
|
||||
| `Div(value)` | `column = column / ?` |
|
||||
| `Mod(value)` | `column = column % ?` |
|
||||
| `Case { branches, default }` | `column = CASE WHEN ... THEN ... ELSE ... END` |
|
||||
| `AddIf { condition, value }` | `column = CASE WHEN cond THEN column + ? ELSE column END` |
|
||||
| `SubIf { condition, value }` | `column = CASE WHEN cond THEN column - ? ELSE column END` |
|
||||
| `Coalesce(value)` | `column = COALESCE(column, ?)` |
|
||||
| `Greatest(value)` | `column = GREATEST(column, ?)` |
|
||||
| `Least(value)` | `column = LEAST(column, ?)` |
|
||||
| `Raw { sql, values }` | `column = {sql}` (escape hatch) |
|
||||
|
||||
## Derive Macro API
|
||||
|
||||
### Entity Attributes
|
||||
|
|
@ -158,6 +289,10 @@ Filter::Or(vec![filters])
|
|||
| JSON type | `JSON` | `JSONB` | `TEXT` |
|
||||
| ILIKE | `LOWER() LIKE LOWER()` | Native | `LOWER() LIKE LOWER()` |
|
||||
| Index hints | `USE INDEX()` | N/A | N/A |
|
||||
| Unsigned ints | Native | Cast to signed | Cast to signed |
|
||||
|
||||
**Unsigned integer conversion (PostgreSQL/SQLite):**
|
||||
- `u8` → `i16`, `u16` → `i32`, `u32` → `i64`, `u64` → `i64`
|
||||
|
||||
## Value Types
|
||||
|
||||
|
|
|
|||
10
Cargo.toml
10
Cargo.toml
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "sqlx-record"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
description = "Entity CRUD and change tracking for SQL databases with SQLx"
|
||||
|
||||
|
|
@ -8,12 +8,16 @@ description = "Entity CRUD and change tracking for SQL databases with SQLx"
|
|||
sqlx-record-derive = { path = "sqlx-record-derive", optional = true }
|
||||
sqlx = { version = "0.8", features = ["runtime-tokio", "uuid", "chrono", "json"] }
|
||||
serde_json = "1.0"
|
||||
uuid = { version = "1", features = ["v4"]}
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
chrono = "0.4"
|
||||
rand = "0.8"
|
||||
paste = "1.0"
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
"sqlx-record-derive",
|
||||
"sqlx-record-ctl"
|
||||
"sqlx-record-ctl",
|
||||
"mcp"
|
||||
]
|
||||
|
||||
[features]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "sqlx-record-mcp"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
description = "MCP server providing sqlx-record documentation and code generation"
|
||||
|
||||
[[bin]]
|
||||
name = "sqlx-record-expert"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "sqlx-record-ctl"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
description = "CLI tool for managing sqlx-record audit tables"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "sqlx-record-derive"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
description = "Derive macros for sqlx-record"
|
||||
|
||||
|
|
|
|||
|
|
@ -728,6 +728,16 @@ fn generate_get_impl(
|
|||
}
|
||||
}
|
||||
}
|
||||
/// Check if a type is a binary type (Vec<u8>)
|
||||
fn is_binary_type(ty: &Type) -> bool {
|
||||
if let Type::Path(type_path) = ty {
|
||||
let path_str = quote!(#type_path).to_string().replace(" ", "");
|
||||
path_str == "Vec<u8>" || path_str == "std::vec::Vec<u8>"
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_update_impl(
|
||||
name: &Ident,
|
||||
update_form_name: &Ident,
|
||||
|
|
@ -779,6 +789,23 @@ fn generate_update_impl(
|
|||
}
|
||||
});
|
||||
|
||||
// Generate eval_* methods for non-binary fields
|
||||
let eval_methods: Vec<_> = update_fields.iter()
|
||||
.filter(|f| !is_binary_type(&f.ty))
|
||||
.map(|field| {
|
||||
let method_name = format_ident!("eval_{}", field.ident);
|
||||
let db_name = &field.db_name;
|
||||
quote! {
|
||||
/// Set field using an UpdateExpr for complex operations (arithmetic, CASE/WHEN, etc.)
|
||||
/// Takes precedence over with_* if both are set.
|
||||
pub fn #method_name(mut self, expr: ::sqlx_record::prelude::UpdateExpr) -> Self {
|
||||
self._exprs.insert(#db_name, expr);
|
||||
self
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Version increment - use CASE WHEN for cross-database compatibility
|
||||
let version_increment = if let Some(vfield) = version_field {
|
||||
let version_db_name = &vfield.db_name;
|
||||
|
|
@ -807,9 +834,20 @@ fn generate_update_impl(
|
|||
};
|
||||
|
||||
quote! {
|
||||
#[derive(Default)]
|
||||
/// Update form with support for simple value updates and complex expressions
|
||||
pub struct #update_form_name #ty_generics #where_clause {
|
||||
#(pub #field_idents: Option<#field_types>,)*
|
||||
/// Expression-based updates (eval_* methods). Takes precedence over with_* for same field.
|
||||
pub _exprs: std::collections::HashMap<&'static str, ::sqlx_record::prelude::UpdateExpr>,
|
||||
}
|
||||
|
||||
impl #impl_generics Default for #update_form_name #ty_generics #where_clause {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
#(#field_idents: None,)*
|
||||
_exprs: std::collections::HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl #impl_generics #name #ty_generics #where_clause {
|
||||
|
|
@ -827,30 +865,106 @@ fn generate_update_impl(
|
|||
|
||||
#(#builder_methods)*
|
||||
|
||||
pub fn update_stmt(&self) -> String {
|
||||
#(#eval_methods)*
|
||||
|
||||
/// Raw SQL expression escape hatch for any field (no bind parameters).
|
||||
/// For expressions with parameters, use `raw_with_values()`.
|
||||
pub fn raw(mut self, field: &'static str, sql: impl Into<String>) -> Self {
|
||||
self._exprs.insert(field, ::sqlx_record::prelude::UpdateExpr::Raw {
|
||||
sql: sql.into(),
|
||||
values: vec![],
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
/// Raw SQL expression escape hatch with bind parameters.
|
||||
/// Use `?` for placeholders - they will be converted to proper database placeholders.
|
||||
pub fn raw_with_values(mut self, field: &'static str, sql: impl Into<String>, values: Vec<::sqlx_record::prelude::Value>) -> Self {
|
||||
self._exprs.insert(field, ::sqlx_record::prelude::UpdateExpr::Raw {
|
||||
sql: sql.into(),
|
||||
values,
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
/// Generate UPDATE SET clause and collect values for binding.
|
||||
/// Expression fields (eval_*) take precedence over simple value fields (with_*).
|
||||
pub fn update_stmt_with_values(&self) -> (String, Vec<::sqlx_record::prelude::Value>) {
|
||||
let mut parts = Vec::new();
|
||||
let mut values = Vec::new();
|
||||
let mut idx = 1usize;
|
||||
|
||||
#(
|
||||
if self.#field_idents.is_some() {
|
||||
// Check if this field has an expression (takes precedence)
|
||||
if let Some(expr) = self._exprs.get(#db_names) {
|
||||
let (sql, expr_values) = expr.build_sql(#db_names, idx);
|
||||
parts.push(format!("{} = {}", #db_names, sql));
|
||||
idx += expr_values.len();
|
||||
values.extend(expr_values);
|
||||
} else if let Some(ref value) = self.#field_idents {
|
||||
parts.push(format!("{} = {}", #db_names, ::sqlx_record::prelude::placeholder(idx)));
|
||||
values.push(::sqlx_record::prelude::Value::from(value.clone()));
|
||||
idx += 1;
|
||||
}
|
||||
)*
|
||||
|
||||
#version_increment
|
||||
|
||||
parts.join(", ")
|
||||
(parts.join(", "), values)
|
||||
}
|
||||
|
||||
/// Generate UPDATE SET clause (backward compatible, without values).
|
||||
/// For new code, prefer `update_stmt_with_values()`.
|
||||
pub fn update_stmt(&self) -> String {
|
||||
self.update_stmt_with_values().0
|
||||
}
|
||||
|
||||
/// Bind all form values to query in correct order.
|
||||
/// Handles both simple values and expression values, respecting expression precedence.
|
||||
pub fn bind_all_values(&self, mut query: sqlx::query::Query<'_, #db, #db_args>)
|
||||
-> sqlx::query::Query<'_, #db, #db_args>
|
||||
{
|
||||
#(
|
||||
// Expression takes precedence over simple value
|
||||
if let Some(expr) = self._exprs.get(#db_names) {
|
||||
let (_, expr_values) = expr.build_sql(#db_names, 1);
|
||||
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
|
||||
}
|
||||
|
||||
/// Legacy binding method - only binds simple Option values (ignores expressions).
|
||||
/// 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>)
|
||||
-> sqlx::query::Query<'q, #db, #db_args>
|
||||
{
|
||||
if self._exprs.is_empty() {
|
||||
// No expressions, use simple binding
|
||||
#(
|
||||
if let Some(ref value) = self.#field_idents {
|
||||
query = query.bind(value);
|
||||
}
|
||||
)*
|
||||
query
|
||||
} else {
|
||||
// Has expressions, use full binding
|
||||
self.bind_all_values(query)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this form uses any expressions
|
||||
pub fn has_expressions(&self) -> bool {
|
||||
!self._exprs.is_empty()
|
||||
}
|
||||
|
||||
/// Get the number of bind parameters this form will use.
|
||||
pub fn param_count(&self) -> usize {
|
||||
self.update_stmt_with_values().1.len()
|
||||
}
|
||||
|
||||
pub const fn table_name() -> &'static str {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,145 @@
|
|||
use sqlx::pool::PoolConnection;
|
||||
|
||||
#[cfg(feature = "mysql")]
|
||||
use sqlx::{MySql, MySqlPool};
|
||||
|
||||
#[cfg(feature = "postgres")]
|
||||
use sqlx::{Postgres, PgPool};
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
use sqlx::{Sqlite, SqlitePool};
|
||||
|
||||
// ============================================================================
|
||||
// MySQL Implementation
|
||||
// ============================================================================
|
||||
|
||||
#[cfg(feature = "mysql")]
|
||||
pub enum ConnProvider<'a> {
|
||||
/// Stores a reference to an existing connection
|
||||
Borrowed {
|
||||
conn: &'a mut PoolConnection<MySql>,
|
||||
},
|
||||
/// Stores an owned connection acquired from a pool
|
||||
Owned {
|
||||
pool: MySqlPool,
|
||||
conn: Option<PoolConnection<MySql>>,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(feature = "mysql")]
|
||||
impl<'a> ConnProvider<'a> {
|
||||
/// Create a ConnProvider from a borrowed connection reference
|
||||
pub fn from_ref(conn: &'a mut PoolConnection<MySql>) -> Self {
|
||||
ConnProvider::Borrowed { conn }
|
||||
}
|
||||
|
||||
/// Create a ConnProvider that will lazily acquire a connection from the pool
|
||||
pub fn from_pool(pool: MySqlPool) -> Self {
|
||||
ConnProvider::Owned { pool, conn: None }
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the underlying connection.
|
||||
/// For borrowed connections, returns the reference directly.
|
||||
/// For owned connections, lazily acquires from pool on first call.
|
||||
pub async fn get_conn(&mut self) -> Result<&mut PoolConnection<MySql>, sqlx::Error> {
|
||||
match self {
|
||||
ConnProvider::Borrowed { conn } => Ok(conn),
|
||||
ConnProvider::Owned { pool, conn } => {
|
||||
if conn.is_none() {
|
||||
*conn = Some(pool.acquire().await?);
|
||||
}
|
||||
Ok(conn.as_mut().unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PostgreSQL Implementation
|
||||
// ============================================================================
|
||||
|
||||
#[cfg(feature = "postgres")]
|
||||
pub enum ConnProvider<'a> {
|
||||
/// Stores a reference to an existing connection
|
||||
Borrowed {
|
||||
conn: &'a mut PoolConnection<Postgres>,
|
||||
},
|
||||
/// Stores an owned connection acquired from a pool
|
||||
Owned {
|
||||
pool: PgPool,
|
||||
conn: Option<PoolConnection<Postgres>>,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(feature = "postgres")]
|
||||
impl<'a> ConnProvider<'a> {
|
||||
/// Create a ConnProvider from a borrowed connection reference
|
||||
pub fn from_ref(conn: &'a mut PoolConnection<Postgres>) -> Self {
|
||||
ConnProvider::Borrowed { conn }
|
||||
}
|
||||
|
||||
/// Create a ConnProvider that will lazily acquire a connection from the pool
|
||||
pub fn from_pool(pool: PgPool) -> Self {
|
||||
ConnProvider::Owned { pool, conn: None }
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the underlying connection.
|
||||
/// For borrowed connections, returns the reference directly.
|
||||
/// For owned connections, lazily acquires from pool on first call.
|
||||
pub async fn get_conn(&mut self) -> Result<&mut PoolConnection<Postgres>, sqlx::Error> {
|
||||
match self {
|
||||
ConnProvider::Borrowed { conn } => Ok(conn),
|
||||
ConnProvider::Owned { pool, conn } => {
|
||||
if conn.is_none() {
|
||||
*conn = Some(pool.acquire().await?);
|
||||
}
|
||||
Ok(conn.as_mut().unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SQLite Implementation
|
||||
// ============================================================================
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
pub enum ConnProvider<'a> {
|
||||
/// Stores a reference to an existing connection
|
||||
Borrowed {
|
||||
conn: &'a mut PoolConnection<Sqlite>,
|
||||
},
|
||||
/// Stores an owned connection acquired from a pool
|
||||
Owned {
|
||||
pool: SqlitePool,
|
||||
conn: Option<PoolConnection<Sqlite>>,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
impl<'a> ConnProvider<'a> {
|
||||
/// Create a ConnProvider from a borrowed connection reference
|
||||
pub fn from_ref(conn: &'a mut PoolConnection<Sqlite>) -> Self {
|
||||
ConnProvider::Borrowed { conn }
|
||||
}
|
||||
|
||||
/// Create a ConnProvider that will lazily acquire a connection from the pool
|
||||
pub fn from_pool(pool: SqlitePool) -> Self {
|
||||
ConnProvider::Owned { pool, conn: None }
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the underlying connection.
|
||||
/// For borrowed connections, returns the reference directly.
|
||||
/// For owned connections, lazily acquires from pool on first call.
|
||||
pub async fn get_conn(&mut self) -> Result<&mut PoolConnection<Sqlite>, sqlx::Error> {
|
||||
match self {
|
||||
ConnProvider::Borrowed { conn } => Ok(conn),
|
||||
ConnProvider::Owned { pool, conn } => {
|
||||
if conn.is_none() {
|
||||
*conn = Some(pool.acquire().await?);
|
||||
}
|
||||
Ok(conn.as_mut().unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -112,6 +112,27 @@ pub fn placeholder(index: usize) -> String {
|
|||
}
|
||||
|
||||
impl Filter<'_> {
|
||||
/// Returns the number of bind parameters this filter will use
|
||||
pub fn param_count(&self) -> usize {
|
||||
match self {
|
||||
Filter::Equal(_, _) => 1,
|
||||
Filter::NotEqual(_, _) => 1,
|
||||
Filter::GreaterThan(_, _) => 1,
|
||||
Filter::GreaterThanOrEqual(_, _) => 1,
|
||||
Filter::LessThan(_, _) => 1,
|
||||
Filter::LessThanOrEqual(_, _) => 1,
|
||||
Filter::Like(_, _) => 1,
|
||||
Filter::ILike(_, _) => 1,
|
||||
Filter::NotLike(_, _) => 1,
|
||||
Filter::In(_, values) => values.len(),
|
||||
Filter::NotIn(_, values) => values.len(),
|
||||
Filter::IsNull(_) => 0,
|
||||
Filter::IsNotNull(_) => 0,
|
||||
Filter::And(filters) => filters.iter().map(|f| f.param_count()).sum(),
|
||||
Filter::Or(filters) => filters.iter().map(|f| f.param_count()).sum(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_where_clause(filters: &[Filter]) -> (String, Vec<Value>) {
|
||||
Self::build_where_clause_with_offset(filters, 1)
|
||||
}
|
||||
|
|
|
|||
164
src/lib.rs
164
src/lib.rs
|
|
@ -1,19 +1,183 @@
|
|||
use chrono::Utc;
|
||||
use rand::random;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub mod models;
|
||||
pub mod repositories;
|
||||
mod helpers;
|
||||
mod value;
|
||||
mod filter;
|
||||
mod conn_provider;
|
||||
|
||||
// Re-export the sqlx_record_derive module on feature flag
|
||||
#[cfg(feature = "derive")]
|
||||
pub use sqlx_record_derive::{Entity, Update};
|
||||
|
||||
/// Creates a time-ordered UUID with timestamp prefix for better database indexing.
|
||||
/// First 8 bytes: millisecond timestamp (big-endian)
|
||||
/// Last 8 bytes: random data
|
||||
#[inline]
|
||||
pub fn new_uuid() -> Uuid {
|
||||
let timestamp = Utc::now().timestamp_millis() as u64;
|
||||
let random = random::<u64>();
|
||||
let mut bytes = [0u8; 16];
|
||||
bytes[..8].copy_from_slice(×tamp.to_be_bytes());
|
||||
bytes[8..].copy_from_slice(&random.to_be_bytes());
|
||||
|
||||
Uuid::from_bytes(bytes)
|
||||
}
|
||||
|
||||
/// Creates a lookup table entity struct with an associated code enum.
|
||||
///
|
||||
/// Generates:
|
||||
/// - A struct with `code`, `name`, `description`, and `is_active` fields
|
||||
/// - An enum `{Name}Code` with variants for each code
|
||||
/// - Constants on the struct for each code string
|
||||
/// - `Display` impl for the enum
|
||||
/// - `TryFrom<&str>` impl for the enum
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// lookup_table!(UserStatus, "active", "inactive", "suspended");
|
||||
/// // Creates:
|
||||
/// // - struct UserStatus { code, name, description, is_active }
|
||||
/// // - enum UserStatusCode { Active, Inactive, Suspended }
|
||||
/// // - UserStatus::ACTIVE, UserStatus::INACTIVE, UserStatus::SUSPENDED constants
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! lookup_table {
|
||||
($name:ident, $($code:literal),+ $(,)?) => {
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, sqlx_record::Entity, sqlx::FromRow)]
|
||||
pub struct $name {
|
||||
#[primary_key]
|
||||
pub code: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub is_active: bool,
|
||||
}
|
||||
|
||||
paste::paste! {
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum [<$name Code>] {
|
||||
$(
|
||||
#[allow(non_camel_case_types)]
|
||||
[<$code:camel>],
|
||||
)+
|
||||
}
|
||||
|
||||
impl std::fmt::Display for [<$name Code>] {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
$(
|
||||
[<$name Code>]::[<$code:camel>] => write!(f, $code),
|
||||
)+
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl $name {
|
||||
$(
|
||||
pub const [<$code:upper>]: &'static str = $code;
|
||||
)+
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for [<$name Code>] {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
||||
match s {
|
||||
$(
|
||||
$code => Ok([<$name Code>]::[<$code:camel>]),
|
||||
)+
|
||||
_ => Err(format!("Unknown {} code: {}", stringify!($name).to_lowercase(), s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Creates a code enum without an associated entity struct.
|
||||
///
|
||||
/// Use this when you need type-safe code constants but the lookup data
|
||||
/// is stored elsewhere or doesn't need CRUD operations.
|
||||
///
|
||||
/// Generates:
|
||||
/// - An enum `{Name}Code` with variants for each code
|
||||
/// - Constants on a unit struct for each code string
|
||||
/// - `Display` impl for the enum
|
||||
/// - `TryFrom<&str>` impl for the enum
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// lookup_options!(RefundPolicy, "pro-rata", "full-24-hours", "no-refunds");
|
||||
/// // Creates:
|
||||
/// // - enum RefundPolicyCode { ProRata, Full24Hours, NoRefunds }
|
||||
/// // - RefundPolicy::PRO_RATA, RefundPolicy::FULL_24_HOURS, etc.
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! lookup_options {
|
||||
($name:ident, $($code:literal),+ $(,)?) => {
|
||||
paste::paste! {
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum [<$name Code>] {
|
||||
$(
|
||||
#[allow(non_camel_case_types)]
|
||||
[<$code:camel>],
|
||||
)+
|
||||
}
|
||||
|
||||
impl std::fmt::Display for [<$name Code>] {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
$(
|
||||
[<$name Code>]::[<$code:camel>] => write!(f, $code),
|
||||
)+
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Constants for code string values
|
||||
#[allow(dead_code)]
|
||||
pub struct $name;
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl $name {
|
||||
$(
|
||||
pub const [<$code:upper>]: &'static str = $code;
|
||||
)+
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for [<$name Code>] {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
||||
match s {
|
||||
$(
|
||||
$code => Ok([<$name Code>]::[<$code:camel>]),
|
||||
)+
|
||||
_ => Err(format!("Unknown {} code: {}", stringify!($name).to_lowercase(), s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub mod prelude {
|
||||
pub use crate::value::*;
|
||||
pub use crate::filter::*;
|
||||
pub use crate::{filter_or, filter_and, filters, update_entity_func};
|
||||
pub use crate::{filter_or as or, filter_and as and};
|
||||
pub use crate::values;
|
||||
pub use crate::{new_uuid, lookup_table, lookup_options};
|
||||
|
||||
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
|
||||
pub use crate::conn_provider::ConnProvider;
|
||||
|
||||
#[cfg(feature = "derive")]
|
||||
pub use sqlx_record_derive::{Entity, Update};
|
||||
|
|
|
|||
439
src/value.rs
439
src/value.rs
|
|
@ -1,5 +1,6 @@
|
|||
use sqlx::query::{Query, QueryAs, QueryScalar};
|
||||
use sqlx::types::chrono::{NaiveDate, NaiveDateTime};
|
||||
use crate::filter::placeholder;
|
||||
|
||||
// Database type alias based on enabled feature
|
||||
#[cfg(feature = "mysql")]
|
||||
|
|
@ -11,7 +12,7 @@ pub type DB = sqlx::Postgres;
|
|||
#[cfg(feature = "sqlite")]
|
||||
pub type DB = sqlx::Sqlite;
|
||||
|
||||
// Arguments type alias
|
||||
// Arguments type alias (used for non-lifetime-sensitive contexts)
|
||||
#[cfg(feature = "mysql")]
|
||||
pub type Arguments = sqlx::mysql::MySqlArguments;
|
||||
|
||||
|
|
@ -21,6 +22,16 @@ pub type Arguments = sqlx::postgres::PgArguments;
|
|||
#[cfg(feature = "sqlite")]
|
||||
pub type Arguments = sqlx::sqlite::SqliteArguments<'static>;
|
||||
|
||||
// Lifetime-aware arguments type for SQLite
|
||||
#[cfg(feature = "sqlite")]
|
||||
pub type Arguments_<'q> = sqlx::sqlite::SqliteArguments<'q>;
|
||||
|
||||
#[cfg(feature = "mysql")]
|
||||
pub type Arguments_<'q> = sqlx::mysql::MySqlArguments;
|
||||
|
||||
#[cfg(feature = "postgres")]
|
||||
pub type Arguments_<'q> = sqlx::postgres::PgArguments;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Value {
|
||||
Int8(i8),
|
||||
|
|
@ -39,15 +50,206 @@ pub enum Value {
|
|||
NaiveDateTime(NaiveDateTime),
|
||||
}
|
||||
|
||||
pub enum Updater<'a> {
|
||||
Set(&'a str, Value),
|
||||
Increment(&'a str, Value),
|
||||
Decrement(&'a str, Value),
|
||||
/// Expression for column updates beyond simple value assignment.
|
||||
/// Used with `eval_*` methods on UpdateForm.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum UpdateExpr {
|
||||
/// column = value (equivalent to with_* methods)
|
||||
Set(Value),
|
||||
|
||||
/// column = column + value
|
||||
Add(Value),
|
||||
|
||||
/// column = column - value
|
||||
Sub(Value),
|
||||
|
||||
/// column = column * value
|
||||
Mul(Value),
|
||||
|
||||
/// column = column / value
|
||||
Div(Value),
|
||||
|
||||
/// column = column % value
|
||||
Mod(Value),
|
||||
|
||||
/// column = CASE WHEN cond1 THEN val1 WHEN cond2 THEN val2 ... ELSE default END
|
||||
Case {
|
||||
/// Vec of (condition, value) pairs for WHEN branches
|
||||
branches: Vec<(crate::filter::Filter<'static>, Value)>,
|
||||
/// Default value for ELSE branch
|
||||
default: Value,
|
||||
},
|
||||
|
||||
/// column = CASE WHEN condition THEN column + value ELSE column END
|
||||
AddIf {
|
||||
condition: crate::filter::Filter<'static>,
|
||||
value: Value,
|
||||
},
|
||||
|
||||
/// column = CASE WHEN condition THEN column - value ELSE column END
|
||||
SubIf {
|
||||
condition: crate::filter::Filter<'static>,
|
||||
value: Value,
|
||||
},
|
||||
|
||||
/// column = COALESCE(column, value)
|
||||
Coalesce(Value),
|
||||
|
||||
/// column = GREATEST(column, value) - MySQL/PostgreSQL only
|
||||
Greatest(Value),
|
||||
|
||||
/// column = LEAST(column, value) - MySQL/PostgreSQL only
|
||||
Least(Value),
|
||||
|
||||
/// Raw SQL expression escape hatch: column = {sql}
|
||||
/// Placeholders in sql should use `?` and will be replaced with proper placeholders
|
||||
Raw {
|
||||
sql: String,
|
||||
values: Vec<Value>,
|
||||
},
|
||||
}
|
||||
|
||||
impl UpdateExpr {
|
||||
/// Build SQL expression and collect values for binding.
|
||||
/// Returns (sql_fragment, values_to_bind)
|
||||
pub fn build_sql(&self, column: &str, start_idx: usize) -> (String, Vec<Value>) {
|
||||
use crate::filter::Filter;
|
||||
|
||||
match self {
|
||||
UpdateExpr::Set(v) => {
|
||||
(placeholder(start_idx), vec![v.clone()])
|
||||
}
|
||||
UpdateExpr::Add(v) => {
|
||||
(format!("{} + {}", column, placeholder(start_idx)), vec![v.clone()])
|
||||
}
|
||||
UpdateExpr::Sub(v) => {
|
||||
(format!("{} - {}", column, placeholder(start_idx)), vec![v.clone()])
|
||||
}
|
||||
UpdateExpr::Mul(v) => {
|
||||
(format!("{} * {}", column, placeholder(start_idx)), vec![v.clone()])
|
||||
}
|
||||
UpdateExpr::Div(v) => {
|
||||
(format!("{} / {}", column, placeholder(start_idx)), vec![v.clone()])
|
||||
}
|
||||
UpdateExpr::Mod(v) => {
|
||||
(format!("{} % {}", column, placeholder(start_idx)), vec![v.clone()])
|
||||
}
|
||||
UpdateExpr::Case { branches, default } => {
|
||||
let mut sql_parts = vec!["CASE".to_string()];
|
||||
let mut values = Vec::new();
|
||||
let mut idx = start_idx;
|
||||
|
||||
for (condition, value) in branches {
|
||||
let (cond_sql, cond_values) = Filter::build_where_clause_with_offset(
|
||||
&[condition.clone()],
|
||||
idx,
|
||||
);
|
||||
idx += cond_values.len();
|
||||
values.extend(cond_values);
|
||||
|
||||
sql_parts.push(format!("WHEN {} THEN {}", cond_sql, placeholder(idx)));
|
||||
values.push(value.clone());
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
sql_parts.push(format!("ELSE {} END", placeholder(idx)));
|
||||
values.push(default.clone());
|
||||
|
||||
(sql_parts.join(" "), values)
|
||||
}
|
||||
UpdateExpr::AddIf { condition, value } => {
|
||||
let (cond_sql, cond_values) = Filter::build_where_clause_with_offset(
|
||||
&[condition.clone()],
|
||||
start_idx,
|
||||
);
|
||||
let mut values = cond_values;
|
||||
let val_idx = start_idx + values.len();
|
||||
|
||||
let sql = format!(
|
||||
"CASE WHEN {} THEN {} + {} ELSE {} END",
|
||||
cond_sql,
|
||||
column,
|
||||
placeholder(val_idx),
|
||||
column
|
||||
);
|
||||
values.push(value.clone());
|
||||
|
||||
(sql, values)
|
||||
}
|
||||
UpdateExpr::SubIf { condition, value } => {
|
||||
let (cond_sql, cond_values) = Filter::build_where_clause_with_offset(
|
||||
&[condition.clone()],
|
||||
start_idx,
|
||||
);
|
||||
let mut values = cond_values;
|
||||
let val_idx = start_idx + values.len();
|
||||
|
||||
let sql = format!(
|
||||
"CASE WHEN {} THEN {} - {} ELSE {} END",
|
||||
cond_sql,
|
||||
column,
|
||||
placeholder(val_idx),
|
||||
column
|
||||
);
|
||||
values.push(value.clone());
|
||||
|
||||
(sql, values)
|
||||
}
|
||||
UpdateExpr::Coalesce(v) => {
|
||||
(format!("COALESCE({}, {})", column, placeholder(start_idx)), vec![v.clone()])
|
||||
}
|
||||
UpdateExpr::Greatest(v) => {
|
||||
(format!("GREATEST({}, {})", column, placeholder(start_idx)), vec![v.clone()])
|
||||
}
|
||||
UpdateExpr::Least(v) => {
|
||||
(format!("LEAST({}, {})", column, placeholder(start_idx)), vec![v.clone()])
|
||||
}
|
||||
UpdateExpr::Raw { sql, values } => {
|
||||
// Replace ? placeholders with proper database placeholders
|
||||
let mut result_sql = String::new();
|
||||
let mut placeholder_count = 0;
|
||||
|
||||
for ch in sql.chars() {
|
||||
if ch == '?' {
|
||||
result_sql.push_str(&placeholder(start_idx + placeholder_count));
|
||||
placeholder_count += 1;
|
||||
} else {
|
||||
result_sql.push(ch);
|
||||
}
|
||||
}
|
||||
|
||||
(result_sql, values.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the number of bind parameters this expression will use
|
||||
pub fn param_count(&self) -> usize {
|
||||
match self {
|
||||
UpdateExpr::Set(_) => 1,
|
||||
UpdateExpr::Add(_) => 1,
|
||||
UpdateExpr::Sub(_) => 1,
|
||||
UpdateExpr::Mul(_) => 1,
|
||||
UpdateExpr::Div(_) => 1,
|
||||
UpdateExpr::Mod(_) => 1,
|
||||
UpdateExpr::Case { branches, default: _ } => {
|
||||
branches.iter().map(|(f, _)| f.param_count() + 1).sum::<usize>() + 1
|
||||
}
|
||||
UpdateExpr::AddIf { condition, value: _ } => condition.param_count() + 1,
|
||||
UpdateExpr::SubIf { condition, value: _ } => condition.param_count() + 1,
|
||||
UpdateExpr::Coalesce(_) => 1,
|
||||
UpdateExpr::Greatest(_) => 1,
|
||||
UpdateExpr::Least(_) => 1,
|
||||
UpdateExpr::Raw { sql: _, values } => values.len(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[deprecated(since = "0.1.0", note = "Please use Value instead")]
|
||||
pub type SqlValue = Value;
|
||||
|
||||
// MySQL supports unsigned integers natively
|
||||
#[cfg(feature = "mysql")]
|
||||
macro_rules! bind_value {
|
||||
($query:expr, $value: expr) => {{
|
||||
let query = match $value {
|
||||
|
|
@ -70,8 +272,32 @@ macro_rules! bind_value {
|
|||
}};
|
||||
}
|
||||
|
||||
// PostgreSQL and SQLite don't support unsigned integers - convert to signed
|
||||
#[cfg(any(feature = "postgres", feature = "sqlite"))]
|
||||
macro_rules! bind_value {
|
||||
($query:expr, $value: expr) => {{
|
||||
let query = match $value {
|
||||
Value::Int8(v) => $query.bind(v),
|
||||
Value::Uint8(v) => $query.bind(*v as i16),
|
||||
Value::Int16(v) => $query.bind(v),
|
||||
Value::Uint16(v) => $query.bind(*v as i32),
|
||||
Value::Int32(v) => $query.bind(v),
|
||||
Value::Uint32(v) => $query.bind(*v as i64),
|
||||
Value::Int64(v) => $query.bind(v),
|
||||
Value::Uint64(v) => $query.bind(*v as i64),
|
||||
Value::VecU8(v) => $query.bind(v),
|
||||
Value::String(v) => $query.bind(v),
|
||||
Value::Bool(v) => $query.bind(v),
|
||||
Value::Uuid(v) => $query.bind(v),
|
||||
Value::NaiveDate(v) => $query.bind(v),
|
||||
Value::NaiveDateTime(v) => $query.bind(v),
|
||||
};
|
||||
query
|
||||
}};
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
|
||||
pub fn bind_values<'q>(query: Query<'q, DB, Arguments>, values: &'q [Value]) -> Query<'q, DB, Arguments> {
|
||||
pub fn bind_values<'q>(query: Query<'q, DB, Arguments_<'q>>, values: &'q [Value]) -> Query<'q, DB, Arguments_<'q>> {
|
||||
let mut query = query;
|
||||
for value in values {
|
||||
query = bind_value!(query, value);
|
||||
|
|
@ -79,15 +305,48 @@ pub fn bind_values<'q>(query: Query<'q, DB, Arguments>, values: &'q [Value]) ->
|
|||
query
|
||||
}
|
||||
|
||||
/// Bind a single owned Value to a query
|
||||
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
|
||||
pub fn bind_as_values<'q, O>(query: QueryAs<'q, DB, O, Arguments>, values: &'q [Value]) -> QueryAs<'q, DB, O, Arguments> {
|
||||
pub fn bind_value_owned<'q>(query: Query<'q, DB, Arguments_<'q>>, value: Value) -> Query<'q, DB, Arguments_<'q>> {
|
||||
match value {
|
||||
Value::Int8(v) => query.bind(v),
|
||||
Value::Int16(v) => query.bind(v),
|
||||
Value::Int32(v) => query.bind(v),
|
||||
Value::Int64(v) => query.bind(v),
|
||||
#[cfg(feature = "mysql")]
|
||||
Value::Uint8(v) => query.bind(v),
|
||||
#[cfg(feature = "mysql")]
|
||||
Value::Uint16(v) => query.bind(v),
|
||||
#[cfg(feature = "mysql")]
|
||||
Value::Uint32(v) => query.bind(v),
|
||||
#[cfg(feature = "mysql")]
|
||||
Value::Uint64(v) => query.bind(v),
|
||||
#[cfg(any(feature = "postgres", feature = "sqlite"))]
|
||||
Value::Uint8(v) => query.bind(v as i16),
|
||||
#[cfg(any(feature = "postgres", feature = "sqlite"))]
|
||||
Value::Uint16(v) => query.bind(v as i32),
|
||||
#[cfg(any(feature = "postgres", feature = "sqlite"))]
|
||||
Value::Uint32(v) => query.bind(v as i64),
|
||||
#[cfg(any(feature = "postgres", feature = "sqlite"))]
|
||||
Value::Uint64(v) => query.bind(v as i64),
|
||||
Value::VecU8(v) => query.bind(v),
|
||||
Value::String(v) => query.bind(v),
|
||||
Value::Bool(v) => query.bind(v),
|
||||
Value::Uuid(v) => query.bind(v),
|
||||
Value::NaiveDate(v) => query.bind(v),
|
||||
Value::NaiveDateTime(v) => query.bind(v),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
|
||||
pub fn bind_as_values<'q, O>(query: QueryAs<'q, DB, O, Arguments_<'q>>, values: &'q [Value]) -> QueryAs<'q, DB, O, Arguments_<'q>> {
|
||||
values.into_iter().fold(query, |query, value| {
|
||||
bind_value!(query, value)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
|
||||
pub fn bind_scalar_values<'q, O>(query: QueryScalar<'q, DB, O, Arguments>, values: &'q [Value]) -> QueryScalar<'q, DB, O, Arguments> {
|
||||
pub fn bind_scalar_values<'q, O>(query: QueryScalar<'q, DB, O, Arguments_<'q>>, values: &'q [Value]) -> QueryScalar<'q, DB, O, Arguments_<'q>> {
|
||||
let mut query = query;
|
||||
for value in values {
|
||||
query = bind_value!(query, value);
|
||||
|
|
@ -101,24 +360,61 @@ pub fn query_fields(fields: Vec<&str>) -> String {
|
|||
.collect::<Vec<_>>().join(", ")
|
||||
}
|
||||
|
||||
// From implementations for owned values
|
||||
impl From<String> for Value {
|
||||
fn from(value: String) -> Self {
|
||||
Value::String(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i8> for Value {
|
||||
fn from(value: i8) -> Self {
|
||||
Value::Int8(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u8> for Value {
|
||||
fn from(value: u8) -> Self {
|
||||
Value::Uint8(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i16> for Value {
|
||||
fn from(value: i16) -> Self {
|
||||
Value::Int16(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u16> for Value {
|
||||
fn from(value: u16) -> Self {
|
||||
Value::Uint16(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i32> for Value {
|
||||
fn from(value: i32) -> Self {
|
||||
Value::Int32(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for Value {
|
||||
fn from(value: u32) -> Self {
|
||||
Value::Uint32(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i64> for Value {
|
||||
fn from(value: i64) -> Self {
|
||||
Value::Int64(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u64> for Value {
|
||||
fn from(value: u64) -> Self {
|
||||
Value::Uint64(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for Value {
|
||||
fn from(value: bool) -> Self {
|
||||
Value::Bool(value)
|
||||
|
|
@ -131,33 +427,9 @@ impl From<uuid::Uuid> for Value {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Value {
|
||||
fn from(value: &str) -> Self {
|
||||
Value::String(value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&i32> for Value {
|
||||
fn from(value: &i32) -> Self {
|
||||
Value::Int32(*value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&i64> for Value {
|
||||
fn from(value: &i64) -> Self {
|
||||
Value::Int64(*value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&bool> for Value {
|
||||
fn from(value: &bool) -> Self {
|
||||
Value::Bool(*value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&uuid::Uuid> for Value {
|
||||
fn from(value: &uuid::Uuid) -> Self {
|
||||
Value::Uuid(*value)
|
||||
impl From<Vec<u8>> for Value {
|
||||
fn from(value: Vec<u8>) -> Self {
|
||||
Value::VecU8(value)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -173,6 +445,91 @@ impl From<NaiveDateTime> for Value {
|
|||
}
|
||||
}
|
||||
|
||||
// From implementations for references
|
||||
impl From<&str> for Value {
|
||||
fn from(value: &str) -> Self {
|
||||
Value::String(value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&String> for Value {
|
||||
fn from(value: &String) -> Self {
|
||||
Value::String(value.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&i8> for Value {
|
||||
fn from(value: &i8) -> Self {
|
||||
Value::Int8(*value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&u8> for Value {
|
||||
fn from(value: &u8) -> Self {
|
||||
Value::Uint8(*value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&i16> for Value {
|
||||
fn from(value: &i16) -> Self {
|
||||
Value::Int16(*value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&u16> for Value {
|
||||
fn from(value: &u16) -> Self {
|
||||
Value::Uint16(*value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&i32> for Value {
|
||||
fn from(value: &i32) -> Self {
|
||||
Value::Int32(*value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&u32> for Value {
|
||||
fn from(value: &u32) -> Self {
|
||||
Value::Uint32(*value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&i64> for Value {
|
||||
fn from(value: &i64) -> Self {
|
||||
Value::Int64(*value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&u64> for Value {
|
||||
fn from(value: &u64) -> Self {
|
||||
Value::Uint64(*value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&bool> for Value {
|
||||
fn from(value: &bool) -> Self {
|
||||
Value::Bool(*value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&uuid::Uuid> for Value {
|
||||
fn from(value: &uuid::Uuid) -> Self {
|
||||
Value::Uuid(*value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&NaiveDate> for Value {
|
||||
fn from(value: &NaiveDate) -> Self {
|
||||
Value::NaiveDate(*value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&NaiveDateTime> for Value {
|
||||
fn from(value: &NaiveDateTime) -> Self {
|
||||
Value::NaiveDateTime(*value)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait BindValues<'q> {
|
||||
type Output;
|
||||
|
||||
|
|
@ -180,8 +537,8 @@ pub trait BindValues<'q> {
|
|||
}
|
||||
|
||||
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
|
||||
impl<'q> BindValues<'q> for Query<'q, DB, Arguments> {
|
||||
type Output = Query<'q, DB, Arguments>;
|
||||
impl<'q> BindValues<'q> for Query<'q, DB, Arguments_<'q>> {
|
||||
type Output = Query<'q, DB, Arguments_<'q>>;
|
||||
|
||||
fn bind_values(self, values: &'q [Value]) -> Self::Output {
|
||||
let mut query = self;
|
||||
|
|
@ -193,8 +550,8 @@ impl<'q> BindValues<'q> for Query<'q, DB, Arguments> {
|
|||
}
|
||||
|
||||
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
|
||||
impl<'q, O> BindValues<'q> for QueryAs<'q, DB, O, Arguments> {
|
||||
type Output = QueryAs<'q, DB, O, Arguments>;
|
||||
impl<'q, O> BindValues<'q> for QueryAs<'q, DB, O, Arguments_<'q>> {
|
||||
type Output = QueryAs<'q, DB, O, Arguments_<'q>>;
|
||||
|
||||
fn bind_values(self, values: &'q [Value]) -> Self::Output {
|
||||
values.into_iter().fold(self, |query, value| {
|
||||
|
|
@ -204,8 +561,8 @@ impl<'q, O> BindValues<'q> for QueryAs<'q, DB, O, Arguments> {
|
|||
}
|
||||
|
||||
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
|
||||
impl<'q, O> BindValues<'q> for QueryScalar<'q, DB, O, Arguments> {
|
||||
type Output = QueryScalar<'q, DB, O, Arguments>;
|
||||
impl<'q, O> BindValues<'q> for QueryScalar<'q, DB, O, Arguments_<'q>> {
|
||||
type Output = QueryScalar<'q, DB, O, Arguments_<'q>>;
|
||||
|
||||
fn bind_values(self, values: &'q [Value]) -> Self::Output {
|
||||
let mut query = self;
|
||||
|
|
|
|||
Loading…
Reference in New Issue