Sync all local changes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2ea8a63ae4
commit
acf7321c1b
|
|
@ -0,0 +1,435 @@
|
||||||
|
# sqlx-record Expert
|
||||||
|
|
||||||
|
You are an expert at using the sqlx-record Rust library for database operations. sqlx-record provides derive macros for automatic CRUD operations, audit trails, and type-safe query building on top of SQLx, supporting MySQL, PostgreSQL, and SQLite.
|
||||||
|
|
||||||
|
**PROACTIVE USE**: This agent should be consulted BEFORE writing Entity structs, filters, lookup tables, or audit trail code to avoid common mistakes and follow best practices.
|
||||||
|
|
||||||
|
## CRITICAL: Quick Reference - Avoid These Mistakes
|
||||||
|
|
||||||
|
| Mistake | Wrong | Correct |
|
||||||
|
|---------|-------|---------|
|
||||||
|
| Missing `FromRow` derive | `#[derive(Entity)]` | `#[derive(Entity, FromRow)]` |
|
||||||
|
| Wrong filter syntax | `filters![("field", ">", 5)]` | `"field".gt(5)` or `Filter::GreaterThan(...)` |
|
||||||
|
| Quoted filter values | `filters![("status", "active")]` for non-String | Use `.into()` for Value conversion |
|
||||||
|
| Forgetting database feature | `sqlx-record = "0.3"` | `sqlx-record = { version = "0.3", features = ["mysql", "derive"] }` |
|
||||||
|
| Using `delete()` expecting hard delete | `user.delete(&pool)` | `user.hard_delete(&pool)` for permanent removal |
|
||||||
|
| Wrong UpdateForm pattern | `User::update_form().name("Bob")` | `User::update_form().with_name("Bob")` |
|
||||||
|
| Lookup with spaces | `lookup_table!(Status, "in progress")` | `lookup_table!(Status, "in-progress")` (use hyphens) |
|
||||||
|
|
||||||
|
## Your Expertise
|
||||||
|
|
||||||
|
1. **Define entities** with proper attributes and field annotations
|
||||||
|
2. **Write filters** using the Filter enum and macro system
|
||||||
|
3. **Set up audit trails** with EntityChange tracking
|
||||||
|
4. **Design lookup tables** for type-safe enumerations
|
||||||
|
5. **Implement batch operations** with insert_many and upsert
|
||||||
|
6. **Use transactions** with the transaction! macro
|
||||||
|
7. **Build update expressions** for arithmetic and conditional updates
|
||||||
|
8. **Configure soft delete** and timestamp management
|
||||||
|
9. **Set up pagination** with Page<T> and PageRequest
|
||||||
|
|
||||||
|
## Entity Definition
|
||||||
|
|
||||||
|
### Struct Attributes
|
||||||
|
```rust
|
||||||
|
#[derive(Entity, FromRow, Debug, Clone)]
|
||||||
|
#[table_name = "users"] // Optional: defaults to snake_case plural
|
||||||
|
struct User {
|
||||||
|
#[primary_key] // Required: one field
|
||||||
|
id: Uuid,
|
||||||
|
|
||||||
|
#[rename("user_name")] // Map to different DB column
|
||||||
|
name: String,
|
||||||
|
|
||||||
|
#[version] // Auto-increment on update, wraps on overflow
|
||||||
|
version: u32,
|
||||||
|
|
||||||
|
#[field_type("TEXT")] // SQLx type hint for compile-time validation
|
||||||
|
bio: Option<String>,
|
||||||
|
|
||||||
|
#[soft_delete] // Enables soft_delete/restore methods
|
||||||
|
is_active: bool, // Convention: is_active auto-detected
|
||||||
|
|
||||||
|
#[created_at] // Auto-set milliseconds on insert
|
||||||
|
created_at: i64,
|
||||||
|
|
||||||
|
#[updated_at] // Auto-set milliseconds on update
|
||||||
|
updated_at: i64,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Primary Key Types
|
||||||
|
Supports: `Uuid`, `String`, `i32`, `i64`, `u32`, `u64`
|
||||||
|
|
||||||
|
### Generated Method Naming
|
||||||
|
Methods are named after the primary key field:
|
||||||
|
- Field `id` -> `get_by_id`, `update_by_id`, `hard_delete_by_id`
|
||||||
|
- Field `user_id` -> `get_by_user_id`, `update_by_user_id`
|
||||||
|
- Plural: `get_by_ids`, `update_by_ids`
|
||||||
|
|
||||||
|
## 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?; // Option<User>
|
||||||
|
let users = User::get_by_ids(&pool, &ids).await?; // Vec<User>
|
||||||
|
|
||||||
|
// 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: (field, is_ascending)
|
||||||
|
let users = User::find_ordered(&pool, filters![], None, vec![("created_at", false)]).await?;
|
||||||
|
|
||||||
|
// Find with limit
|
||||||
|
let users = User::find_ordered_with_limit(
|
||||||
|
&pool, filters![], None, vec![("name", true)], Some((0, 10))
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
// Count
|
||||||
|
let n = User::count(&pool, filters![("is_active", true)], None).await?;
|
||||||
|
|
||||||
|
// Update (only set fields are updated)
|
||||||
|
User::update_by_id(&pool, &id, User::update_form().with_name("Bob")).await?;
|
||||||
|
|
||||||
|
// Hard delete (permanent)
|
||||||
|
user.hard_delete(&pool).await?;
|
||||||
|
User::hard_delete_by_id(&pool, &id).await?;
|
||||||
|
|
||||||
|
// Soft delete (sets is_active = false)
|
||||||
|
user.soft_delete(&pool).await?;
|
||||||
|
user.restore(&pool).await?;
|
||||||
|
|
||||||
|
// Batch insert
|
||||||
|
User::insert_many(&pool, &users).await?;
|
||||||
|
|
||||||
|
// Upsert (insert or update on PK conflict)
|
||||||
|
user.upsert(&pool).await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Filter System
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use sqlx_record::prelude::*;
|
||||||
|
|
||||||
|
// Simple equality (AND)
|
||||||
|
filters![("active", true), ("role", "admin")]
|
||||||
|
|
||||||
|
// OR conditions
|
||||||
|
filter_or![("status", "active"), ("status", "pending")]
|
||||||
|
|
||||||
|
// Operator methods via FilterOps trait
|
||||||
|
"age".gt(18) // GreaterThan
|
||||||
|
"age".ge(18) // GreaterThanOrEqual
|
||||||
|
"age".lt(65) // LessThan
|
||||||
|
"age".le(65) // LessThanOrEqual
|
||||||
|
"name".eq("Bob") // Equal
|
||||||
|
"name".ne("Bob") // NotEqual
|
||||||
|
|
||||||
|
// Pattern matching
|
||||||
|
Filter::Like("name", "%alice%".into())
|
||||||
|
Filter::ILike("name", "%alice%".into()) // Case-insensitive
|
||||||
|
|
||||||
|
// Set operations
|
||||||
|
Filter::In("status", vec!["active".into(), "pending".into()])
|
||||||
|
Filter::NotIn("role", vec!["banned".into()])
|
||||||
|
|
||||||
|
// Null checks
|
||||||
|
Filter::IsNull("deleted_at")
|
||||||
|
Filter::IsNotNull("email")
|
||||||
|
|
||||||
|
// Composition
|
||||||
|
Filter::And(vec![...])
|
||||||
|
Filter::Or(vec![...])
|
||||||
|
|
||||||
|
// MySQL index hints
|
||||||
|
User::find(&pool, filters, Some("idx_users_email")).await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
## UpdateExpr - Advanced Updates
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use sqlx_record::prelude::UpdateExpr;
|
||||||
|
|
||||||
|
// Arithmetic: column = column OP value
|
||||||
|
User::update_form().eval_score(UpdateExpr::Add(10.into())) // score + 10
|
||||||
|
User::update_form().eval_score(UpdateExpr::Sub(5.into())) // score - 5
|
||||||
|
User::update_form().eval_score(UpdateExpr::Mul(2.into())) // score * 2
|
||||||
|
User::update_form().eval_score(UpdateExpr::Div(2.into())) // score / 2
|
||||||
|
|
||||||
|
// Conditional: CASE/WHEN
|
||||||
|
User::update_form().eval_tier(UpdateExpr::Case {
|
||||||
|
branches: vec![
|
||||||
|
("score".gt(100), "gold".into()),
|
||||||
|
("score".gt(50), "silver".into()),
|
||||||
|
],
|
||||||
|
default: "bronze".into(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Conditional increment: only if condition met
|
||||||
|
User::update_form().eval_balance(UpdateExpr::AddIf {
|
||||||
|
condition: "is_premium".eq(true),
|
||||||
|
value: 100.into(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Utility operations
|
||||||
|
UpdateExpr::Coalesce(value) // COALESCE(column, ?)
|
||||||
|
UpdateExpr::Greatest(value) // GREATEST(column, ?)
|
||||||
|
UpdateExpr::Least(value) // LEAST(column, ?)
|
||||||
|
|
||||||
|
// Raw SQL escape hatch
|
||||||
|
User::update_form()
|
||||||
|
.raw("computed", "COALESCE(a, 0) + COALESCE(b, 0)")
|
||||||
|
.raw_with_values("adjusted", "value * ? + ?", values![1.5, 10])
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lookup Tables
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// With database entity (creates struct + enum + constants)
|
||||||
|
lookup_table!(OrderStatus, "pending", "shipped", "delivered");
|
||||||
|
// Generated: struct OrderStatus, enum OrderStatusCode, OrderStatus::PENDING, etc.
|
||||||
|
|
||||||
|
// Without database entity (enum + constants only)
|
||||||
|
lookup_options!(PaymentMethod, "credit-card", "paypal", "bank-transfer");
|
||||||
|
// Generated: enum PaymentMethodCode, PaymentMethod::CREDIT_CARD, etc.
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
let status = OrderStatus::PENDING; // &str constant
|
||||||
|
let code = OrderStatusCode::try_from("pending")?; // Enum variant
|
||||||
|
println!("{}", code.as_str()); // Back to string
|
||||||
|
```
|
||||||
|
|
||||||
|
## Audit Trail (EntityChange)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use sqlx_record::prelude::*;
|
||||||
|
|
||||||
|
// Record a change
|
||||||
|
let change = EntityChange {
|
||||||
|
id: new_uuid(),
|
||||||
|
entity_id: user.id,
|
||||||
|
action: Action::Update,
|
||||||
|
changed_at: now_millis(),
|
||||||
|
actor_id: Some(current_user.id),
|
||||||
|
session_id: Some(session.id),
|
||||||
|
change_set_id: Some(batch_id),
|
||||||
|
new_value: User::model_diff(&form, &user), // JSON diff
|
||||||
|
};
|
||||||
|
change.insert(&pool, "entity_changes_users").await?;
|
||||||
|
|
||||||
|
// Diff methods
|
||||||
|
User::model_diff(&form, &existing) // Compare form with model
|
||||||
|
User::db_diff(&form, &id, &pool).await? // Compare form with DB
|
||||||
|
User::diff_modify(&mut form, &existing) // Modify form to only include changes
|
||||||
|
user.to_update_form() // Convert entity to UpdateForm
|
||||||
|
user.initial_diff() // Full entity as JSON
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pagination
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use sqlx_record::prelude::{Page, PageRequest};
|
||||||
|
|
||||||
|
let page_req = PageRequest::new(1, 20); // page 1, 20 items per page
|
||||||
|
let page: Page<User> = User::paginate(
|
||||||
|
&pool, filters![], None, vec![("name", true)], page_req
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
page.items // Vec<User>
|
||||||
|
page.total_count // u64
|
||||||
|
page.page // u32 (1-indexed)
|
||||||
|
page.page_size // u32
|
||||||
|
page.total_pages() // u32
|
||||||
|
page.has_next() // bool
|
||||||
|
page.has_prev() // bool
|
||||||
|
page.is_empty() // bool
|
||||||
|
page.len() // usize
|
||||||
|
```
|
||||||
|
|
||||||
|
## Transactions
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use sqlx_record::transaction;
|
||||||
|
|
||||||
|
// Automatically commits on success, rolls back on error
|
||||||
|
let order_id = transaction!(&pool, |tx| {
|
||||||
|
user.insert(&mut *tx).await?;
|
||||||
|
order.insert(&mut *tx).await?;
|
||||||
|
Ok::<_, sqlx::Error>(order.id)
|
||||||
|
}).await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
## ConnProvider - Flexible Connection Management
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use sqlx_record::prelude::ConnProvider;
|
||||||
|
|
||||||
|
// From borrowed connection
|
||||||
|
let mut conn = pool.acquire().await?;
|
||||||
|
let mut provider = ConnProvider::from_ref(&mut conn);
|
||||||
|
|
||||||
|
// From pool (lazy acquisition on first use)
|
||||||
|
let mut provider = ConnProvider::from_pool(pool.clone());
|
||||||
|
|
||||||
|
// From transaction
|
||||||
|
let mut tx = pool.begin().await?;
|
||||||
|
let mut provider = ConnProvider::from_tx(&mut tx);
|
||||||
|
// ... operations participate in the transaction ...
|
||||||
|
tx.commit().await?;
|
||||||
|
|
||||||
|
// Get underlying connection
|
||||||
|
let conn = provider.get_conn().await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database Differences
|
||||||
|
|
||||||
|
| Feature | MySQL | PostgreSQL | SQLite |
|
||||||
|
|---------|-------|------------|--------|
|
||||||
|
| Placeholder | `?` | `$1, $2` | `?` |
|
||||||
|
| Table quote | `` ` `` | `"` | `"` |
|
||||||
|
| UUID type | `BINARY(16)` | `UUID` | `BLOB` |
|
||||||
|
| 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
|
||||||
|
|
||||||
|
The `Value` enum wraps all supported database types:
|
||||||
|
- Integers: `Int8`, `Uint8`, `Int16`, `Uint16`, `Int32`, `Uint32`, `Int64`, `Uint64`
|
||||||
|
- `String`, `Bool`, `VecU8`, `Uuid`
|
||||||
|
- `NaiveDate`, `NaiveDateTime`
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Implicit conversion via Into<Value>
|
||||||
|
let v: Value = "hello".into(); // String
|
||||||
|
let v: Value = 42i64.into(); // Int64
|
||||||
|
let v: Value = true.into(); // Bool
|
||||||
|
let v: Value = uuid.into(); // Uuid
|
||||||
|
|
||||||
|
// values! macro for collections
|
||||||
|
let vals = values![1, "hello", true];
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Repository with Audit Trail
|
||||||
|
```rust
|
||||||
|
pub async fn update_user(
|
||||||
|
pool: &Pool,
|
||||||
|
user_id: &Uuid,
|
||||||
|
form: UserUpdateForm,
|
||||||
|
actor_id: &Uuid,
|
||||||
|
session_id: &Uuid,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let diff = User::db_diff(&form, user_id, pool).await?;
|
||||||
|
User::update_by_id(pool, user_id, form).await?;
|
||||||
|
|
||||||
|
let change = EntityChange {
|
||||||
|
id: new_uuid(),
|
||||||
|
entity_id: *user_id,
|
||||||
|
action: Action::Update,
|
||||||
|
changed_at: now_millis(),
|
||||||
|
actor_id: Some(*actor_id),
|
||||||
|
session_id: Some(*session_id),
|
||||||
|
change_set_id: None,
|
||||||
|
new_value: diff,
|
||||||
|
};
|
||||||
|
change.insert(pool, "entity_changes_users").await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Filtered Search with Pagination
|
||||||
|
```rust
|
||||||
|
pub async fn search_users(
|
||||||
|
pool: &Pool,
|
||||||
|
query: Option<&str>,
|
||||||
|
role: Option<&str>,
|
||||||
|
page: u32,
|
||||||
|
page_size: u32,
|
||||||
|
) -> Result<Page<User>, Error> {
|
||||||
|
let mut filters = vec![Filter::Equal("is_active", true.into())];
|
||||||
|
|
||||||
|
if let Some(q) = query {
|
||||||
|
filters.push(Filter::ILike("name", format!("%{q}%").into()));
|
||||||
|
}
|
||||||
|
if let Some(r) = role {
|
||||||
|
filters.push(Filter::Equal("role", r.into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
User::paginate(
|
||||||
|
pool, filters, None,
|
||||||
|
vec![("name", true)],
|
||||||
|
PageRequest::new(page, page_size),
|
||||||
|
).await
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lookup-Driven Status Workflow
|
||||||
|
```rust
|
||||||
|
lookup_table!(OrderStatus, "pending", "processing", "shipped", "delivered", "cancelled");
|
||||||
|
|
||||||
|
pub async fn transition_order(
|
||||||
|
pool: &Pool,
|
||||||
|
order_id: &Uuid,
|
||||||
|
new_status: OrderStatusCode,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let order = Order::get_by_id(pool, order_id).await?.unwrap();
|
||||||
|
let current = OrderStatusCode::try_from(order.status.as_str())?;
|
||||||
|
|
||||||
|
// Validate transition
|
||||||
|
let valid = matches!(
|
||||||
|
(current, new_status),
|
||||||
|
(OrderStatusCode::Pending, OrderStatusCode::Processing)
|
||||||
|
| (OrderStatusCode::Processing, OrderStatusCode::Shipped)
|
||||||
|
| (OrderStatusCode::Shipped, OrderStatusCode::Delivered)
|
||||||
|
| (_, OrderStatusCode::Cancelled)
|
||||||
|
);
|
||||||
|
|
||||||
|
if !valid {
|
||||||
|
return Err(Error::Protocol("Invalid status transition".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Order::update_by_id(pool, order_id,
|
||||||
|
Order::update_form().with_status(new_status.as_str().to_string())
|
||||||
|
).await
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Feature Flags Reference
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
sqlx-record = { version = "0.3", features = ["mysql", "derive"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `mysql` | MySQL/MariaDB/TiDB support |
|
||||||
|
| `postgres` | PostgreSQL support |
|
||||||
|
| `sqlite` | SQLite support |
|
||||||
|
| `derive` | `#[derive(Entity, Update)]` macros |
|
||||||
|
| `static-validation` | Compile-time SQLx query validation |
|
||||||
|
|
||||||
|
**Must enable at least one database feature.**
|
||||||
|
|
||||||
|
## CLI Tool (sqlx-record-ctl)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate audit table for an entity
|
||||||
|
sqlx-record-ctl generate-audit-table users
|
||||||
|
|
||||||
|
# List auditable entities
|
||||||
|
sqlx-record-ctl list-entities
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires `entity_changes_metadata` table with `table_name` and `is_auditable` columns.
|
||||||
|
|
@ -5,7 +5,10 @@ use std::io::{self, BufRead, Write};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "sqlx-record-mcp")]
|
#[command(name = "sqlx-record-mcp")]
|
||||||
#[command(version, about = "MCP server for sqlx-record documentation and code generation")]
|
#[command(
|
||||||
|
version,
|
||||||
|
about = "MCP server for sqlx-record documentation and code generation"
|
||||||
|
)]
|
||||||
struct Args {}
|
struct Args {}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
@ -1581,9 +1584,16 @@ async fn get_user_history(pool: &Pool, user_id: &Uuid) -> Result<Vec<EntityChang
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
fn generate_entity_code(params: &Value) -> String {
|
fn generate_entity_code(params: &Value) -> String {
|
||||||
let name = params.get("name").and_then(|v| v.as_str()).unwrap_or("Entity");
|
let name = params
|
||||||
let table = params.get("table").and_then(|v| v.as_str()).unwrap_or("entities");
|
.get("name")
|
||||||
let fields: Vec<(&str, &str)> = params.get("fields")
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("Entity");
|
||||||
|
let table = params
|
||||||
|
.get("table")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("entities");
|
||||||
|
let fields: Vec<(&str, &str)> = params
|
||||||
|
.get("fields")
|
||||||
.and_then(|v| v.as_array())
|
.and_then(|v| v.as_array())
|
||||||
.map(|arr| {
|
.map(|arr| {
|
||||||
arr.iter()
|
arr.iter()
|
||||||
|
|
@ -1595,7 +1605,10 @@ fn generate_entity_code(params: &Value) -> String {
|
||||||
.collect()
|
.collect()
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let has_version = params.get("version").and_then(|v| v.as_bool()).unwrap_or(false);
|
let has_version = params
|
||||||
|
.get("version")
|
||||||
|
.and_then(|v| v.as_bool())
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
let mut code = format!(
|
let mut code = format!(
|
||||||
r#"use sqlx_record::prelude::*;
|
r#"use sqlx_record::prelude::*;
|
||||||
|
|
@ -1624,7 +1637,8 @@ pub struct {} {{
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_filter_code(params: &Value) -> String {
|
fn generate_filter_code(params: &Value) -> String {
|
||||||
let conditions: Vec<String> = params.get("conditions")
|
let conditions: Vec<String> = params
|
||||||
|
.get("conditions")
|
||||||
.and_then(|v| v.as_array())
|
.and_then(|v| v.as_array())
|
||||||
.map(|arr| {
|
.map(|arr| {
|
||||||
arr.iter()
|
arr.iter()
|
||||||
|
|
@ -1652,7 +1666,10 @@ fn generate_filter_code(params: &Value) -> String {
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let logic = params.get("logic").and_then(|v| v.as_str()).unwrap_or("and");
|
let logic = params
|
||||||
|
.get("logic")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("and");
|
||||||
|
|
||||||
if conditions.is_empty() {
|
if conditions.is_empty() {
|
||||||
return "filters![]".to_string();
|
return "filters![]".to_string();
|
||||||
|
|
@ -1665,14 +1682,22 @@ fn generate_filter_code(params: &Value) -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_lookup_code(params: &Value) -> String {
|
fn generate_lookup_code(params: &Value) -> String {
|
||||||
let name = params.get("name").and_then(|v| v.as_str()).unwrap_or("Status");
|
let name = params
|
||||||
let codes: Vec<&str> = params.get("codes")
|
.get("name")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("Status");
|
||||||
|
let codes: Vec<&str> = params
|
||||||
|
.get("codes")
|
||||||
.and_then(|v| v.as_array())
|
.and_then(|v| v.as_array())
|
||||||
.map(|arr| arr.iter().filter_map(|c| c.as_str()).collect())
|
.map(|arr| arr.iter().filter_map(|c| c.as_str()).collect())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let with_entity = params.get("with_entity").and_then(|v| v.as_bool()).unwrap_or(true);
|
let with_entity = params
|
||||||
|
.get("with_entity")
|
||||||
|
.and_then(|v| v.as_bool())
|
||||||
|
.unwrap_or(true);
|
||||||
|
|
||||||
let codes_str = codes.iter()
|
let codes_str = codes
|
||||||
|
.iter()
|
||||||
.map(|c| format!("\"{}\"", c))
|
.map(|c| format!("\"{}\"", c))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(", ");
|
.join(", ");
|
||||||
|
|
@ -1815,7 +1840,10 @@ fn handle_call_tool(params: &Value) -> Value {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
"explain_feature" => {
|
"explain_feature" => {
|
||||||
let feature = arguments.get("feature").and_then(|v| v.as_str()).unwrap_or("overview");
|
let feature = arguments
|
||||||
|
.get("feature")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("overview");
|
||||||
let doc = match feature {
|
let doc = match feature {
|
||||||
"overview" => OVERVIEW,
|
"overview" => OVERVIEW,
|
||||||
"derive" => DERIVE_ENTITY,
|
"derive" => DERIVE_ENTITY,
|
||||||
|
|
|
||||||
|
|
@ -63,9 +63,8 @@ async fn main() -> Result<(), sqlx::Error> {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Find all tables marked as auditable in the metadata table
|
// Find all tables marked as auditable in the metadata table
|
||||||
let tables: Vec<String> = sqlx::query(
|
let tables: Vec<String> =
|
||||||
"SELECT table_name FROM entity_changes_metadata WHERE is_auditable = TRUE"
|
sqlx::query("SELECT table_name FROM entity_changes_metadata WHERE is_auditable = TRUE")
|
||||||
)
|
|
||||||
.fetch_all(&pool)
|
.fetch_all(&pool)
|
||||||
.await?
|
.await?
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -81,10 +80,16 @@ async fn main() -> Result<(), sqlx::Error> {
|
||||||
println!("delete table: {}", entity_changes_table);
|
println!("delete table: {}", entity_changes_table);
|
||||||
|
|
||||||
#[cfg(feature = "mysql")]
|
#[cfg(feature = "mysql")]
|
||||||
let drop_stmt = format!("DROP TABLE IF EXISTS {}.{}", schema_name, entity_changes_table);
|
let drop_stmt = format!(
|
||||||
|
"DROP TABLE IF EXISTS {}.{}",
|
||||||
|
schema_name, entity_changes_table
|
||||||
|
);
|
||||||
|
|
||||||
#[cfg(feature = "postgres")]
|
#[cfg(feature = "postgres")]
|
||||||
let drop_stmt = format!("DROP TABLE IF EXISTS \"{}\".\"{}\"", schema_name, entity_changes_table);
|
let drop_stmt = format!(
|
||||||
|
"DROP TABLE IF EXISTS \"{}\".\"{}\"",
|
||||||
|
schema_name, entity_changes_table
|
||||||
|
);
|
||||||
|
|
||||||
#[cfg(feature = "sqlite")]
|
#[cfg(feature = "sqlite")]
|
||||||
let drop_stmt = format!("DROP TABLE IF EXISTS \"{}\"", entity_changes_table);
|
let drop_stmt = format!("DROP TABLE IF EXISTS \"{}\"", entity_changes_table);
|
||||||
|
|
@ -108,28 +113,38 @@ async fn main() -> Result<(), sqlx::Error> {
|
||||||
new_value JSON
|
new_value JSON
|
||||||
);",
|
);",
|
||||||
schema_name, entity_changes_table,
|
schema_name, entity_changes_table,
|
||||||
)).execute(&pool).await?;
|
))
|
||||||
|
.execute(&pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
// Create indexes
|
// Create indexes
|
||||||
sqlx::query(&format!(
|
sqlx::query(&format!(
|
||||||
"CREATE INDEX IF NOT EXISTS idx_{}_entity_id ON {}.{} (entity_id);",
|
"CREATE INDEX IF NOT EXISTS idx_{}_entity_id ON {}.{} (entity_id);",
|
||||||
entity_changes_table, schema_name, entity_changes_table,
|
entity_changes_table, schema_name, entity_changes_table,
|
||||||
)).execute(&pool).await?;
|
))
|
||||||
|
.execute(&pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
sqlx::query(&format!(
|
sqlx::query(&format!(
|
||||||
"CREATE INDEX IF NOT EXISTS idx_{}_change_set_id ON {}.{} (change_set_id);",
|
"CREATE INDEX IF NOT EXISTS idx_{}_change_set_id ON {}.{} (change_set_id);",
|
||||||
entity_changes_table, schema_name, entity_changes_table,
|
entity_changes_table, schema_name, entity_changes_table,
|
||||||
)).execute(&pool).await?;
|
))
|
||||||
|
.execute(&pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
sqlx::query(&format!(
|
sqlx::query(&format!(
|
||||||
"CREATE INDEX IF NOT EXISTS idx_{}_session_id ON {}.{} (session_id);",
|
"CREATE INDEX IF NOT EXISTS idx_{}_session_id ON {}.{} (session_id);",
|
||||||
entity_changes_table, schema_name, entity_changes_table,
|
entity_changes_table, schema_name, entity_changes_table,
|
||||||
)).execute(&pool).await?;
|
))
|
||||||
|
.execute(&pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
sqlx::query(&format!(
|
sqlx::query(&format!(
|
||||||
"CREATE INDEX IF NOT EXISTS idx_{}_actor_id ON {}.{} (actor_id);",
|
"CREATE INDEX IF NOT EXISTS idx_{}_actor_id ON {}.{} (actor_id);",
|
||||||
entity_changes_table, schema_name, entity_changes_table,
|
entity_changes_table, schema_name, entity_changes_table,
|
||||||
)).execute(&pool).await?;
|
))
|
||||||
|
.execute(&pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
sqlx::query(&format!(
|
sqlx::query(&format!(
|
||||||
"CREATE INDEX IF NOT EXISTS idx_{}_entity_id_actor_id ON {}.{} (entity_id, actor_id);",
|
"CREATE INDEX IF NOT EXISTS idx_{}_entity_id_actor_id ON {}.{} (entity_id, actor_id);",
|
||||||
|
|
@ -157,7 +172,9 @@ async fn main() -> Result<(), sqlx::Error> {
|
||||||
sqlx::query(&format!(
|
sqlx::query(&format!(
|
||||||
r#"CREATE INDEX IF NOT EXISTS idx_{}_entity_id ON "{}"."{}" (entity_id);"#,
|
r#"CREATE INDEX IF NOT EXISTS idx_{}_entity_id ON "{}"."{}" (entity_id);"#,
|
||||||
entity_changes_table, schema_name, entity_changes_table,
|
entity_changes_table, schema_name, entity_changes_table,
|
||||||
)).execute(&pool).await?;
|
))
|
||||||
|
.execute(&pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
sqlx::query(&format!(
|
sqlx::query(&format!(
|
||||||
r#"CREATE INDEX IF NOT EXISTS idx_{}_change_set_id ON "{}"."{}" (change_set_id);"#,
|
r#"CREATE INDEX IF NOT EXISTS idx_{}_change_set_id ON "{}"."{}" (change_set_id);"#,
|
||||||
|
|
@ -167,12 +184,16 @@ async fn main() -> Result<(), sqlx::Error> {
|
||||||
sqlx::query(&format!(
|
sqlx::query(&format!(
|
||||||
r#"CREATE INDEX IF NOT EXISTS idx_{}_session_id ON "{}"."{}" (session_id);"#,
|
r#"CREATE INDEX IF NOT EXISTS idx_{}_session_id ON "{}"."{}" (session_id);"#,
|
||||||
entity_changes_table, schema_name, entity_changes_table,
|
entity_changes_table, schema_name, entity_changes_table,
|
||||||
)).execute(&pool).await?;
|
))
|
||||||
|
.execute(&pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
sqlx::query(&format!(
|
sqlx::query(&format!(
|
||||||
r#"CREATE INDEX IF NOT EXISTS idx_{}_actor_id ON "{}"."{}" (actor_id);"#,
|
r#"CREATE INDEX IF NOT EXISTS idx_{}_actor_id ON "{}"."{}" (actor_id);"#,
|
||||||
entity_changes_table, schema_name, entity_changes_table,
|
entity_changes_table, schema_name, entity_changes_table,
|
||||||
)).execute(&pool).await?;
|
))
|
||||||
|
.execute(&pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
sqlx::query(&format!(
|
sqlx::query(&format!(
|
||||||
r#"CREATE INDEX IF NOT EXISTS idx_{}_entity_id_actor_id ON "{}"."{}" (entity_id, actor_id);"#,
|
r#"CREATE INDEX IF NOT EXISTS idx_{}_entity_id_actor_id ON "{}"."{}" (entity_id, actor_id);"#,
|
||||||
|
|
@ -200,22 +221,30 @@ async fn main() -> Result<(), sqlx::Error> {
|
||||||
sqlx::query(&format!(
|
sqlx::query(&format!(
|
||||||
r#"CREATE INDEX IF NOT EXISTS idx_{}_entity_id ON "{}" (entity_id);"#,
|
r#"CREATE INDEX IF NOT EXISTS idx_{}_entity_id ON "{}" (entity_id);"#,
|
||||||
entity_changes_table, entity_changes_table,
|
entity_changes_table, entity_changes_table,
|
||||||
)).execute(&pool).await?;
|
))
|
||||||
|
.execute(&pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
sqlx::query(&format!(
|
sqlx::query(&format!(
|
||||||
r#"CREATE INDEX IF NOT EXISTS idx_{}_change_set_id ON "{}" (change_set_id);"#,
|
r#"CREATE INDEX IF NOT EXISTS idx_{}_change_set_id ON "{}" (change_set_id);"#,
|
||||||
entity_changes_table, entity_changes_table,
|
entity_changes_table, entity_changes_table,
|
||||||
)).execute(&pool).await?;
|
))
|
||||||
|
.execute(&pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
sqlx::query(&format!(
|
sqlx::query(&format!(
|
||||||
r#"CREATE INDEX IF NOT EXISTS idx_{}_session_id ON "{}" (session_id);"#,
|
r#"CREATE INDEX IF NOT EXISTS idx_{}_session_id ON "{}" (session_id);"#,
|
||||||
entity_changes_table, entity_changes_table,
|
entity_changes_table, entity_changes_table,
|
||||||
)).execute(&pool).await?;
|
))
|
||||||
|
.execute(&pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
sqlx::query(&format!(
|
sqlx::query(&format!(
|
||||||
r#"CREATE INDEX IF NOT EXISTS idx_{}_actor_id ON "{}" (actor_id);"#,
|
r#"CREATE INDEX IF NOT EXISTS idx_{}_actor_id ON "{}" (actor_id);"#,
|
||||||
entity_changes_table, entity_changes_table,
|
entity_changes_table, entity_changes_table,
|
||||||
)).execute(&pool).await?;
|
))
|
||||||
|
.execute(&pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
sqlx::query(&format!(
|
sqlx::query(&format!(
|
||||||
r#"CREATE INDEX IF NOT EXISTS idx_{}_entity_id_actor_id ON "{}" (entity_id, actor_id);"#,
|
r#"CREATE INDEX IF NOT EXISTS idx_{}_entity_id_actor_id ON "{}" (entity_id, actor_id);"#,
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,13 @@ extern crate proc_macro;
|
||||||
|
|
||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use proc_macro2::{Ident, TokenStream as TokenStream2};
|
use proc_macro2::{Ident, TokenStream as TokenStream2};
|
||||||
use quote::{quote, format_ident};
|
use quote::{format_ident, quote};
|
||||||
use syn::{parse_macro_input, DeriveInput, Data, LitStr, Type, ImplGenerics, TypeGenerics, WhereClause};
|
use syn::{
|
||||||
|
parse_macro_input, Data, DeriveInput, ImplGenerics, LitStr, Type, TypeGenerics, WhereClause,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::string_utils::{pluralize, to_snake_case};
|
use crate::string_utils::{pluralize, to_snake_case};
|
||||||
|
|
||||||
|
|
||||||
struct EntityField {
|
struct EntityField {
|
||||||
ident: Ident,
|
ident: Ident,
|
||||||
db_name: String,
|
db_name: String,
|
||||||
|
|
@ -33,7 +34,11 @@ fn parse_string_attr(attr: &syn::Attribute) -> Option<String> {
|
||||||
}
|
}
|
||||||
syn::Meta::NameValue(nv) => {
|
syn::Meta::NameValue(nv) => {
|
||||||
// #[attr = "value"] style
|
// #[attr = "value"] style
|
||||||
if let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(lit), .. }) = &nv.value {
|
if let syn::Expr::Lit(syn::ExprLit {
|
||||||
|
lit: syn::Lit::Str(lit),
|
||||||
|
..
|
||||||
|
}) = &nv.value
|
||||||
|
{
|
||||||
Some(lit.value())
|
Some(lit.value())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|
@ -49,7 +54,19 @@ pub fn derive_update(input: TokenStream) -> TokenStream {
|
||||||
derive_entity_internal(input)
|
derive_entity_internal(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[proc_macro_derive(Entity, attributes(rename, table_name, primary_key, version, field_type, soft_delete, created_at, updated_at))]
|
#[proc_macro_derive(
|
||||||
|
Entity,
|
||||||
|
attributes(
|
||||||
|
rename,
|
||||||
|
table_name,
|
||||||
|
primary_key,
|
||||||
|
version,
|
||||||
|
field_type,
|
||||||
|
soft_delete,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
)
|
||||||
|
)]
|
||||||
pub fn derive_entity(input: TokenStream) -> TokenStream {
|
pub fn derive_entity(input: TokenStream) -> TokenStream {
|
||||||
derive_entity_internal(input)
|
derive_entity_internal(input)
|
||||||
}
|
}
|
||||||
|
|
@ -97,21 +114,34 @@ fn db_arguments() -> TokenStream2 {
|
||||||
/// Get table quote character
|
/// Get table quote character
|
||||||
fn table_quote() -> &'static str {
|
fn table_quote() -> &'static str {
|
||||||
#[cfg(feature = "postgres")]
|
#[cfg(feature = "postgres")]
|
||||||
{ "\"" }
|
{
|
||||||
|
"\""
|
||||||
|
}
|
||||||
#[cfg(feature = "sqlite")]
|
#[cfg(feature = "sqlite")]
|
||||||
{ return "\""; }
|
{
|
||||||
|
return "\"";
|
||||||
|
}
|
||||||
#[cfg(feature = "mysql")]
|
#[cfg(feature = "mysql")]
|
||||||
{ "`" }
|
{
|
||||||
|
"`"
|
||||||
|
}
|
||||||
#[cfg(not(any(feature = "mysql", feature = "postgres", feature = "sqlite")))]
|
#[cfg(not(any(feature = "mysql", feature = "postgres", feature = "sqlite")))]
|
||||||
{ "`" }
|
{
|
||||||
|
"`"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get compile-time placeholder for static-check SQL
|
/// Get compile-time placeholder for static-check SQL
|
||||||
fn static_placeholder(index: usize) -> String {
|
fn static_placeholder(index: usize) -> String {
|
||||||
#[cfg(feature = "postgres")]
|
#[cfg(feature = "postgres")]
|
||||||
{ format!("${}", index) }
|
{
|
||||||
|
format!("${}", index)
|
||||||
|
}
|
||||||
#[cfg(not(feature = "postgres"))]
|
#[cfg(not(feature = "postgres"))]
|
||||||
{ let _ = index; "?".to_string() }
|
{
|
||||||
|
let _ = index;
|
||||||
|
"?".to_string()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn derive_entity_internal(input: TokenStream) -> TokenStream {
|
fn derive_entity_internal(input: TokenStream) -> TokenStream {
|
||||||
|
|
@ -129,31 +159,91 @@ fn derive_entity_internal(input: TokenStream) -> TokenStream {
|
||||||
.expect("Struct must have a primary key field, either explicitly specified or named 'id' or 'code'");
|
.expect("Struct must have a primary key field, either explicitly specified or named 'id' or 'code'");
|
||||||
|
|
||||||
// Check for timestamp fields - either by attribute or by name
|
// Check for timestamp fields - either by attribute or by name
|
||||||
let has_created_at = fields.iter().any(|f| f.is_created_at) ||
|
let has_created_at = fields.iter().any(|f| f.is_created_at)
|
||||||
fields.iter().any(|f| f.ident == "created_at" && matches!(&f.ty, Type::Path(p) if p.path.is_ident("i64")));
|
|| fields.iter().any(|f| {
|
||||||
let has_updated_at = fields.iter().any(|f| f.is_updated_at) ||
|
f.ident == "created_at" && matches!(&f.ty, Type::Path(p) if p.path.is_ident("i64"))
|
||||||
fields.iter().any(|f| f.ident == "updated_at" && matches!(&f.ty, Type::Path(p) if p.path.is_ident("i64")));
|
});
|
||||||
|
let has_updated_at = fields.iter().any(|f| f.is_updated_at)
|
||||||
|
|| fields.iter().any(|f| {
|
||||||
|
f.ident == "updated_at" && matches!(&f.ty, Type::Path(p) if p.path.is_ident("i64"))
|
||||||
|
});
|
||||||
|
|
||||||
let version_field = fields.iter()
|
let version_field = fields
|
||||||
|
.iter()
|
||||||
.find(|f| f.is_version_field)
|
.find(|f| f.is_version_field)
|
||||||
.or_else(|| fields.iter().find(|&f| is_version_field(f)));
|
.or_else(|| fields.iter().find(|&f| is_version_field(f)));
|
||||||
|
|
||||||
// Find soft delete field (by attribute or by name convention)
|
// Find soft delete field (by attribute or by name convention)
|
||||||
// Convention: `is_active` (FALSE = deleted), `is_deleted`/`deleted` (TRUE = deleted)
|
// Convention: `is_active` (FALSE = deleted), `is_deleted`/`deleted` (TRUE = deleted)
|
||||||
let soft_delete_field = fields.iter()
|
let soft_delete_field = fields.iter().find(|f| f.is_soft_delete).or_else(|| {
|
||||||
.find(|f| f.is_soft_delete)
|
fields.iter().find(|f| {
|
||||||
.or_else(|| fields.iter().find(|f| {
|
(f.ident == "is_active" || f.ident == "is_deleted" || f.ident == "deleted")
|
||||||
(f.ident == "is_active" || f.ident == "is_deleted" || f.ident == "deleted") &&
|
&& matches!(&f.ty, Type::Path(p) if p.path.is_ident("bool"))
|
||||||
matches!(&f.ty, Type::Path(p) if p.path.is_ident("bool"))
|
})
|
||||||
}));
|
});
|
||||||
|
|
||||||
// Generate all implementations
|
// Generate all implementations
|
||||||
let insert_impl = generate_insert_impl(&name, &table_name, primary_key, &fields, has_created_at, has_updated_at, &impl_generics, &ty_generics, &where_clause);
|
let insert_impl = generate_insert_impl(
|
||||||
let get_impl = generate_get_impl(&name, &table_name, primary_key, version_field, soft_delete_field, &fields, &impl_generics, &ty_generics, &where_clause);
|
&name,
|
||||||
let update_impl = generate_update_impl(&name, &update_form_name, &table_name, &fields, primary_key, version_field, has_updated_at, &impl_generics, &ty_generics, &where_clause);
|
&table_name,
|
||||||
let diff_impl = generate_diff_impl(&name, &update_form_name, &fields, primary_key, version_field, &impl_generics, &ty_generics, &where_clause);
|
primary_key,
|
||||||
let delete_impl = generate_delete_impl(&name, &table_name, primary_key, &impl_generics, &ty_generics, &where_clause);
|
&fields,
|
||||||
let soft_delete_impl = generate_soft_delete_impl(&name, &table_name, primary_key, soft_delete_field, &impl_generics, &ty_generics, &where_clause);
|
has_created_at,
|
||||||
|
has_updated_at,
|
||||||
|
&impl_generics,
|
||||||
|
&ty_generics,
|
||||||
|
&where_clause,
|
||||||
|
);
|
||||||
|
let get_impl = generate_get_impl(
|
||||||
|
&name,
|
||||||
|
&table_name,
|
||||||
|
primary_key,
|
||||||
|
version_field,
|
||||||
|
soft_delete_field,
|
||||||
|
&fields,
|
||||||
|
&impl_generics,
|
||||||
|
&ty_generics,
|
||||||
|
&where_clause,
|
||||||
|
);
|
||||||
|
let update_impl = generate_update_impl(
|
||||||
|
&name,
|
||||||
|
&update_form_name,
|
||||||
|
&table_name,
|
||||||
|
&fields,
|
||||||
|
primary_key,
|
||||||
|
version_field,
|
||||||
|
has_updated_at,
|
||||||
|
&impl_generics,
|
||||||
|
&ty_generics,
|
||||||
|
&where_clause,
|
||||||
|
);
|
||||||
|
let diff_impl = generate_diff_impl(
|
||||||
|
&name,
|
||||||
|
&update_form_name,
|
||||||
|
&fields,
|
||||||
|
primary_key,
|
||||||
|
version_field,
|
||||||
|
&impl_generics,
|
||||||
|
&ty_generics,
|
||||||
|
&where_clause,
|
||||||
|
);
|
||||||
|
let delete_impl = generate_delete_impl(
|
||||||
|
&name,
|
||||||
|
&table_name,
|
||||||
|
primary_key,
|
||||||
|
&impl_generics,
|
||||||
|
&ty_generics,
|
||||||
|
&where_clause,
|
||||||
|
);
|
||||||
|
let soft_delete_impl = generate_soft_delete_impl(
|
||||||
|
&name,
|
||||||
|
&table_name,
|
||||||
|
primary_key,
|
||||||
|
soft_delete_field,
|
||||||
|
&impl_generics,
|
||||||
|
&ty_generics,
|
||||||
|
&where_clause,
|
||||||
|
);
|
||||||
|
|
||||||
let pk_type = &primary_key.ty;
|
let pk_type = &primary_key.ty;
|
||||||
let pk_field_name = &primary_key.ident;
|
let pk_field_name = &primary_key.ident;
|
||||||
|
|
@ -179,10 +269,13 @@ fn derive_entity_internal(input: TokenStream) -> TokenStream {
|
||||||
format!("entity_changes_{}", #table_name)
|
format!("entity_changes_{}", #table_name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.into()
|
}
|
||||||
|
.into()
|
||||||
}
|
}
|
||||||
fn get_table_name(input: &DeriveInput) -> String {
|
fn get_table_name(input: &DeriveInput) -> String {
|
||||||
input.attrs.iter()
|
input
|
||||||
|
.attrs
|
||||||
|
.iter()
|
||||||
.find_map(|attr| {
|
.find_map(|attr| {
|
||||||
if attr.path().is_ident("table_name") {
|
if attr.path().is_ident("table_name") {
|
||||||
parse_string_attr(attr)
|
parse_string_attr(attr)
|
||||||
|
|
@ -195,10 +288,14 @@ fn get_table_name(input: &DeriveInput) -> String {
|
||||||
|
|
||||||
fn parse_fields(input: &DeriveInput) -> Vec<EntityField> {
|
fn parse_fields(input: &DeriveInput) -> Vec<EntityField> {
|
||||||
match &input.data {
|
match &input.data {
|
||||||
Data::Struct(data_struct) => {
|
Data::Struct(data_struct) => data_struct
|
||||||
data_struct.fields.iter().map(|field| {
|
.fields
|
||||||
|
.iter()
|
||||||
|
.map(|field| {
|
||||||
let ident = field.ident.as_ref().unwrap().clone();
|
let ident = field.ident.as_ref().unwrap().clone();
|
||||||
let db_name = field.attrs.iter()
|
let db_name = field
|
||||||
|
.attrs
|
||||||
|
.iter()
|
||||||
.find_map(|attr| {
|
.find_map(|attr| {
|
||||||
if attr.path().is_ident("rename") {
|
if attr.path().is_ident("rename") {
|
||||||
parse_string_attr(attr)
|
parse_string_attr(attr)
|
||||||
|
|
@ -208,8 +305,7 @@ fn parse_fields(input: &DeriveInput) -> Vec<EntityField> {
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| ident.to_string());
|
.unwrap_or_else(|| ident.to_string());
|
||||||
|
|
||||||
let type_override = field.attrs.iter()
|
let type_override = field.attrs.iter().find_map(|attr| {
|
||||||
.find_map(|attr| {
|
|
||||||
if attr.path().is_ident("field_type") {
|
if attr.path().is_ident("field_type") {
|
||||||
parse_string_attr(attr)
|
parse_string_attr(attr)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -224,15 +320,25 @@ fn parse_fields(input: &DeriveInput) -> Vec<EntityField> {
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
let is_primary_key = field.attrs.iter()
|
let is_primary_key = field
|
||||||
|
.attrs
|
||||||
|
.iter()
|
||||||
.any(|attr| attr.path().is_ident("primary_key"));
|
.any(|attr| attr.path().is_ident("primary_key"));
|
||||||
let is_version_field = field.attrs.iter()
|
let is_version_field = field
|
||||||
|
.attrs
|
||||||
|
.iter()
|
||||||
.any(|attr| attr.path().is_ident("version"));
|
.any(|attr| attr.path().is_ident("version"));
|
||||||
let is_soft_delete = field.attrs.iter()
|
let is_soft_delete = field
|
||||||
|
.attrs
|
||||||
|
.iter()
|
||||||
.any(|attr| attr.path().is_ident("soft_delete"));
|
.any(|attr| attr.path().is_ident("soft_delete"));
|
||||||
let is_created_at = field.attrs.iter()
|
let is_created_at = field
|
||||||
|
.attrs
|
||||||
|
.iter()
|
||||||
.any(|attr| attr.path().is_ident("created_at"));
|
.any(|attr| attr.path().is_ident("created_at"));
|
||||||
let is_updated_at = field.attrs.iter()
|
let is_updated_at = field
|
||||||
|
.attrs
|
||||||
|
.iter()
|
||||||
.any(|attr| attr.path().is_ident("updated_at"));
|
.any(|attr| attr.path().is_ident("updated_at"));
|
||||||
|
|
||||||
EntityField {
|
EntityField {
|
||||||
|
|
@ -247,14 +353,15 @@ fn parse_fields(input: &DeriveInput) -> Vec<EntityField> {
|
||||||
is_created_at,
|
is_created_at,
|
||||||
is_updated_at,
|
is_updated_at,
|
||||||
}
|
}
|
||||||
}).collect()
|
})
|
||||||
}
|
.collect(),
|
||||||
_ => panic!("Entity can only be derived for structs"),
|
_ => panic!("Entity can only be derived for structs"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_version_field(f: &EntityField) -> bool {
|
fn is_version_field(f: &EntityField) -> bool {
|
||||||
f.ident == "version" && matches!(&f.ty, Type::Path(p) if p.path.is_ident("u64") ||
|
f.ident == "version"
|
||||||
|
&& matches!(&f.ty, Type::Path(p) if p.path.is_ident("u64") ||
|
||||||
p.path.is_ident("u32") || p.path.is_ident("i64") || p.path.is_ident("i32"))
|
p.path.is_ident("u32") || p.path.is_ident("i64") || p.path.is_ident("i32"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -276,10 +383,13 @@ fn generate_insert_impl(
|
||||||
let db = db_type();
|
let db = db_type();
|
||||||
let pk_db_name = &primary_key.db_name;
|
let pk_db_name = &primary_key.db_name;
|
||||||
|
|
||||||
let bindings: Vec<_> = fields.iter().map(|f| {
|
let bindings: Vec<_> = fields
|
||||||
|
.iter()
|
||||||
|
.map(|f| {
|
||||||
let ident = &f.ident;
|
let ident = &f.ident;
|
||||||
quote! { &self.#ident }
|
quote! { &self.#ident }
|
||||||
}).collect();
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
let pk_field = &primary_key.ident;
|
let pk_field = &primary_key.ident;
|
||||||
let pk_type = &primary_key.ty;
|
let pk_type = &primary_key.ty;
|
||||||
|
|
@ -410,7 +520,7 @@ fn get_type_string(field: &EntityField) -> String {
|
||||||
|
|
||||||
if clean_type.starts_with("Option<") && clean_type.ends_with(">") {
|
if clean_type.starts_with("Option<") && clean_type.ends_with(">") {
|
||||||
// Extract inner type between < and >
|
// Extract inner type between < and >
|
||||||
clean_type[7..clean_type.len()-1].to_string()
|
clean_type[7..clean_type.len() - 1].to_string()
|
||||||
} else {
|
} else {
|
||||||
type_str
|
type_str
|
||||||
}
|
}
|
||||||
|
|
@ -456,9 +566,11 @@ fn generate_get_impl(
|
||||||
let db = db_type();
|
let db = db_type();
|
||||||
|
|
||||||
let new_fields = select_fields.clone().collect::<Vec<_>>();
|
let new_fields = select_fields.clone().collect::<Vec<_>>();
|
||||||
let select_fields_str = new_fields.iter()
|
let select_fields_str = new_fields
|
||||||
.filter_map(|e| e.split(" ")
|
.iter()
|
||||||
.next()).collect::<Vec<_>>().join(", ");
|
.filter_map(|e| e.split(" ").next())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
let select_field_list = select_fields.clone().collect::<Vec<_>>();
|
let select_field_list = select_fields.clone().collect::<Vec<_>>();
|
||||||
|
|
||||||
|
|
@ -528,8 +640,6 @@ fn generate_get_impl(
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// If no version field, generate empty implementation
|
// If no version field, generate empty implementation
|
||||||
quote! {}
|
quote! {}
|
||||||
|
|
@ -545,7 +655,10 @@ fn generate_get_impl(
|
||||||
let select_stmt = format!(
|
let select_stmt = format!(
|
||||||
r#"SELECT DISTINCT {} FROM {}{}{} WHERE {} = {}"#,
|
r#"SELECT DISTINCT {} FROM {}{}{} WHERE {} = {}"#,
|
||||||
select_fields.clone().collect::<Vec<_>>().join(", "),
|
select_fields.clone().collect::<Vec<_>>().join(", "),
|
||||||
tq, table_name, tq, pk_db_field_name,
|
tq,
|
||||||
|
table_name,
|
||||||
|
tq,
|
||||||
|
pk_db_field_name,
|
||||||
static_placeholder(1)
|
static_placeholder(1)
|
||||||
);
|
);
|
||||||
quote! {
|
quote! {
|
||||||
|
|
@ -922,9 +1035,16 @@ fn generate_update_impl(
|
||||||
ty_generics: &TypeGenerics,
|
ty_generics: &TypeGenerics,
|
||||||
where_clause: &Option<&WhereClause>,
|
where_clause: &Option<&WhereClause>,
|
||||||
) -> TokenStream2 {
|
) -> TokenStream2 {
|
||||||
let update_fields: Vec<_> = fields.iter()
|
let update_fields: Vec<_> = fields
|
||||||
.filter(|f| f.ident != primary_key.ident && f.ident != "created_at" &&
|
.iter()
|
||||||
version_field.as_ref().map(|vf| f.ident != vf.ident).unwrap_or(true))
|
.filter(|f| {
|
||||||
|
f.ident != primary_key.ident
|
||||||
|
&& f.ident != "created_at"
|
||||||
|
&& version_field
|
||||||
|
.as_ref()
|
||||||
|
.map(|vf| f.ident != vf.ident)
|
||||||
|
.unwrap_or(true)
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let field_idents: Vec<_> = update_fields.iter().map(|f| &f.ident).collect();
|
let field_idents: Vec<_> = update_fields.iter().map(|f| &f.ident).collect();
|
||||||
|
|
@ -933,7 +1053,9 @@ fn generate_update_impl(
|
||||||
let db = db_type();
|
let db = db_type();
|
||||||
let db_args = db_arguments();
|
let db_args = db_arguments();
|
||||||
|
|
||||||
let setter_methods: Vec<_> = update_fields.iter().map(|field| {
|
let setter_methods: Vec<_> = update_fields
|
||||||
|
.iter()
|
||||||
|
.map(|field| {
|
||||||
let method_name = format_ident!("set_{}", field.ident);
|
let method_name = format_ident!("set_{}", field.ident);
|
||||||
let field_type = &field.ty;
|
let field_type = &field.ty;
|
||||||
let field_ident = &field.ident;
|
let field_ident = &field.ident;
|
||||||
|
|
@ -945,7 +1067,8 @@ fn generate_update_impl(
|
||||||
self.#field_ident = Some(value.into());
|
self.#field_ident = Some(value.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).collect();
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
let builder_methods = update_fields.iter().map(|field| {
|
let builder_methods = update_fields.iter().map(|field| {
|
||||||
let method_name = format_ident!("with_{}", field.ident);
|
let method_name = format_ident!("with_{}", field.ident);
|
||||||
|
|
@ -963,7 +1086,8 @@ fn generate_update_impl(
|
||||||
});
|
});
|
||||||
|
|
||||||
// Generate eval_* methods for non-binary fields
|
// Generate eval_* methods for non-binary fields
|
||||||
let eval_methods: Vec<_> = update_fields.iter()
|
let eval_methods: Vec<_> = update_fields
|
||||||
|
.iter()
|
||||||
.filter(|f| !is_binary_type(&f.ty))
|
.filter(|f| !is_binary_type(&f.ty))
|
||||||
.map(|field| {
|
.map(|field| {
|
||||||
let method_name = format_ident!("eval_{}", field.ident);
|
let method_name = format_ident!("eval_{}", field.ident);
|
||||||
|
|
@ -1163,9 +1287,16 @@ fn generate_diff_impl(
|
||||||
ty_generics: &TypeGenerics,
|
ty_generics: &TypeGenerics,
|
||||||
where_clause: &Option<&WhereClause>,
|
where_clause: &Option<&WhereClause>,
|
||||||
) -> TokenStream2 {
|
) -> TokenStream2 {
|
||||||
let update_fields: Vec<_> = fields.iter()
|
let update_fields: Vec<_> = fields
|
||||||
.filter(|f| f.ident != primary_key.ident && f.ident != "created_at" &&
|
.iter()
|
||||||
version_field.as_ref().map(|vf| f.ident != vf.ident).unwrap_or(true))
|
.filter(|f| {
|
||||||
|
f.ident != primary_key.ident
|
||||||
|
&& f.ident != "created_at"
|
||||||
|
&& version_field
|
||||||
|
.as_ref()
|
||||||
|
.map(|vf| f.ident != vf.ident)
|
||||||
|
.unwrap_or(true)
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let field_idents: Vec<_> = update_fields.iter().map(|f| &f.ident).collect();
|
let field_idents: Vec<_> = update_fields.iter().map(|f| &f.ident).collect();
|
||||||
|
|
@ -1178,7 +1309,7 @@ fn generate_diff_impl(
|
||||||
let pk_type = primary_key.ty.clone();
|
let pk_type = primary_key.ty.clone();
|
||||||
let pk_db_name = primary_key.db_name.clone();
|
let pk_db_name = primary_key.db_name.clone();
|
||||||
|
|
||||||
let multi_pk_field= format_ident!("{}", pluralize(&pk_field.to_string()));
|
let multi_pk_field = format_ident!("{}", pluralize(&pk_field.to_string()));
|
||||||
let update_by_func = format_ident!("update_by_{}", pk_field);
|
let update_by_func = format_ident!("update_by_{}", pk_field);
|
||||||
let multi_update_by_func = format_ident!("update_by_{}", multi_pk_field);
|
let multi_update_by_func = format_ident!("update_by_{}", multi_pk_field);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,8 @@ pub(crate) fn pluralize(word: &str) -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle possessives and existing plurals
|
// Handle possessives and existing plurals
|
||||||
if word.ends_with("'s") || word.ends_with("'") ||
|
if word.ends_with("'s") || word.ends_with("'") || word.ends_with("s's") || word.ends_with("s'")
|
||||||
word.ends_with("s's") || word.ends_with("s'") {
|
{
|
||||||
return word.to_string();
|
return word.to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,19 +40,15 @@ pub(crate) fn pluralize(word: &str) -> String {
|
||||||
// Compound words with hyphens
|
// Compound words with hyphens
|
||||||
if word.contains('-') {
|
if word.contains('-') {
|
||||||
let parts: Vec<&str> = word.split('-').collect();
|
let parts: Vec<&str> = word.split('-').collect();
|
||||||
return format!("{}-{}",
|
return format!("{}-{}", pluralize(parts[0]), parts[1..].join("-"));
|
||||||
pluralize(parts[0]),
|
|
||||||
parts[1..].join("-")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invariant words (same singular and plural)
|
// Invariant words (same singular and plural)
|
||||||
match word.to_lowercase().as_str() {
|
match word.to_lowercase().as_str() {
|
||||||
"sheep" | "deer" | "moose" | "swine" | "buffalo" | "fish" | "trout" |
|
"sheep" | "deer" | "moose" | "swine" | "buffalo" | "fish" | "trout" | "salmon" | "pike"
|
||||||
"salmon" | "pike" | "aircraft" | "series" | "species" | "means" |
|
| "aircraft" | "series" | "species" | "means" | "crossroads" | "swiss" | "portuguese"
|
||||||
"crossroads" | "swiss" | "portuguese" | "vietnamese" | "japanese" |
|
| "vietnamese" | "japanese" | "chinese" | "chassis" | "corps" | "headquarters"
|
||||||
"chinese" | "chassis" | "corps" | "headquarters" | "diabetes" |
|
| "diabetes" | "news" | "odds" | "innings" => return word.to_string(),
|
||||||
"news" | "odds" | "innings" => return word.to_string(),
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -103,7 +99,8 @@ pub(crate) fn pluralize(word: &str) -> String {
|
||||||
"millennium" => "millennia",
|
"millennium" => "millennia",
|
||||||
|
|
||||||
_ => return apply_general_rules(word),
|
_ => return apply_general_rules(word),
|
||||||
}.to_string()
|
}
|
||||||
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_general_rules(word: &str) -> String {
|
fn apply_general_rules(word: &str) -> String {
|
||||||
|
|
@ -111,9 +108,22 @@ fn apply_general_rules(word: &str) -> String {
|
||||||
if word.ends_with('o') {
|
if word.ends_with('o') {
|
||||||
match word.to_lowercase().as_str() {
|
match word.to_lowercase().as_str() {
|
||||||
// -o → -oes
|
// -o → -oes
|
||||||
w if matches!(w, "hero" | "potato" | "tomato" | "echo" |
|
w if matches!(
|
||||||
"tornado" | "torpedo" | "veto" | "mosquito" |
|
w,
|
||||||
"volcano" | "buffalo" | "domino" | "embargo") => {
|
"hero"
|
||||||
|
| "potato"
|
||||||
|
| "tomato"
|
||||||
|
| "echo"
|
||||||
|
| "tornado"
|
||||||
|
| "torpedo"
|
||||||
|
| "veto"
|
||||||
|
| "mosquito"
|
||||||
|
| "volcano"
|
||||||
|
| "buffalo"
|
||||||
|
| "domino"
|
||||||
|
| "embargo"
|
||||||
|
) =>
|
||||||
|
{
|
||||||
return format!("{}es", word);
|
return format!("{}es", word);
|
||||||
}
|
}
|
||||||
// -o → -os
|
// -o → -os
|
||||||
|
|
@ -147,9 +157,13 @@ fn apply_general_rules(word: &str) -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Words ending in sibilants (-s, -ss, -sh, -ch, -x, -z)
|
// Words ending in sibilants (-s, -ss, -sh, -ch, -x, -z)
|
||||||
if word.ends_with('s') || word.ends_with("ss") ||
|
if word.ends_with('s')
|
||||||
word.ends_with("sh") || word.ends_with("ch") ||
|
|| word.ends_with("ss")
|
||||||
word.ends_with('x') || word.ends_with('z') {
|
|| word.ends_with("sh")
|
||||||
|
|| word.ends_with("ch")
|
||||||
|
|| word.ends_with('x')
|
||||||
|
|| word.ends_with('z')
|
||||||
|
{
|
||||||
return format!("{}es", word);
|
return format!("{}es", word);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ use sqlx::pool::PoolConnection;
|
||||||
use sqlx::{MySql, MySqlConnection, MySqlPool, Transaction};
|
use sqlx::{MySql, MySqlConnection, MySqlPool, Transaction};
|
||||||
|
|
||||||
#[cfg(feature = "postgres")]
|
#[cfg(feature = "postgres")]
|
||||||
use sqlx::{Postgres, PgConnection, PgPool, Transaction};
|
use sqlx::{PgConnection, PgPool, Postgres, Transaction};
|
||||||
|
|
||||||
#[cfg(feature = "sqlite")]
|
#[cfg(feature = "sqlite")]
|
||||||
use sqlx::{Sqlite, SqliteConnection, SqlitePool, Transaction};
|
use sqlx::{Sqlite, SqliteConnection, SqlitePool, Transaction};
|
||||||
|
|
@ -16,9 +16,7 @@ use sqlx::{Sqlite, SqliteConnection, SqlitePool, Transaction};
|
||||||
#[cfg(feature = "mysql")]
|
#[cfg(feature = "mysql")]
|
||||||
pub enum ConnProvider<'a> {
|
pub enum ConnProvider<'a> {
|
||||||
/// Stores a reference to an existing connection
|
/// Stores a reference to an existing connection
|
||||||
Borrowed {
|
Borrowed { conn: &'a mut PoolConnection<MySql> },
|
||||||
conn: &'a mut PoolConnection<MySql>,
|
|
||||||
},
|
|
||||||
/// Stores an owned connection acquired from a pool
|
/// Stores an owned connection acquired from a pool
|
||||||
Owned {
|
Owned {
|
||||||
pool: MySqlPool,
|
pool: MySqlPool,
|
||||||
|
|
|
||||||
|
|
@ -115,13 +115,21 @@ pub fn placeholder(index: usize) -> String {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn table_quote() -> &'static str {
|
pub fn table_quote() -> &'static str {
|
||||||
#[cfg(feature = "mysql")]
|
#[cfg(feature = "mysql")]
|
||||||
{ "`" }
|
{
|
||||||
|
"`"
|
||||||
|
}
|
||||||
#[cfg(feature = "postgres")]
|
#[cfg(feature = "postgres")]
|
||||||
{ "\"" }
|
{
|
||||||
|
"\""
|
||||||
|
}
|
||||||
#[cfg(feature = "sqlite")]
|
#[cfg(feature = "sqlite")]
|
||||||
{ "\"" }
|
{
|
||||||
|
"\""
|
||||||
|
}
|
||||||
#[cfg(not(any(feature = "mysql", feature = "postgres", feature = "sqlite")))]
|
#[cfg(not(any(feature = "mysql", feature = "postgres", feature = "sqlite")))]
|
||||||
{ "`" }
|
{
|
||||||
|
"`"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds an index hint clause (MySQL-specific, empty for other databases)
|
/// Builds an index hint clause (MySQL-specific, empty for other databases)
|
||||||
|
|
@ -129,7 +137,9 @@ pub fn table_quote() -> &'static str {
|
||||||
pub fn build_index_clause(index: Option<&str>) -> String {
|
pub fn build_index_clause(index: Option<&str>) -> String {
|
||||||
#[cfg(feature = "mysql")]
|
#[cfg(feature = "mysql")]
|
||||||
{
|
{
|
||||||
index.map(|idx| format!("USE INDEX ({})", idx)).unwrap_or_default()
|
index
|
||||||
|
.map(|idx| format!("USE INDEX ({})", idx))
|
||||||
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
#[cfg(not(feature = "mysql"))]
|
#[cfg(not(feature = "mysql"))]
|
||||||
{
|
{
|
||||||
|
|
@ -252,7 +262,10 @@ impl Filter<'_> {
|
||||||
Self::build_where_clause_with_offset(filters, 1)
|
Self::build_where_clause_with_offset(filters, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_where_clause_with_offset(filters: &[Filter], start_index: usize) -> (String, Vec<Value>) {
|
pub fn build_where_clause_with_offset(
|
||||||
|
filters: &[Filter],
|
||||||
|
start_index: usize,
|
||||||
|
) -> (String, Vec<Value>) {
|
||||||
let mut values = Vec::new();
|
let mut values = Vec::new();
|
||||||
let mut current_index = start_index;
|
let mut current_index = start_index;
|
||||||
|
|
||||||
|
|
@ -323,20 +336,26 @@ impl Filter<'_> {
|
||||||
format!("{} NOT LIKE {}", field, ph)
|
format!("{} NOT LIKE {}", field, ph)
|
||||||
}
|
}
|
||||||
Filter::In(field, value_vec) => {
|
Filter::In(field, value_vec) => {
|
||||||
let placeholders: Vec<String> = value_vec.iter().map(|_| {
|
let placeholders: Vec<String> = value_vec
|
||||||
|
.iter()
|
||||||
|
.map(|_| {
|
||||||
let ph = placeholder(current_index);
|
let ph = placeholder(current_index);
|
||||||
current_index += 1;
|
current_index += 1;
|
||||||
ph
|
ph
|
||||||
}).collect();
|
})
|
||||||
|
.collect();
|
||||||
values.extend(value_vec.clone());
|
values.extend(value_vec.clone());
|
||||||
format!("{} IN ({})", field, placeholders.join(", "))
|
format!("{} IN ({})", field, placeholders.join(", "))
|
||||||
}
|
}
|
||||||
Filter::NotIn(field, value_vec) => {
|
Filter::NotIn(field, value_vec) => {
|
||||||
let placeholders: Vec<String> = value_vec.iter().map(|_| {
|
let placeholders: Vec<String> = value_vec
|
||||||
|
.iter()
|
||||||
|
.map(|_| {
|
||||||
let ph = placeholder(current_index);
|
let ph = placeholder(current_index);
|
||||||
current_index += 1;
|
current_index += 1;
|
||||||
ph
|
ph
|
||||||
}).collect();
|
})
|
||||||
|
.collect();
|
||||||
values.extend(value_vec.clone());
|
values.extend(value_vec.clone());
|
||||||
format!("{} NOT IN ({})", field, placeholders.join(", "))
|
format!("{} NOT IN ({})", field, placeholders.join(", "))
|
||||||
}
|
}
|
||||||
|
|
@ -347,16 +366,24 @@ impl Filter<'_> {
|
||||||
format!("{} IS NOT NULL", field)
|
format!("{} IS NOT NULL", field)
|
||||||
}
|
}
|
||||||
Filter::And(nested_filters) => {
|
Filter::And(nested_filters) => {
|
||||||
let (nested_clause, nested_values) = Self::build_where_clause_with_offset(nested_filters, current_index);
|
let (nested_clause, nested_values) =
|
||||||
|
Self::build_where_clause_with_offset(nested_filters, current_index);
|
||||||
current_index += nested_values.len();
|
current_index += nested_values.len();
|
||||||
values.extend(nested_values);
|
values.extend(nested_values);
|
||||||
format!("({})", nested_clause)
|
format!("({})", nested_clause)
|
||||||
}
|
}
|
||||||
Filter::Or(nested_filters) => {
|
Filter::Or(nested_filters) => {
|
||||||
let (nested_clause, nested_values) = Self::build_where_clause_with_offset(nested_filters, current_index);
|
let (nested_clause, nested_values) =
|
||||||
|
Self::build_where_clause_with_offset(nested_filters, current_index);
|
||||||
current_index += nested_values.len();
|
current_index += nested_values.len();
|
||||||
values.extend(nested_values);
|
values.extend(nested_values);
|
||||||
format!("({})", nested_clause.split(" AND ").collect::<Vec<_>>().join(" OR "))
|
format!(
|
||||||
|
"({})",
|
||||||
|
nested_clause
|
||||||
|
.split(" AND ")
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" OR ")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,27 @@
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! update_entity_func {
|
macro_rules! update_entity_func {
|
||||||
($form_type:ident, $func_name:ident) => {
|
($form_type:ident, $func_name:ident) => {
|
||||||
pub async fn $func_name<'a, E>(executor: E, id: &Uuid, form: $form_type) -> Result<(), RepositoryError>
|
pub async fn $func_name<'a, E>(
|
||||||
|
executor: E,
|
||||||
|
id: &Uuid,
|
||||||
|
form: $form_type,
|
||||||
|
) -> Result<(), RepositoryError>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database = $crate::prelude::DB>,
|
E: sqlx::Executor<'a, Database = $crate::prelude::DB>,
|
||||||
{
|
{
|
||||||
//If the section exists, we update it
|
//If the section exists, we update it
|
||||||
let result = sqlx::query(
|
let result = sqlx::query(
|
||||||
format!(r#"
|
format!(
|
||||||
|
r#"
|
||||||
UPDATE {}
|
UPDATE {}
|
||||||
SET {}
|
SET {}
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
"#,
|
"#,
|
||||||
$form_type::table_name(),
|
$form_type::table_name(),
|
||||||
form.update_stmt()).as_str())
|
form.update_stmt()
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
)
|
||||||
.bind_form_values(form)
|
.bind_form_values(form)
|
||||||
.bind(id)
|
.bind(id)
|
||||||
.execute(executor)
|
.execute(executor)
|
||||||
|
|
|
||||||
21
src/lib.rs
21
src/lib.rs
|
|
@ -2,14 +2,14 @@ use chrono::Utc;
|
||||||
use rand::random;
|
use rand::random;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub mod models;
|
|
||||||
pub mod repositories;
|
|
||||||
mod helpers;
|
|
||||||
mod value;
|
|
||||||
mod filter;
|
|
||||||
mod conn_provider;
|
mod conn_provider;
|
||||||
|
mod filter;
|
||||||
|
mod helpers;
|
||||||
|
pub mod models;
|
||||||
mod pagination;
|
mod pagination;
|
||||||
|
pub mod repositories;
|
||||||
mod transaction;
|
mod transaction;
|
||||||
|
mod value;
|
||||||
|
|
||||||
pub use pagination::{Page, PageRequest};
|
pub use pagination::{Page, PageRequest};
|
||||||
// transaction! macro is exported via #[macro_export] in transaction.rs
|
// transaction! macro is exported via #[macro_export] in transaction.rs
|
||||||
|
|
@ -174,14 +174,13 @@ macro_rules! lookup_options {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
pub use crate::value::*;
|
|
||||||
pub use crate::filter::*;
|
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, transaction};
|
|
||||||
pub use crate::pagination::{Page, PageRequest};
|
pub use crate::pagination::{Page, PageRequest};
|
||||||
pub use crate::conn_provider::*;
|
pub use crate::value::*;
|
||||||
|
pub use crate::values;
|
||||||
|
pub use crate::{filter_and as and, filter_or as or};
|
||||||
|
pub use crate::{filter_and, filter_or, filters, update_entity_func};
|
||||||
|
pub use crate::{lookup_options, lookup_table, new_uuid, transaction};
|
||||||
|
|
||||||
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
|
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
|
||||||
pub use crate::conn_provider::ConnProvider;
|
pub use crate::conn_provider::ConnProvider;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use std::fmt::Display;
|
|
||||||
use sqlx::FromRow;
|
|
||||||
use uuid::Uuid;
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
use sqlx::FromRow;
|
||||||
|
use std::fmt::Display;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Debug, FromRow)]
|
#[derive(Debug, FromRow)]
|
||||||
pub struct EntityChange {
|
pub struct EntityChange {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,12 @@ pub struct Page<T> {
|
||||||
|
|
||||||
impl<T> Page<T> {
|
impl<T> Page<T> {
|
||||||
pub fn new(items: Vec<T>, total_count: u64, page: u32, page_size: u32) -> Self {
|
pub fn new(items: Vec<T>, total_count: u64, page: u32, page_size: u32) -> Self {
|
||||||
Self { items, total_count, page, page_size }
|
Self {
|
||||||
|
items,
|
||||||
|
total_count,
|
||||||
|
page,
|
||||||
|
page_size,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Total number of pages
|
/// Total number of pages
|
||||||
|
|
@ -93,7 +98,11 @@ impl PageRequest {
|
||||||
|
|
||||||
/// Calculate SQL OFFSET (0-indexed)
|
/// Calculate SQL OFFSET (0-indexed)
|
||||||
pub fn offset(&self) -> u32 {
|
pub fn offset(&self) -> u32 {
|
||||||
if self.page <= 1 { 0 } else { (self.page - 1) * self.page_size }
|
if self.page <= 1 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
(self.page - 1) * self.page_size
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate SQL LIMIT
|
/// Calculate SQL LIMIT
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
|
use crate::models::EntityChange;
|
||||||
use sqlx::Error;
|
use sqlx::Error;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use crate::models::EntityChange;
|
|
||||||
|
|
||||||
#[cfg(feature = "mysql")]
|
#[cfg(feature = "mysql")]
|
||||||
use sqlx::MySqlExecutor as Executor;
|
use sqlx::MySqlExecutor as Executor;
|
||||||
|
|
@ -29,13 +29,21 @@ fn ph(index: usize) -> String {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn table_quote() -> &'static str {
|
fn table_quote() -> &'static str {
|
||||||
#[cfg(feature = "mysql")]
|
#[cfg(feature = "mysql")]
|
||||||
{ "`" }
|
{
|
||||||
|
"`"
|
||||||
|
}
|
||||||
#[cfg(feature = "postgres")]
|
#[cfg(feature = "postgres")]
|
||||||
{ "\"" }
|
{
|
||||||
|
"\""
|
||||||
|
}
|
||||||
#[cfg(feature = "sqlite")]
|
#[cfg(feature = "sqlite")]
|
||||||
{ "\"" }
|
{
|
||||||
|
"\""
|
||||||
|
}
|
||||||
#[cfg(not(any(feature = "mysql", feature = "postgres", feature = "sqlite")))]
|
#[cfg(not(any(feature = "mysql", feature = "postgres", feature = "sqlite")))]
|
||||||
{ "" }
|
{
|
||||||
|
""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
|
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
|
||||||
|
|
@ -51,7 +59,15 @@ pub async fn create_entity_change<'q>(
|
||||||
session_id, change_set_id, new_value)
|
session_id, change_set_id, new_value)
|
||||||
VALUES ({}, {}, {}, {}, {}, {}, {}, {})"#,
|
VALUES ({}, {}, {}, {}, {}, {}, {}, {})"#,
|
||||||
table_name,
|
table_name,
|
||||||
ph(1), ph(2), ph(3), ph(4), ph(5), ph(6), ph(7), ph(8));
|
ph(1),
|
||||||
|
ph(2),
|
||||||
|
ph(3),
|
||||||
|
ph(4),
|
||||||
|
ph(5),
|
||||||
|
ph(6),
|
||||||
|
ph(7),
|
||||||
|
ph(8)
|
||||||
|
);
|
||||||
|
|
||||||
sqlx::query(&query)
|
sqlx::query(&query)
|
||||||
.bind(&change.id)
|
.bind(&change.id)
|
||||||
|
|
@ -71,7 +87,9 @@ pub async fn create_entity_change<'q>(
|
||||||
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
|
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
|
||||||
pub async fn get_entity_changes_by_id<'q>(
|
pub async fn get_entity_changes_by_id<'q>(
|
||||||
conn: impl Executor<'q>,
|
conn: impl Executor<'q>,
|
||||||
table_name: &str, id: &Uuid) -> Result<Vec<EntityChange>, Error> {
|
table_name: &str,
|
||||||
|
id: &Uuid,
|
||||||
|
) -> Result<Vec<EntityChange>, Error> {
|
||||||
let q = table_quote();
|
let q = table_quote();
|
||||||
let query = format!(
|
let query = format!(
|
||||||
r#"SELECT
|
r#"SELECT
|
||||||
|
|
@ -84,7 +102,9 @@ pub async fn get_entity_changes_by_id<'q>(
|
||||||
change_set_id,
|
change_set_id,
|
||||||
new_value
|
new_value
|
||||||
FROM {q}{}{q} WHERE id = {}"#,
|
FROM {q}{}{q} WHERE id = {}"#,
|
||||||
table_name, ph(1));
|
table_name,
|
||||||
|
ph(1)
|
||||||
|
);
|
||||||
|
|
||||||
let changes = sqlx::query_as::<_, EntityChange>(&query)
|
let changes = sqlx::query_as::<_, EntityChange>(&query)
|
||||||
.bind(id)
|
.bind(id)
|
||||||
|
|
@ -97,7 +117,9 @@ pub async fn get_entity_changes_by_id<'q>(
|
||||||
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
|
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
|
||||||
pub async fn get_entity_changes_by_entity<'q>(
|
pub async fn get_entity_changes_by_entity<'q>(
|
||||||
conn: impl Executor<'q>,
|
conn: impl Executor<'q>,
|
||||||
table_name: &str, entity_id: &Uuid) -> Result<Vec<EntityChange>, Error> {
|
table_name: &str,
|
||||||
|
entity_id: &Uuid,
|
||||||
|
) -> Result<Vec<EntityChange>, Error> {
|
||||||
let q = table_quote();
|
let q = table_quote();
|
||||||
let query = format!(
|
let query = format!(
|
||||||
r#"SELECT
|
r#"SELECT
|
||||||
|
|
@ -110,7 +132,9 @@ pub async fn get_entity_changes_by_entity<'q>(
|
||||||
change_set_id,
|
change_set_id,
|
||||||
new_value
|
new_value
|
||||||
FROM {q}{}{q} WHERE entity_id = {}"#,
|
FROM {q}{}{q} WHERE entity_id = {}"#,
|
||||||
table_name, ph(1));
|
table_name,
|
||||||
|
ph(1)
|
||||||
|
);
|
||||||
|
|
||||||
let changes = sqlx::query_as::<_, EntityChange>(&query)
|
let changes = sqlx::query_as::<_, EntityChange>(&query)
|
||||||
.bind(entity_id)
|
.bind(entity_id)
|
||||||
|
|
@ -123,8 +147,9 @@ pub async fn get_entity_changes_by_entity<'q>(
|
||||||
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
|
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
|
||||||
pub async fn get_entity_changes_session<'q>(
|
pub async fn get_entity_changes_session<'q>(
|
||||||
conn: impl Executor<'q>,
|
conn: impl Executor<'q>,
|
||||||
table_name: &str, session_id: &Uuid,
|
table_name: &str,
|
||||||
) -> Result<Vec<EntityChange>, Error>{
|
session_id: &Uuid,
|
||||||
|
) -> Result<Vec<EntityChange>, Error> {
|
||||||
let q = table_quote();
|
let q = table_quote();
|
||||||
let query = format!(
|
let query = format!(
|
||||||
r#"SELECT
|
r#"SELECT
|
||||||
|
|
@ -137,7 +162,9 @@ pub async fn get_entity_changes_session<'q>(
|
||||||
change_set_id,
|
change_set_id,
|
||||||
new_value
|
new_value
|
||||||
FROM {q}{}{q} WHERE session_id = {}"#,
|
FROM {q}{}{q} WHERE session_id = {}"#,
|
||||||
table_name, ph(1));
|
table_name,
|
||||||
|
ph(1)
|
||||||
|
);
|
||||||
|
|
||||||
let changes = sqlx::query_as::<_, EntityChange>(&query)
|
let changes = sqlx::query_as::<_, EntityChange>(&query)
|
||||||
.bind(session_id)
|
.bind(session_id)
|
||||||
|
|
@ -150,7 +177,9 @@ pub async fn get_entity_changes_session<'q>(
|
||||||
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
|
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
|
||||||
pub async fn get_entity_changes_actor<'q>(
|
pub async fn get_entity_changes_actor<'q>(
|
||||||
conn: impl Executor<'q>,
|
conn: impl Executor<'q>,
|
||||||
table_name: &str, actor_id: &Uuid) -> Result<Vec<EntityChange>, Error>{
|
table_name: &str,
|
||||||
|
actor_id: &Uuid,
|
||||||
|
) -> Result<Vec<EntityChange>, Error> {
|
||||||
let q = table_quote();
|
let q = table_quote();
|
||||||
let query = format!(
|
let query = format!(
|
||||||
r#"SELECT
|
r#"SELECT
|
||||||
|
|
@ -163,7 +192,9 @@ pub async fn get_entity_changes_actor<'q>(
|
||||||
change_set_id,
|
change_set_id,
|
||||||
new_value
|
new_value
|
||||||
FROM {q}{}{q} WHERE actor_id = {}"#,
|
FROM {q}{}{q} WHERE actor_id = {}"#,
|
||||||
table_name, ph(1));
|
table_name,
|
||||||
|
ph(1)
|
||||||
|
);
|
||||||
|
|
||||||
let changes = sqlx::query_as::<_, EntityChange>(&query)
|
let changes = sqlx::query_as::<_, EntityChange>(&query)
|
||||||
.bind(actor_id)
|
.bind(actor_id)
|
||||||
|
|
@ -175,8 +206,10 @@ pub async fn get_entity_changes_actor<'q>(
|
||||||
|
|
||||||
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
|
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
|
||||||
pub async fn get_entity_changes_by_change_set<'q>(
|
pub async fn get_entity_changes_by_change_set<'q>(
|
||||||
conn: impl Executor<'q>, table_name: &str, change_set_id: &Uuid) -> Result<Vec<EntityChange>, Error>
|
conn: impl Executor<'q>,
|
||||||
{
|
table_name: &str,
|
||||||
|
change_set_id: &Uuid,
|
||||||
|
) -> Result<Vec<EntityChange>, Error> {
|
||||||
let q = table_quote();
|
let q = table_quote();
|
||||||
let query = format!(
|
let query = format!(
|
||||||
r#"SELECT
|
r#"SELECT
|
||||||
|
|
@ -189,7 +222,9 @@ pub async fn get_entity_changes_by_change_set<'q>(
|
||||||
change_set_id,
|
change_set_id,
|
||||||
new_value
|
new_value
|
||||||
FROM {q}{}{q} WHERE change_set_id = {}"#,
|
FROM {q}{}{q} WHERE change_set_id = {}"#,
|
||||||
table_name, ph(1));
|
table_name,
|
||||||
|
ph(1)
|
||||||
|
);
|
||||||
|
|
||||||
let changes = sqlx::query_as::<_, EntityChange>(&query)
|
let changes = sqlx::query_as::<_, EntityChange>(&query)
|
||||||
.bind(change_set_id)
|
.bind(change_set_id)
|
||||||
|
|
@ -201,7 +236,11 @@ pub async fn get_entity_changes_by_change_set<'q>(
|
||||||
|
|
||||||
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
|
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
|
||||||
pub async fn get_entity_changes_by_entity_and_actor<'q>(
|
pub async fn get_entity_changes_by_entity_and_actor<'q>(
|
||||||
conn: impl Executor<'q>, table_name: &str, entity_id: &Uuid, actor_id: &Uuid) -> Result<Vec<EntityChange>, Error>{
|
conn: impl Executor<'q>,
|
||||||
|
table_name: &str,
|
||||||
|
entity_id: &Uuid,
|
||||||
|
actor_id: &Uuid,
|
||||||
|
) -> Result<Vec<EntityChange>, Error> {
|
||||||
let q = table_quote();
|
let q = table_quote();
|
||||||
let query = format!(
|
let query = format!(
|
||||||
r#"SELECT
|
r#"SELECT
|
||||||
|
|
@ -214,7 +253,10 @@ pub async fn get_entity_changes_by_entity_and_actor<'q>(
|
||||||
change_set_id,
|
change_set_id,
|
||||||
new_value
|
new_value
|
||||||
FROM {q}{}{q} WHERE entity_id = {} AND actor_id = {}"#,
|
FROM {q}{}{q} WHERE entity_id = {} AND actor_id = {}"#,
|
||||||
table_name, ph(1), ph(2));
|
table_name,
|
||||||
|
ph(1),
|
||||||
|
ph(2)
|
||||||
|
);
|
||||||
|
|
||||||
let changes = sqlx::query_as::<_, EntityChange>(&query)
|
let changes = sqlx::query_as::<_, EntityChange>(&query)
|
||||||
.bind(entity_id)
|
.bind(entity_id)
|
||||||
|
|
|
||||||
146
src/value.rs
146
src/value.rs
|
|
@ -1,6 +1,6 @@
|
||||||
|
use crate::filter::placeholder;
|
||||||
use sqlx::query::{Query, QueryAs, QueryScalar};
|
use sqlx::query::{Query, QueryAs, QueryScalar};
|
||||||
use sqlx::types::chrono::{NaiveDate, NaiveDateTime, NaiveTime};
|
use sqlx::types::chrono::{NaiveDate, NaiveDateTime, NaiveTime};
|
||||||
use crate::filter::placeholder;
|
|
||||||
|
|
||||||
// Database type alias based on enabled feature
|
// Database type alias based on enabled feature
|
||||||
#[cfg(feature = "mysql")]
|
#[cfg(feature = "mysql")]
|
||||||
|
|
@ -110,10 +110,7 @@ pub enum UpdateExpr {
|
||||||
|
|
||||||
/// Raw SQL expression escape hatch: column = {sql}
|
/// Raw SQL expression escape hatch: column = {sql}
|
||||||
/// Placeholders in sql should use `?` and will be replaced with proper placeholders
|
/// Placeholders in sql should use `?` and will be replaced with proper placeholders
|
||||||
Raw {
|
Raw { sql: String, values: Vec<Value> },
|
||||||
sql: String,
|
|
||||||
values: Vec<Value>,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UpdateExpr {
|
impl UpdateExpr {
|
||||||
|
|
@ -123,34 +120,35 @@ impl UpdateExpr {
|
||||||
use crate::filter::Filter;
|
use crate::filter::Filter;
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
UpdateExpr::Set(v) => {
|
UpdateExpr::Set(v) => (placeholder(start_idx), vec![v.clone()]),
|
||||||
(placeholder(start_idx), vec![v.clone()])
|
UpdateExpr::Add(v) => (
|
||||||
}
|
format!("{} + {}", column, placeholder(start_idx)),
|
||||||
UpdateExpr::Add(v) => {
|
vec![v.clone()],
|
||||||
(format!("{} + {}", column, placeholder(start_idx)), vec![v.clone()])
|
),
|
||||||
}
|
UpdateExpr::Sub(v) => (
|
||||||
UpdateExpr::Sub(v) => {
|
format!("{} - {}", column, placeholder(start_idx)),
|
||||||
(format!("{} - {}", column, placeholder(start_idx)), vec![v.clone()])
|
vec![v.clone()],
|
||||||
}
|
),
|
||||||
UpdateExpr::Mul(v) => {
|
UpdateExpr::Mul(v) => (
|
||||||
(format!("{} * {}", column, placeholder(start_idx)), vec![v.clone()])
|
format!("{} * {}", column, placeholder(start_idx)),
|
||||||
}
|
vec![v.clone()],
|
||||||
UpdateExpr::Div(v) => {
|
),
|
||||||
(format!("{} / {}", column, placeholder(start_idx)), vec![v.clone()])
|
UpdateExpr::Div(v) => (
|
||||||
}
|
format!("{} / {}", column, placeholder(start_idx)),
|
||||||
UpdateExpr::Mod(v) => {
|
vec![v.clone()],
|
||||||
(format!("{} % {}", column, placeholder(start_idx)), vec![v.clone()])
|
),
|
||||||
}
|
UpdateExpr::Mod(v) => (
|
||||||
|
format!("{} % {}", column, placeholder(start_idx)),
|
||||||
|
vec![v.clone()],
|
||||||
|
),
|
||||||
UpdateExpr::Case { branches, default } => {
|
UpdateExpr::Case { branches, default } => {
|
||||||
let mut sql_parts = vec!["CASE".to_string()];
|
let mut sql_parts = vec!["CASE".to_string()];
|
||||||
let mut values = Vec::new();
|
let mut values = Vec::new();
|
||||||
let mut idx = start_idx;
|
let mut idx = start_idx;
|
||||||
|
|
||||||
for (condition, value) in branches {
|
for (condition, value) in branches {
|
||||||
let (cond_sql, cond_values) = Filter::build_where_clause_with_offset(
|
let (cond_sql, cond_values) =
|
||||||
&[condition.clone()],
|
Filter::build_where_clause_with_offset(&[condition.clone()], idx);
|
||||||
idx,
|
|
||||||
);
|
|
||||||
idx += cond_values.len();
|
idx += cond_values.len();
|
||||||
values.extend(cond_values);
|
values.extend(cond_values);
|
||||||
|
|
||||||
|
|
@ -165,10 +163,8 @@ impl UpdateExpr {
|
||||||
(sql_parts.join(" "), values)
|
(sql_parts.join(" "), values)
|
||||||
}
|
}
|
||||||
UpdateExpr::AddIf { condition, value } => {
|
UpdateExpr::AddIf { condition, value } => {
|
||||||
let (cond_sql, cond_values) = Filter::build_where_clause_with_offset(
|
let (cond_sql, cond_values) =
|
||||||
&[condition.clone()],
|
Filter::build_where_clause_with_offset(&[condition.clone()], start_idx);
|
||||||
start_idx,
|
|
||||||
);
|
|
||||||
let mut values = cond_values;
|
let mut values = cond_values;
|
||||||
let val_idx = start_idx + values.len();
|
let val_idx = start_idx + values.len();
|
||||||
|
|
||||||
|
|
@ -184,10 +180,8 @@ impl UpdateExpr {
|
||||||
(sql, values)
|
(sql, values)
|
||||||
}
|
}
|
||||||
UpdateExpr::SubIf { condition, value } => {
|
UpdateExpr::SubIf { condition, value } => {
|
||||||
let (cond_sql, cond_values) = Filter::build_where_clause_with_offset(
|
let (cond_sql, cond_values) =
|
||||||
&[condition.clone()],
|
Filter::build_where_clause_with_offset(&[condition.clone()], start_idx);
|
||||||
start_idx,
|
|
||||||
);
|
|
||||||
let mut values = cond_values;
|
let mut values = cond_values;
|
||||||
let val_idx = start_idx + values.len();
|
let val_idx = start_idx + values.len();
|
||||||
|
|
||||||
|
|
@ -202,15 +196,18 @@ impl UpdateExpr {
|
||||||
|
|
||||||
(sql, values)
|
(sql, values)
|
||||||
}
|
}
|
||||||
UpdateExpr::Coalesce(v) => {
|
UpdateExpr::Coalesce(v) => (
|
||||||
(format!("COALESCE({}, {})", column, placeholder(start_idx)), vec![v.clone()])
|
format!("COALESCE({}, {})", column, placeholder(start_idx)),
|
||||||
}
|
vec![v.clone()],
|
||||||
UpdateExpr::Greatest(v) => {
|
),
|
||||||
(format!("GREATEST({}, {})", column, placeholder(start_idx)), vec![v.clone()])
|
UpdateExpr::Greatest(v) => (
|
||||||
}
|
format!("GREATEST({}, {})", column, placeholder(start_idx)),
|
||||||
UpdateExpr::Least(v) => {
|
vec![v.clone()],
|
||||||
(format!("LEAST({}, {})", column, placeholder(start_idx)), vec![v.clone()])
|
),
|
||||||
}
|
UpdateExpr::Least(v) => (
|
||||||
|
format!("LEAST({}, {})", column, placeholder(start_idx)),
|
||||||
|
vec![v.clone()],
|
||||||
|
),
|
||||||
UpdateExpr::Raw { sql, values } => {
|
UpdateExpr::Raw { sql, values } => {
|
||||||
// Replace ? placeholders with proper database placeholders
|
// Replace ? placeholders with proper database placeholders
|
||||||
let mut result_sql = String::new();
|
let mut result_sql = String::new();
|
||||||
|
|
@ -239,11 +236,24 @@ impl UpdateExpr {
|
||||||
UpdateExpr::Mul(_) => 1,
|
UpdateExpr::Mul(_) => 1,
|
||||||
UpdateExpr::Div(_) => 1,
|
UpdateExpr::Div(_) => 1,
|
||||||
UpdateExpr::Mod(_) => 1,
|
UpdateExpr::Mod(_) => 1,
|
||||||
UpdateExpr::Case { branches, default: _ } => {
|
UpdateExpr::Case {
|
||||||
branches.iter().map(|(f, _)| f.param_count() + 1).sum::<usize>() + 1
|
branches,
|
||||||
|
default: _,
|
||||||
|
} => {
|
||||||
|
branches
|
||||||
|
.iter()
|
||||||
|
.map(|(f, _)| f.param_count() + 1)
|
||||||
|
.sum::<usize>()
|
||||||
|
+ 1
|
||||||
}
|
}
|
||||||
UpdateExpr::AddIf { condition, value: _ } => condition.param_count() + 1,
|
UpdateExpr::AddIf {
|
||||||
UpdateExpr::SubIf { condition, value: _ } => condition.param_count() + 1,
|
condition,
|
||||||
|
value: _,
|
||||||
|
} => condition.param_count() + 1,
|
||||||
|
UpdateExpr::SubIf {
|
||||||
|
condition,
|
||||||
|
value: _,
|
||||||
|
} => condition.param_count() + 1,
|
||||||
UpdateExpr::Coalesce(_) => 1,
|
UpdateExpr::Coalesce(_) => 1,
|
||||||
UpdateExpr::Greatest(_) => 1,
|
UpdateExpr::Greatest(_) => 1,
|
||||||
UpdateExpr::Least(_) => 1,
|
UpdateExpr::Least(_) => 1,
|
||||||
|
|
@ -319,7 +329,10 @@ macro_rules! bind_value {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
|
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
|
||||||
pub fn bind_values<'q>(query: Query<'q, DB, Arguments_<'q>>, values: &'q [Value]) -> Query<'q, DB, Arguments_<'q>> {
|
pub fn bind_values<'q>(
|
||||||
|
query: Query<'q, DB, Arguments_<'q>>,
|
||||||
|
values: &'q [Value],
|
||||||
|
) -> Query<'q, DB, Arguments_<'q>> {
|
||||||
let mut query = query;
|
let mut query = query;
|
||||||
for value in values {
|
for value in values {
|
||||||
query = bind_value!(query, value);
|
query = bind_value!(query, value);
|
||||||
|
|
@ -329,7 +342,10 @@ pub fn bind_values<'q>(query: Query<'q, DB, Arguments_<'q>>, values: &'q [Value]
|
||||||
|
|
||||||
/// Bind a single owned Value to a query
|
/// Bind a single owned Value to a query
|
||||||
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
|
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
|
||||||
pub fn bind_value_owned<'q>(query: Query<'q, DB, Arguments_<'q>>, value: Value) -> Query<'q, DB, Arguments_<'q>> {
|
pub fn bind_value_owned<'q>(
|
||||||
|
query: Query<'q, DB, Arguments_<'q>>,
|
||||||
|
value: Value,
|
||||||
|
) -> Query<'q, DB, Arguments_<'q>> {
|
||||||
match value {
|
match value {
|
||||||
Value::Null => query.bind(None::<String>),
|
Value::Null => query.bind(None::<String>),
|
||||||
Value::Int8(v) => query.bind(v),
|
Value::Int8(v) => query.bind(v),
|
||||||
|
|
@ -368,14 +384,20 @@ pub fn bind_value_owned<'q>(query: Query<'q, DB, Arguments_<'q>>, value: Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
|
#[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>> {
|
pub fn bind_as_values<'q, O>(
|
||||||
values.into_iter().fold(query, |query, value| {
|
query: QueryAs<'q, DB, O, Arguments_<'q>>,
|
||||||
bind_value!(query, value)
|
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"))]
|
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
|
||||||
pub fn bind_scalar_values<'q, O>(query: QueryScalar<'q, DB, O, Arguments_<'q>>, values: &'q [Value]) -> QueryScalar<'q, DB, O, Arguments_<'q>> {
|
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;
|
let mut query = query;
|
||||||
for value in values {
|
for value in values {
|
||||||
query = bind_value!(query, value);
|
query = bind_value!(query, value);
|
||||||
|
|
@ -385,8 +407,11 @@ pub fn bind_scalar_values<'q, O>(query: QueryScalar<'q, DB, O, Arguments_<'q>>,
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn query_fields(fields: Vec<&str>) -> String {
|
pub fn query_fields(fields: Vec<&str>) -> String {
|
||||||
fields.iter().filter_map(|e| e.split(" ").next())
|
fields
|
||||||
.collect::<Vec<_>>().join(", ")
|
.iter()
|
||||||
|
.filter_map(|e| e.split(" ").next())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
// From implementations for owned values
|
// From implementations for owned values
|
||||||
|
|
@ -656,9 +681,9 @@ impl<'q, O> BindValues<'q> for QueryAs<'q, DB, O, Arguments_<'q>> {
|
||||||
type Output = QueryAs<'q, DB, O, Arguments_<'q>>;
|
type Output = QueryAs<'q, DB, O, Arguments_<'q>>;
|
||||||
|
|
||||||
fn bind_values(self, values: &'q [Value]) -> Self::Output {
|
fn bind_values(self, values: &'q [Value]) -> Self::Output {
|
||||||
values.into_iter().fold(self, |query, value| {
|
values
|
||||||
bind_value!(query, value)
|
.into_iter()
|
||||||
})
|
.fold(self, |query, value| bind_value!(query, value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -675,7 +700,6 @@ impl<'q, O> BindValues<'q> for QueryScalar<'q, DB, O, Arguments_<'q>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! values {
|
macro_rules! values {
|
||||||
() => {
|
() => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue