Rename to sqlx-record with multi-database support
- Renamed project from entity-changes to sqlx-record - Renamed derive crate from entity-update_derive to sqlx-record-derive - Renamed CLI tool from entity-changes-ctl to sqlx-record-ctl - Renamed Condition to Filter (condition.rs -> filter.rs) - Renamed condition_or/condition_and/conditions macros to filter_or/filter_and/filters - Added multi-database support via feature flags: - mysql: MySQL support - postgres: PostgreSQL support (with $1, $2 placeholders) - sqlite: SQLite support - Updated query generation for database-specific syntax: - Placeholder styles (? vs $1) - Table quoting (` vs ") - COUNT expressions - ILIKE handling - Version increment (IF vs CASE WHEN) - Updated CLI tool for all three databases - Updated documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
e9079834c7
commit
aba05b2b52
45
CLAUDE.md
45
CLAUDE.md
|
|
@ -4,51 +4,59 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
This is a Rust library called `entity-changes` that provides entity change tracking functionality for MySQL databases using SQLx. The library supports tracking entity modifications with actors, sessions, and change sets, along with procedural macros for automatic derivation of entity tracking capabilities.
|
`sqlx-record` is a Rust library that provides derive macros for automatic CRUD operations and comprehensive audit trails for SQL entities. It supports MySQL, PostgreSQL, and SQLite via SQLx, tracking who changed what, when, and why with actor, session, and change set metadata.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
### Workspace Structure
|
### Workspace Structure
|
||||||
- **Main library** (`src/`): Core entity change tracking functionality
|
- **Main library** (`src/`): Core entity CRUD and change tracking functionality
|
||||||
- **entity-update_derive** (`entity-update_derive/`): Procedural macro crate for deriving Entity and Update traits
|
- **sqlx-record-derive** (`sqlx-record-derive/`): Procedural macro crate for deriving Entity and Update traits
|
||||||
- **entity-changes-ctl** (`entity-changes-ctl/`): Command-line utility for the library
|
- **sqlx-record-ctl** (`sqlx-record-ctl/`): Command-line utility for managing audit tables
|
||||||
|
|
||||||
### Core Components
|
### Core Components
|
||||||
- **models.rs**: Defines `EntityChange` struct and `Action` enum for tracking entity modifications
|
- **models.rs**: Defines `EntityChange` struct and `Action` enum for tracking entity modifications
|
||||||
- **repositories.rs**: Database query functions for creating and retrieving entity changes by various criteria (ID, entity, session, actor, change set)
|
- **repositories.rs**: Database query functions for creating and retrieving entity changes by various criteria (ID, entity, session, actor, change set)
|
||||||
- **value.rs**: Type-safe value system supporting MySQL types (integers, strings, UUIDs, dates, etc.) with `Value` enum and `Updater` for SQL operations
|
- **value.rs**: Type-safe value system supporting SQL types (integers, strings, UUIDs, dates, etc.) with `Value` enum and `Updater` for SQL operations
|
||||||
- **condition.rs**: Query condition system with `Condition` enum supporting SQL operations (Equal, Like, In, And, Or, etc.)
|
- **filter.rs**: Query filter system with `Filter` enum supporting SQL operations (Equal, Like, In, And, Or, etc.)
|
||||||
- **helpers.rs**: Utility functions and macros for the library
|
- **helpers.rs**: Utility functions and macros for the library
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
- `derive`: Enables procedural macro support for Entity and Update traits
|
- `derive`: Enables procedural macro support for Entity and Update traits
|
||||||
- `static-validation`: Enables static SQLx validation during compilation
|
- `static-validation`: Enables static SQLx validation during compilation
|
||||||
|
- `mysql`: MySQL database support
|
||||||
|
- `postgres`: PostgreSQL database support
|
||||||
|
- `sqlite`: SQLite database support
|
||||||
|
|
||||||
## Development Commands
|
## Development Commands
|
||||||
|
|
||||||
### Building
|
### Building
|
||||||
```bash
|
```bash
|
||||||
cargo build
|
# Build with MySQL support (default for backwards compatibility)
|
||||||
|
cargo build --features mysql
|
||||||
|
|
||||||
|
# Build with PostgreSQL support
|
||||||
|
cargo build --features postgres
|
||||||
|
|
||||||
|
# Build with SQLite support
|
||||||
|
cargo build --features sqlite
|
||||||
|
|
||||||
|
# Build with derive macros
|
||||||
|
cargo build --features "mysql,derive"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
```bash
|
```bash
|
||||||
cargo test
|
cargo test --features mysql
|
||||||
```
|
|
||||||
|
|
||||||
### Building with all features
|
|
||||||
```bash
|
|
||||||
cargo build --all-features
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Working with workspace members
|
### Working with workspace members
|
||||||
```bash
|
```bash
|
||||||
# Build specific workspace member
|
# Build specific workspace member
|
||||||
cargo build -p entity-update_derive
|
cargo build -p sqlx-record-derive --features mysql
|
||||||
cargo build -p entity-changes-ctl
|
cargo build -p sqlx-record-ctl --features mysql
|
||||||
|
|
||||||
# Test specific workspace member
|
# Test specific workspace member
|
||||||
cargo test -p entity-changes
|
cargo test -p sqlx-record --features mysql
|
||||||
```
|
```
|
||||||
|
|
||||||
### Releasing
|
### Releasing
|
||||||
|
|
@ -60,9 +68,10 @@ This creates a git tag based on the version in Cargo.toml and pushes it to the r
|
||||||
|
|
||||||
## Important Notes
|
## Important Notes
|
||||||
|
|
||||||
- The library is designed specifically for MySQL databases via SQLx
|
- The library supports MySQL, PostgreSQL, and SQLite databases via SQLx feature flags
|
||||||
- All entity changes include metadata: actor_id, session_id, change_set_id, and timestamps
|
- All entity changes include metadata: actor_id, session_id, change_set_id, and timestamps
|
||||||
- The `new_value` field stores JSON data for tracking actual changes
|
- The `new_value` field stores JSON data for tracking actual changes
|
||||||
- Procedural macros are optional and gated behind the "derive" feature
|
- Procedural macros are optional and gated behind the "derive" feature
|
||||||
- The library uses UUIDs for all entity identifiers
|
- The library uses UUIDs for all entity identifiers
|
||||||
- Query conditions support both simple field-value pairs and complex nested And/Or logic
|
- Query filters support both simple field-value pairs and complex nested And/Or logic
|
||||||
|
- PostgreSQL uses `$1, $2, ...` placeholders; MySQL/SQLite use `?`
|
||||||
|
|
|
||||||
22
Cargo.toml
22
Cargo.toml
|
|
@ -1,21 +1,27 @@
|
||||||
[package]
|
[package]
|
||||||
name = "entity-changes"
|
name = "sqlx-record"
|
||||||
version = "0.1.37"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
description = "Entity CRUD and change tracking for SQL databases with SQLx"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
entity-update_derive = { path = "entity-update_derive", optional = true }
|
sqlx-record-derive = { path = "sqlx-record-derive", optional = true }
|
||||||
sqlx = { version = "0.8", features = ["runtime-tokio", "mysql", "uuid", "chrono", "json"] }
|
sqlx = { version = "0.8", features = ["runtime-tokio", "uuid", "chrono", "json"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
uuid = { version = "1", features = ["v4"]}
|
uuid = { version = "1", features = ["v4"]}
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"entity-update_derive",
|
"sqlx-record-derive",
|
||||||
"entity-changes-ctl"
|
"sqlx-record-ctl"
|
||||||
]
|
]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
derive = ["dep:entity-update_derive"]
|
derive = ["dep:sqlx-record-derive"]
|
||||||
static-validation = ["entity-update_derive?/static-validation"]
|
static-validation = ["sqlx-record-derive?/static-validation"]
|
||||||
|
|
||||||
|
# Database backends - user must enable at least one
|
||||||
|
mysql = ["sqlx/mysql", "sqlx-record-derive?/mysql"]
|
||||||
|
postgres = ["sqlx/postgres", "sqlx-record-derive?/postgres"]
|
||||||
|
sqlite = ["sqlx/sqlite", "sqlx-record-derive?/sqlite"]
|
||||||
|
|
|
||||||
61
README.md
61
README.md
|
|
@ -0,0 +1,61 @@
|
||||||
|
# sqlx-record
|
||||||
|
|
||||||
|
Entity CRUD and change tracking for SQL databases with SQLx.
|
||||||
|
|
||||||
|
A Rust library that provides derive macros for automatic CRUD operations and comprehensive audit trails for SQL entities. Track who changed what, when, and why with actor, session, and change set metadata.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- `#[derive(Entity)]` generates complete CRUD operations
|
||||||
|
- Type-safe query building with composable filters
|
||||||
|
- Change tracking with WHO, WHAT, WHEN, and WHERE metadata
|
||||||
|
- Version fields for optimistic locking
|
||||||
|
- Diff detection between model states
|
||||||
|
- CLI tool for managing audit tables
|
||||||
|
- Supports MySQL, PostgreSQL, and SQLite
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Add to your `Cargo.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
sqlx-record = { version = "0.1", features = ["mysql", "derive"] }
|
||||||
|
# or for PostgreSQL:
|
||||||
|
# sqlx-record = { version = "0.1", features = ["postgres", "derive"] }
|
||||||
|
# or for SQLite:
|
||||||
|
# sqlx-record = { version = "0.1", features = ["sqlite", "derive"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use sqlx_record::prelude::*;
|
||||||
|
use sqlx::FromRow;
|
||||||
|
|
||||||
|
#[derive(Entity, FromRow)]
|
||||||
|
#[table_name = "users"]
|
||||||
|
struct User {
|
||||||
|
#[primary_key]
|
||||||
|
id: Uuid,
|
||||||
|
name: String,
|
||||||
|
email: String,
|
||||||
|
#[version]
|
||||||
|
version: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert
|
||||||
|
let id = user.insert(&pool).await?;
|
||||||
|
|
||||||
|
// Query with filters
|
||||||
|
let users = User::find(&pool, filters![("active", true)], None).await?;
|
||||||
|
|
||||||
|
// Update with diff tracking
|
||||||
|
let mut form = User::update_form().with_name("New Name".into());
|
||||||
|
let diff = form.db_diff(&id, &pool).await?;
|
||||||
|
user.update(&pool, form).await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,127 +0,0 @@
|
||||||
use clap::Parser;
|
|
||||||
use dotenv::dotenv;
|
|
||||||
use sqlx::{mysql::MySqlPool, Row};
|
|
||||||
use std::env;
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
/// Command line arguments
|
|
||||||
#[derive(Parser, Debug)]
|
|
||||||
#[command(author, version, about, long_about = None)]
|
|
||||||
struct Args {
|
|
||||||
/// The name of the schema to operate on
|
|
||||||
#[arg(long, short)]
|
|
||||||
schema_name: Option<String>,
|
|
||||||
|
|
||||||
/// The database URL, optionally provided. Defaults to the DATABASE_URL environment variable.
|
|
||||||
#[arg(long, short)]
|
|
||||||
db_url: Option<String>,
|
|
||||||
|
|
||||||
#[arg(long, short)]
|
|
||||||
env: Option<String>,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
delete: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() -> Result<(), sqlx::Error> {
|
|
||||||
// Parse command-line arguments
|
|
||||||
let args = Args::parse();
|
|
||||||
|
|
||||||
// Load environment variables from .env file
|
|
||||||
if let Some(env) = args.env {
|
|
||||||
dotenv::from_filename(env).ok();
|
|
||||||
} else {
|
|
||||||
dotenv().ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use provided --db-url or fallback to the DATABASE_URL from .env
|
|
||||||
let database_url = args
|
|
||||||
.db_url
|
|
||||||
.unwrap_or_else(|| env::var("DATABASE_URL").expect("DATABASE_URL must be set"));
|
|
||||||
|
|
||||||
// Connect to the TiDB database
|
|
||||||
let pool = MySqlPool::connect(&database_url).await?;
|
|
||||||
|
|
||||||
// Use the schema name from command line arguments
|
|
||||||
let schema_name = args.schema_name.unwrap_or_else(|| {
|
|
||||||
let parsed_url = Url::parse(&database_url).expect("Failed to parse database URL");
|
|
||||||
parsed_url
|
|
||||||
.path_segments()
|
|
||||||
.and_then(|segments| segments.last())
|
|
||||||
.map(|db_name| db_name.to_string())
|
|
||||||
.expect("No schema (database) name found in the database URL")
|
|
||||||
});
|
|
||||||
|
|
||||||
// Find all tables with 'actor_id' and 'session_id' columns in the specified schema,
|
|
||||||
// excluding tables that start with 'entity_changes_'
|
|
||||||
let tables: Vec<String> = sqlx::query(
|
|
||||||
"SELECT table_name FROM entity_changes_metadata WHERE is_auditable = TRUE"
|
|
||||||
)
|
|
||||||
.fetch_all(&pool)
|
|
||||||
.await?
|
|
||||||
.iter()
|
|
||||||
.map(|row| row.get::<String, _>("table_name"))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Iterate over each table and create an entity_changes table and triggers
|
|
||||||
for table_name in tables {
|
|
||||||
// Create the corresponding entity_changes table
|
|
||||||
println!("table_name: {}", table_name);
|
|
||||||
if args.delete {
|
|
||||||
let entity_changes_table = format!("entity_changes_{}", table_name);
|
|
||||||
println!("delete table: {}", entity_changes_table);
|
|
||||||
sqlx::query(&format!(
|
|
||||||
"DROP TABLE IF EXISTS {}.{}",
|
|
||||||
schema_name, entity_changes_table
|
|
||||||
)).execute(&pool).await?;
|
|
||||||
} else {
|
|
||||||
let entity_changes_table = format!("entity_changes_{}", table_name);
|
|
||||||
println!("create table: {}", entity_changes_table);
|
|
||||||
sqlx::query(&format!(
|
|
||||||
"CREATE TABLE IF NOT EXISTS {}.{} (
|
|
||||||
id BINARY(16) PRIMARY KEY /* T! CLUSTERED */,
|
|
||||||
entity_id BINARY(16) NOT NULL,
|
|
||||||
action ENUM('insert', 'update', 'delete', 'restore', 'hard-delete') NOT NULL,
|
|
||||||
changed_at BIGINT NOT NULL,
|
|
||||||
actor_id BINARY(16) NOT NULL,
|
|
||||||
session_id BINARY(16) NOT NULL,
|
|
||||||
change_set_id BINARY(16) NOT NULL,
|
|
||||||
new_value JSON
|
|
||||||
);",
|
|
||||||
schema_name, entity_changes_table,
|
|
||||||
)).execute(&pool).await?;
|
|
||||||
|
|
||||||
println!("create index _ entity_id");
|
|
||||||
sqlx::query(&format!(
|
|
||||||
"CREATE INDEX IF NOT EXISTS idx_{}_entity_id ON {} (entity_id);",
|
|
||||||
entity_changes_table, entity_changes_table,
|
|
||||||
)).execute(&pool).await?;
|
|
||||||
|
|
||||||
println!("create index _ change_set_id");
|
|
||||||
sqlx::query(&format!(
|
|
||||||
"CREATE INDEX IF NOT EXISTS idx_{}_change_set_id ON {} (change_set_id);",
|
|
||||||
entity_changes_table, entity_changes_table,
|
|
||||||
)).execute(&pool).await?;
|
|
||||||
|
|
||||||
println!("create index _ session_id");
|
|
||||||
sqlx::query(&format!(
|
|
||||||
"CREATE INDEX IF NOT EXISTS idx_{}_session_id ON {} (session_id);",
|
|
||||||
entity_changes_table, entity_changes_table,
|
|
||||||
)).execute(&pool).await?;
|
|
||||||
|
|
||||||
println!("create index _ actor_id");
|
|
||||||
sqlx::query(&format!(
|
|
||||||
"CREATE INDEX IF NOT EXISTS idx_{}_actor_id ON {} (actor_id);",
|
|
||||||
entity_changes_table, entity_changes_table,
|
|
||||||
)).execute(&pool).await?;
|
|
||||||
|
|
||||||
println!("create index _ entity_id_actor_id");
|
|
||||||
sqlx::query(&format!(
|
|
||||||
"CREATE INDEX IF NOT EXISTS idx_{}_entity_id_actor_id ON {} (entity_id, actor_id);",
|
|
||||||
entity_changes_table, entity_changes_table,
|
|
||||||
)).execute(&pool).await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,13 +1,20 @@
|
||||||
[package]
|
[package]
|
||||||
name = "entity-changes-ctl"
|
name = "sqlx-record-ctl"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
description = "CLI tool for managing sqlx-record audit tables"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
uuid = { version = "1", features = ["v4"] }
|
uuid = { version = "1", features = ["v4"] }
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
dotenv = "0.15"
|
dotenv = "0.15"
|
||||||
sqlx = { version = "0.8", features = ["mysql", "runtime-tokio-native-tls"] }
|
sqlx = { version = "0.8", features = ["runtime-tokio-native-tls"] }
|
||||||
tokio = { version = "1", features = ["rt", "macros", "time", "net", "rt-multi-thread"] }
|
tokio = { version = "1", features = ["rt", "macros", "time", "net", "rt-multi-thread"] }
|
||||||
clap = { version = "4.1", features = ["derive"] }
|
clap = { version = "4.1", features = ["derive"] }
|
||||||
url = "2"
|
url = "2"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["mysql"]
|
||||||
|
mysql = ["sqlx/mysql"]
|
||||||
|
postgres = ["sqlx/postgres"]
|
||||||
|
sqlite = ["sqlx/sqlite"]
|
||||||
|
|
@ -0,0 +1,230 @@
|
||||||
|
use clap::Parser;
|
||||||
|
use dotenv::dotenv;
|
||||||
|
use sqlx::Row;
|
||||||
|
use std::env;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[cfg(feature = "mysql")]
|
||||||
|
use sqlx::mysql::MySqlPool as Pool;
|
||||||
|
|
||||||
|
#[cfg(feature = "postgres")]
|
||||||
|
use sqlx::postgres::PgPool as Pool;
|
||||||
|
|
||||||
|
#[cfg(feature = "sqlite")]
|
||||||
|
use sqlx::sqlite::SqlitePool as Pool;
|
||||||
|
|
||||||
|
/// Command line arguments
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(author, version, about = "CLI tool for managing sqlx-record audit tables", long_about = None)]
|
||||||
|
struct Args {
|
||||||
|
/// The name of the schema to operate on
|
||||||
|
#[arg(long, short)]
|
||||||
|
schema_name: Option<String>,
|
||||||
|
|
||||||
|
/// The database URL, optionally provided. Defaults to the DATABASE_URL environment variable.
|
||||||
|
#[arg(long, short)]
|
||||||
|
db_url: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long, short)]
|
||||||
|
env: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
delete: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), sqlx::Error> {
|
||||||
|
// Parse command-line arguments
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
// Load environment variables from .env file
|
||||||
|
if let Some(env) = args.env {
|
||||||
|
dotenv::from_filename(env).ok();
|
||||||
|
} else {
|
||||||
|
dotenv().ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use provided --db-url or fallback to the DATABASE_URL from .env
|
||||||
|
let database_url = args
|
||||||
|
.db_url
|
||||||
|
.unwrap_or_else(|| env::var("DATABASE_URL").expect("DATABASE_URL must be set"));
|
||||||
|
|
||||||
|
// Connect to the database
|
||||||
|
let pool = Pool::connect(&database_url).await?;
|
||||||
|
|
||||||
|
// Use the schema name from command line arguments
|
||||||
|
let schema_name = args.schema_name.unwrap_or_else(|| {
|
||||||
|
let parsed_url = Url::parse(&database_url).expect("Failed to parse database URL");
|
||||||
|
parsed_url
|
||||||
|
.path_segments()
|
||||||
|
.and_then(|segments| segments.last())
|
||||||
|
.map(|db_name| db_name.to_string())
|
||||||
|
.expect("No schema (database) name found in the database URL")
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find all tables marked as auditable in the metadata table
|
||||||
|
let tables: Vec<String> = sqlx::query(
|
||||||
|
"SELECT table_name FROM entity_changes_metadata WHERE is_auditable = TRUE"
|
||||||
|
)
|
||||||
|
.fetch_all(&pool)
|
||||||
|
.await?
|
||||||
|
.iter()
|
||||||
|
.map(|row| row.get::<String, _>("table_name"))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Iterate over each table and create/delete an entity_changes table
|
||||||
|
for table_name in tables {
|
||||||
|
println!("table_name: {}", table_name);
|
||||||
|
let entity_changes_table = format!("entity_changes_{}", table_name);
|
||||||
|
|
||||||
|
if args.delete {
|
||||||
|
println!("delete table: {}", entity_changes_table);
|
||||||
|
|
||||||
|
#[cfg(feature = "mysql")]
|
||||||
|
let drop_stmt = format!("DROP TABLE IF EXISTS {}.{}", schema_name, entity_changes_table);
|
||||||
|
|
||||||
|
#[cfg(feature = "postgres")]
|
||||||
|
let drop_stmt = format!("DROP TABLE IF EXISTS \"{}\".\"{}\"", schema_name, entity_changes_table);
|
||||||
|
|
||||||
|
#[cfg(feature = "sqlite")]
|
||||||
|
let drop_stmt = format!("DROP TABLE IF EXISTS \"{}\"", entity_changes_table);
|
||||||
|
|
||||||
|
sqlx::query(&drop_stmt).execute(&pool).await?;
|
||||||
|
} else {
|
||||||
|
println!("create table: {}", entity_changes_table);
|
||||||
|
|
||||||
|
// Create table with database-specific syntax
|
||||||
|
#[cfg(feature = "mysql")]
|
||||||
|
{
|
||||||
|
sqlx::query(&format!(
|
||||||
|
"CREATE TABLE IF NOT EXISTS {}.{} (
|
||||||
|
id BINARY(16) PRIMARY KEY,
|
||||||
|
entity_id BINARY(16) NOT NULL,
|
||||||
|
action ENUM('insert', 'update', 'delete', 'restore', 'hard-delete') NOT NULL,
|
||||||
|
changed_at BIGINT NOT NULL,
|
||||||
|
actor_id BINARY(16) NOT NULL,
|
||||||
|
session_id BINARY(16) NOT NULL,
|
||||||
|
change_set_id BINARY(16) NOT NULL,
|
||||||
|
new_value JSON
|
||||||
|
);",
|
||||||
|
schema_name, entity_changes_table,
|
||||||
|
)).execute(&pool).await?;
|
||||||
|
|
||||||
|
// Create indexes
|
||||||
|
sqlx::query(&format!(
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_{}_entity_id ON {}.{} (entity_id);",
|
||||||
|
entity_changes_table, schema_name, entity_changes_table,
|
||||||
|
)).execute(&pool).await?;
|
||||||
|
|
||||||
|
sqlx::query(&format!(
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_{}_change_set_id ON {}.{} (change_set_id);",
|
||||||
|
entity_changes_table, schema_name, entity_changes_table,
|
||||||
|
)).execute(&pool).await?;
|
||||||
|
|
||||||
|
sqlx::query(&format!(
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_{}_session_id ON {}.{} (session_id);",
|
||||||
|
entity_changes_table, schema_name, entity_changes_table,
|
||||||
|
)).execute(&pool).await?;
|
||||||
|
|
||||||
|
sqlx::query(&format!(
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_{}_actor_id ON {}.{} (actor_id);",
|
||||||
|
entity_changes_table, schema_name, entity_changes_table,
|
||||||
|
)).execute(&pool).await?;
|
||||||
|
|
||||||
|
sqlx::query(&format!(
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_{}_entity_id_actor_id ON {}.{} (entity_id, actor_id);",
|
||||||
|
entity_changes_table, schema_name, entity_changes_table,
|
||||||
|
)).execute(&pool).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "postgres")]
|
||||||
|
{
|
||||||
|
sqlx::query(&format!(
|
||||||
|
r#"CREATE TABLE IF NOT EXISTS "{}"."{}" (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
entity_id UUID NOT NULL,
|
||||||
|
action VARCHAR(20) NOT NULL CHECK (action IN ('insert', 'update', 'delete', 'restore', 'hard-delete')),
|
||||||
|
changed_at BIGINT NOT NULL,
|
||||||
|
actor_id UUID NOT NULL,
|
||||||
|
session_id UUID NOT NULL,
|
||||||
|
change_set_id UUID NOT NULL,
|
||||||
|
new_value JSONB
|
||||||
|
);"#,
|
||||||
|
schema_name, entity_changes_table,
|
||||||
|
)).execute(&pool).await?;
|
||||||
|
|
||||||
|
// Create indexes
|
||||||
|
sqlx::query(&format!(
|
||||||
|
r#"CREATE INDEX IF NOT EXISTS idx_{}_entity_id ON "{}"."{}" (entity_id);"#,
|
||||||
|
entity_changes_table, schema_name, entity_changes_table,
|
||||||
|
)).execute(&pool).await?;
|
||||||
|
|
||||||
|
sqlx::query(&format!(
|
||||||
|
r#"CREATE INDEX IF NOT EXISTS idx_{}_change_set_id ON "{}"."{}" (change_set_id);"#,
|
||||||
|
entity_changes_table, schema_name, entity_changes_table,
|
||||||
|
)).execute(&pool).await?;
|
||||||
|
|
||||||
|
sqlx::query(&format!(
|
||||||
|
r#"CREATE INDEX IF NOT EXISTS idx_{}_session_id ON "{}"."{}" (session_id);"#,
|
||||||
|
entity_changes_table, schema_name, entity_changes_table,
|
||||||
|
)).execute(&pool).await?;
|
||||||
|
|
||||||
|
sqlx::query(&format!(
|
||||||
|
r#"CREATE INDEX IF NOT EXISTS idx_{}_actor_id ON "{}"."{}" (actor_id);"#,
|
||||||
|
entity_changes_table, schema_name, entity_changes_table,
|
||||||
|
)).execute(&pool).await?;
|
||||||
|
|
||||||
|
sqlx::query(&format!(
|
||||||
|
r#"CREATE INDEX IF NOT EXISTS idx_{}_entity_id_actor_id ON "{}"."{}" (entity_id, actor_id);"#,
|
||||||
|
entity_changes_table, schema_name, entity_changes_table,
|
||||||
|
)).execute(&pool).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "sqlite")]
|
||||||
|
{
|
||||||
|
sqlx::query(&format!(
|
||||||
|
r#"CREATE TABLE IF NOT EXISTS "{}" (
|
||||||
|
id BLOB PRIMARY KEY,
|
||||||
|
entity_id BLOB NOT NULL,
|
||||||
|
action TEXT NOT NULL CHECK (action IN ('insert', 'update', 'delete', 'restore', 'hard-delete')),
|
||||||
|
changed_at INTEGER NOT NULL,
|
||||||
|
actor_id BLOB NOT NULL,
|
||||||
|
session_id BLOB NOT NULL,
|
||||||
|
change_set_id BLOB NOT NULL,
|
||||||
|
new_value TEXT
|
||||||
|
);"#,
|
||||||
|
entity_changes_table,
|
||||||
|
)).execute(&pool).await?;
|
||||||
|
|
||||||
|
// Create indexes
|
||||||
|
sqlx::query(&format!(
|
||||||
|
r#"CREATE INDEX IF NOT EXISTS idx_{}_entity_id ON "{}" (entity_id);"#,
|
||||||
|
entity_changes_table, entity_changes_table,
|
||||||
|
)).execute(&pool).await?;
|
||||||
|
|
||||||
|
sqlx::query(&format!(
|
||||||
|
r#"CREATE INDEX IF NOT EXISTS idx_{}_change_set_id ON "{}" (change_set_id);"#,
|
||||||
|
entity_changes_table, entity_changes_table,
|
||||||
|
)).execute(&pool).await?;
|
||||||
|
|
||||||
|
sqlx::query(&format!(
|
||||||
|
r#"CREATE INDEX IF NOT EXISTS idx_{}_session_id ON "{}" (session_id);"#,
|
||||||
|
entity_changes_table, entity_changes_table,
|
||||||
|
)).execute(&pool).await?;
|
||||||
|
|
||||||
|
sqlx::query(&format!(
|
||||||
|
r#"CREATE INDEX IF NOT EXISTS idx_{}_actor_id ON "{}" (actor_id);"#,
|
||||||
|
entity_changes_table, entity_changes_table,
|
||||||
|
)).execute(&pool).await?;
|
||||||
|
|
||||||
|
sqlx::query(&format!(
|
||||||
|
r#"CREATE INDEX IF NOT EXISTS idx_{}_entity_id_actor_id ON "{}" (entity_id, actor_id);"#,
|
||||||
|
entity_changes_table, entity_changes_table,
|
||||||
|
)).execute(&pool).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Created indexes for {}", entity_changes_table);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
[package]
|
[package]
|
||||||
name = "entity-update_derive"
|
name = "sqlx-record-derive"
|
||||||
version = "0.1.6"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
description = "Derive macros for sqlx-record"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
syn = "2.0"
|
syn = "2.0"
|
||||||
|
|
@ -14,6 +14,9 @@ futures = "0.3"
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
static-validation = []
|
static-validation = []
|
||||||
|
mysql = []
|
||||||
|
postgres = []
|
||||||
|
sqlite = []
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
proc-macro = true
|
proc-macro = true
|
||||||
|
|
@ -50,6 +50,59 @@ pub fn derive_update(input: TokenStream) -> TokenStream {
|
||||||
pub fn derive_entity(input: TokenStream) -> TokenStream {
|
pub fn derive_entity(input: TokenStream) -> TokenStream {
|
||||||
derive_entity_internal(input)
|
derive_entity_internal(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generate database-specific types based on features
|
||||||
|
fn db_type() -> TokenStream2 {
|
||||||
|
#[cfg(feature = "postgres")]
|
||||||
|
{
|
||||||
|
quote! { sqlx::Postgres }
|
||||||
|
}
|
||||||
|
#[cfg(feature = "sqlite")]
|
||||||
|
{
|
||||||
|
quote! { sqlx::Sqlite }
|
||||||
|
}
|
||||||
|
#[cfg(feature = "mysql")]
|
||||||
|
{
|
||||||
|
quote! { sqlx::MySql }
|
||||||
|
}
|
||||||
|
#[cfg(not(any(feature = "mysql", feature = "postgres", feature = "sqlite")))]
|
||||||
|
{
|
||||||
|
// Default to MySql for backwards compatibility
|
||||||
|
quote! { sqlx::MySql }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn db_arguments() -> TokenStream2 {
|
||||||
|
#[cfg(feature = "postgres")]
|
||||||
|
{
|
||||||
|
quote! { sqlx::postgres::PgArguments }
|
||||||
|
}
|
||||||
|
#[cfg(feature = "sqlite")]
|
||||||
|
{
|
||||||
|
quote! { sqlx::sqlite::SqliteArguments<'static> }
|
||||||
|
}
|
||||||
|
#[cfg(feature = "mysql")]
|
||||||
|
{
|
||||||
|
quote! { sqlx::mysql::MySqlArguments }
|
||||||
|
}
|
||||||
|
#[cfg(not(any(feature = "mysql", feature = "postgres", feature = "sqlite")))]
|
||||||
|
{
|
||||||
|
quote! { sqlx::mysql::MySqlArguments }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get table quote character
|
||||||
|
fn table_quote() -> &'static str {
|
||||||
|
#[cfg(feature = "postgres")]
|
||||||
|
{ "\"" }
|
||||||
|
#[cfg(feature = "sqlite")]
|
||||||
|
{ "\"" }
|
||||||
|
#[cfg(feature = "mysql")]
|
||||||
|
{ "`" }
|
||||||
|
#[cfg(not(any(feature = "mysql", feature = "postgres", feature = "sqlite")))]
|
||||||
|
{ "`" }
|
||||||
|
}
|
||||||
|
|
||||||
fn derive_entity_internal(input: TokenStream) -> TokenStream {
|
fn derive_entity_internal(input: TokenStream) -> TokenStream {
|
||||||
let input = parse_macro_input!(input as DeriveInput);
|
let input = parse_macro_input!(input as DeriveInput);
|
||||||
let name = &input.ident;
|
let name = &input.ident;
|
||||||
|
|
@ -191,36 +244,38 @@ fn generate_insert_impl(
|
||||||
where_clause: &Option<&WhereClause>,
|
where_clause: &Option<&WhereClause>,
|
||||||
) -> TokenStream2 {
|
) -> TokenStream2 {
|
||||||
let db_names: Vec<_> = fields.iter().map(|f| &f.db_name).collect();
|
let db_names: Vec<_> = fields.iter().map(|f| &f.db_name).collect();
|
||||||
|
let tq = table_quote();
|
||||||
let insert_stmt = format!(
|
let db = db_type();
|
||||||
"INSERT INTO `{}` ({}) VALUES ({})",
|
|
||||||
table_name,
|
|
||||||
db_names.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(", "),
|
|
||||||
std::iter::repeat("?").take(db_names.len()).collect::<Vec<_>>().join(", ")
|
|
||||||
);
|
|
||||||
|
|
||||||
let bindings: Vec<_> = fields.iter().map(|f| {
|
let bindings: Vec<_> = fields.iter().map(|f| {
|
||||||
let ident = &f.ident;
|
let ident = &f.ident;
|
||||||
/* match ident.to_string().as_str() {
|
|
||||||
"created_at" | "updated_at" if has_created_at || has_updated_at => {
|
|
||||||
quote! { chrono::Utc::now().timestamp_millis() }
|
|
||||||
}
|
|
||||||
_ => quote! { &self.#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;
|
||||||
|
|
||||||
// Rest of function remains the same
|
let field_count = db_names.len();
|
||||||
|
|
||||||
quote! {
|
quote! {
|
||||||
impl #impl_generics #name #ty_generics #where_clause {
|
impl #impl_generics #name #ty_generics #where_clause {
|
||||||
pub async fn insert<'a, E>(&self, executor: E) -> Result<#pk_type, sqlx::Error>
|
pub async fn insert<'a, E>(&self, executor: E) -> Result<#pk_type, sqlx::Error>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database=sqlx::MySql>,
|
E: sqlx::Executor<'a, Database=#db>,
|
||||||
{
|
{
|
||||||
let result = sqlx::query(#insert_stmt)
|
let placeholders: String = (1..=#field_count)
|
||||||
|
.map(|i| ::sqlx_record::prelude::placeholder(i))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
let insert_stmt = format!(
|
||||||
|
"INSERT INTO {}{}{} ({}) VALUES ({})",
|
||||||
|
#tq, #table_name, #tq,
|
||||||
|
vec![#(#db_names),*].join(", "),
|
||||||
|
placeholders
|
||||||
|
);
|
||||||
|
|
||||||
|
let result = sqlx::query(&insert_stmt)
|
||||||
#(.bind(#bindings))*
|
#(.bind(#bindings))*
|
||||||
.execute(executor)
|
.execute(executor)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
@ -285,12 +340,8 @@ fn generate_get_impl(
|
||||||
let pk_type = &primary_key.ty;
|
let pk_type = &primary_key.ty;
|
||||||
let pk_field_name = primary_key.ident.to_string();
|
let pk_field_name = primary_key.ident.to_string();
|
||||||
let pk_db_field_name = &primary_key.db_name;
|
let pk_db_field_name = &primary_key.db_name;
|
||||||
|
let tq = table_quote();
|
||||||
let select_stmt = format!(
|
let db = db_type();
|
||||||
r#"SELECT DISTINCT {} FROM `{}` WHERE {} = ?"#,
|
|
||||||
select_fields.clone().collect::<Vec<_>>().join(", "),
|
|
||||||
table_name, pk_db_field_name
|
|
||||||
);
|
|
||||||
|
|
||||||
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.iter()
|
||||||
|
|
@ -298,9 +349,6 @@ fn generate_get_impl(
|
||||||
.next()).collect::<Vec<_>>().join(", ");
|
.next()).collect::<Vec<_>>().join(", ");
|
||||||
|
|
||||||
let select_field_list = select_fields.clone().collect::<Vec<_>>();
|
let select_field_list = select_fields.clone().collect::<Vec<_>>();
|
||||||
// let field_list = fields.iter().map(|f| f.db_name.clone()).collect::<Vec<_>>();
|
|
||||||
// println!("cargo:warning=select_fields for {} -> : {:?}", name.to_string(), select_fields.clone().collect::<Vec<_>>().join(", "));
|
|
||||||
// println!("cargo:warning=select_stmt for {} -> : {:?}", name.to_string(), select_stmt);
|
|
||||||
|
|
||||||
// Generate the get_version function if version_field exists
|
// Generate the get_version function if version_field exists
|
||||||
let get_version_impl = if let Some(vfield) = version_field {
|
let get_version_impl = if let Some(vfield) = version_field {
|
||||||
|
|
@ -310,13 +358,14 @@ fn generate_get_impl(
|
||||||
quote! {
|
quote! {
|
||||||
pub async fn get_version<'a, E>(executor: E, #pk_field: &#pk_type) -> Result<Option<#version_field_type>, sqlx::Error>
|
pub async fn get_version<'a, E>(executor: E, #pk_field: &#pk_type) -> Result<Option<#version_field_type>, sqlx::Error>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database=sqlx::MySql>,
|
E: sqlx::Executor<'a, Database=#db>,
|
||||||
{
|
{
|
||||||
let query = format!(
|
let query = format!(
|
||||||
r#"SELECT DISTINCT {} FROM `{}` WHERE {} = ?"#,
|
r#"SELECT DISTINCT {} FROM {}{}{} WHERE {} = {}"#,
|
||||||
#version_db_name,
|
#version_db_name,
|
||||||
Self::table_name(),
|
#tq, Self::table_name(), #tq,
|
||||||
#pk_db_field_name
|
#pk_db_field_name,
|
||||||
|
::sqlx_record::prelude::placeholder(1)
|
||||||
);
|
);
|
||||||
|
|
||||||
let result = sqlx::query_scalar(&query)
|
let result = sqlx::query_scalar(&query)
|
||||||
|
|
@ -328,19 +377,22 @@ fn generate_get_impl(
|
||||||
|
|
||||||
pub async fn get_versions<'a, E>(executor: E, keys: &Vec<#pk_type>) -> Result<::std::collections::HashMap<#pk_type, #version_field_type>, sqlx::Error>
|
pub async fn get_versions<'a, E>(executor: E, keys: &Vec<#pk_type>) -> Result<::std::collections::HashMap<#pk_type, #version_field_type>, sqlx::Error>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database=sqlx::MySql>,
|
E: sqlx::Executor<'a, Database=#db>,
|
||||||
{
|
{
|
||||||
if keys.is_empty() {
|
if keys.is_empty() {
|
||||||
return Ok(::std::collections::HashMap::new());
|
return Ok(::std::collections::HashMap::new());
|
||||||
}
|
}
|
||||||
use sqlx::Row;
|
use sqlx::Row;
|
||||||
|
|
||||||
let placeholders = std::iter::repeat("?").take(keys.len()).collect::<Vec<_>>().join(",");
|
let placeholders: String = (1..=keys.len())
|
||||||
|
.map(|i| ::sqlx_record::prelude::placeholder(i))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(",");
|
||||||
let query = format!(
|
let query = format!(
|
||||||
r#"SELECT DISTINCT {}, {} FROM `{}` WHERE {} IN ({})"#,
|
r#"SELECT DISTINCT {}, {} FROM {}{}{} WHERE {} IN ({})"#,
|
||||||
#pk_db_field_name,
|
#pk_db_field_name,
|
||||||
#version_db_name,
|
#version_db_name,
|
||||||
Self::table_name(),
|
#tq, Self::table_name(), #tq,
|
||||||
#pk_db_field_name,
|
#pk_db_field_name,
|
||||||
placeholders
|
placeholders
|
||||||
);
|
);
|
||||||
|
|
@ -375,10 +427,15 @@ fn generate_get_impl(
|
||||||
let use_static_validation = cfg!(feature = "static-validation");
|
let use_static_validation = cfg!(feature = "static-validation");
|
||||||
|
|
||||||
let get_by_impl = if use_static_validation {
|
let get_by_impl = if use_static_validation {
|
||||||
|
let select_stmt = format!(
|
||||||
|
r#"SELECT DISTINCT {} FROM {}{}{} WHERE {} = $1"#,
|
||||||
|
select_fields.clone().collect::<Vec<_>>().join(", "),
|
||||||
|
tq, table_name, tq, pk_db_field_name
|
||||||
|
);
|
||||||
quote! {
|
quote! {
|
||||||
pub async fn #get_by_func<'a, E>(executor: E, #pk_field: &#pk_type) -> Result<Option<Self>, sqlx::Error>
|
pub async fn #get_by_func<'a, E>(executor: E, #pk_field: &#pk_type) -> Result<Option<Self>, sqlx::Error>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database=sqlx::MySql>,
|
E: sqlx::Executor<'a, Database=#db>,
|
||||||
{
|
{
|
||||||
let result = sqlx::query_as!(
|
let result = sqlx::query_as!(
|
||||||
Self,
|
Self,
|
||||||
|
|
@ -393,7 +450,7 @@ fn generate_get_impl(
|
||||||
|
|
||||||
pub async fn get_by_primary_key<'a, E>(executor: E, #pk_field: &#pk_type) -> Result<Option<Self>, sqlx::Error>
|
pub async fn get_by_primary_key<'a, E>(executor: E, #pk_field: &#pk_type) -> Result<Option<Self>, sqlx::Error>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database=sqlx::MySql>,
|
E: sqlx::Executor<'a, Database=#db>,
|
||||||
{
|
{
|
||||||
let result = sqlx::query_as!(
|
let result = sqlx::query_as!(
|
||||||
Self,
|
Self,
|
||||||
|
|
@ -408,18 +465,19 @@ fn generate_get_impl(
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let field_list = fields.iter().map(|f| f.db_name.clone()).collect::<Vec<_>>();
|
let field_list = fields.iter().map(|f| f.db_name.clone()).collect::<Vec<_>>();
|
||||||
let select_stmt = format!(
|
|
||||||
r#"SELECT DISTINCT {} FROM `{}` WHERE {} = ?"#,
|
|
||||||
field_list.join(","),
|
|
||||||
table_name, pk_db_field_name
|
|
||||||
);
|
|
||||||
|
|
||||||
quote! {
|
quote! {
|
||||||
pub async fn #get_by_func<'a, E>(executor: E, #pk_field: &#pk_type) -> Result<Option<Self>, sqlx::Error>
|
pub async fn #get_by_func<'a, E>(executor: E, #pk_field: &#pk_type) -> Result<Option<Self>, sqlx::Error>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database=sqlx::MySql>,
|
E: sqlx::Executor<'a, Database=#db>,
|
||||||
{
|
{
|
||||||
let result = sqlx::query_as::<_, Self>(#select_stmt)
|
let select_stmt = format!(
|
||||||
|
r#"SELECT DISTINCT {} FROM {}{}{} WHERE {} = {}"#,
|
||||||
|
vec![#(#field_list),*].join(","),
|
||||||
|
#tq, #table_name, #tq, #pk_db_field_name,
|
||||||
|
::sqlx_record::prelude::placeholder(1)
|
||||||
|
);
|
||||||
|
let result = sqlx::query_as::<_, Self>(&select_stmt)
|
||||||
.bind(#pk_field)
|
.bind(#pk_field)
|
||||||
.fetch_optional(executor)
|
.fetch_optional(executor)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
@ -429,9 +487,15 @@ fn generate_get_impl(
|
||||||
|
|
||||||
pub async fn get_by_primary_key<'a, E>(executor: E, #pk_field: &#pk_type) -> Result<Option<Self>, sqlx::Error>
|
pub async fn get_by_primary_key<'a, E>(executor: E, #pk_field: &#pk_type) -> Result<Option<Self>, sqlx::Error>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database=sqlx::MySql>,
|
E: sqlx::Executor<'a, Database=#db>,
|
||||||
{
|
{
|
||||||
let result = sqlx::query_as::<_, Self>(#select_stmt)
|
let select_stmt = format!(
|
||||||
|
r#"SELECT DISTINCT {} FROM {}{}{} WHERE {} = {}"#,
|
||||||
|
vec![#(#field_list),*].join(","),
|
||||||
|
#tq, #table_name, #tq, #pk_db_field_name,
|
||||||
|
::sqlx_record::prelude::placeholder(1)
|
||||||
|
);
|
||||||
|
let result = sqlx::query_as::<_, Self>(&select_stmt)
|
||||||
.bind(#pk_field)
|
.bind(#pk_field)
|
||||||
.fetch_optional(executor)
|
.fetch_optional(executor)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
@ -465,21 +529,21 @@ fn generate_get_impl(
|
||||||
|
|
||||||
pub async fn #multi_get_by_func<'a, E>(executor: E, ids: &[#pk_type]) -> Result<Vec<Self>, sqlx::Error>
|
pub async fn #multi_get_by_func<'a, E>(executor: E, ids: &[#pk_type]) -> Result<Vec<Self>, sqlx::Error>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database=sqlx::MySql>,
|
E: sqlx::Executor<'a, Database=#db>,
|
||||||
{
|
{
|
||||||
if ids.is_empty() {
|
if ids.is_empty() {
|
||||||
return Ok(vec![]);
|
return Ok(vec![]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let placeholders = (0..ids.len())
|
let placeholders: String = (1..=ids.len())
|
||||||
.map(|_| "?")
|
.map(|i| ::sqlx_record::prelude::placeholder(i))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(",");
|
.join(",");
|
||||||
|
|
||||||
let query = format!(
|
let query = format!(
|
||||||
r#"SELECT DISTINCT {} FROM `{}` WHERE {} IN ({})"#,
|
r#"SELECT DISTINCT {} FROM {}{}{} WHERE {} IN ({})"#,
|
||||||
#select_fields_str,
|
#select_fields_str,
|
||||||
Self::table_name(),
|
#tq, Self::table_name(), #tq,
|
||||||
#pk_db_field_name,
|
#pk_db_field_name,
|
||||||
placeholders
|
placeholders
|
||||||
);
|
);
|
||||||
|
|
@ -496,60 +560,64 @@ fn generate_get_impl(
|
||||||
|
|
||||||
pub async fn find<'a, E>(
|
pub async fn find<'a, E>(
|
||||||
executor: E,
|
executor: E,
|
||||||
conditions: Vec<::entity_changes::prelude::Condition<'a>>,
|
filters: Vec<::sqlx_record::prelude::Filter<'a>>,
|
||||||
index: Option<&str>,
|
index: Option<&str>,
|
||||||
) -> Result<Vec<Self>, sqlx::Error>
|
) -> Result<Vec<Self>, sqlx::Error>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database=sqlx::MySql>,
|
E: sqlx::Executor<'a, Database=#db>,
|
||||||
{
|
{
|
||||||
Self::find_ordered_with_limit(executor, conditions, index, Vec::new(), None).await
|
Self::find_ordered_with_limit(executor, filters, index, Vec::new(), None).await
|
||||||
}
|
}
|
||||||
pub async fn find_ordered<'a, E>(
|
pub async fn find_ordered<'a, E>(
|
||||||
executor: E,
|
executor: E,
|
||||||
conditions: Vec<::entity_changes::prelude::Condition<'a>>,
|
filters: Vec<::sqlx_record::prelude::Filter<'a>>,
|
||||||
index: Option<&str>,
|
index: Option<&str>,
|
||||||
order_by: Vec<(&str, bool)>,
|
order_by: Vec<(&str, bool)>,
|
||||||
) -> Result<Vec<Self>, sqlx::Error>
|
) -> Result<Vec<Self>, sqlx::Error>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database=sqlx::MySql>,
|
E: sqlx::Executor<'a, Database=#db>,
|
||||||
{
|
{
|
||||||
Self::find_ordered_with_limit(executor, conditions, index, order_by, None).await
|
Self::find_ordered_with_limit(executor, filters, index, order_by, None).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn find_one<'a, E>(
|
pub async fn find_one<'a, E>(
|
||||||
executor: E,
|
executor: E,
|
||||||
conditions: Vec<::entity_changes::prelude::Condition<'a>>,
|
filters: Vec<::sqlx_record::prelude::Filter<'a>>,
|
||||||
index: Option<&str>,
|
index: Option<&str>,
|
||||||
) -> Result<Option<Self>, sqlx::Error>
|
) -> Result<Option<Self>, sqlx::Error>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database=sqlx::MySql>,
|
E: sqlx::Executor<'a, Database=#db>,
|
||||||
{
|
{
|
||||||
let found = Self::find_ordered_with_limit(executor, conditions, index, Vec::new(), Some((0, 1))).await?;
|
let found = Self::find_ordered_with_limit(executor, filters, index, Vec::new(), Some((0, 1))).await?;
|
||||||
Ok(found.into_iter().next())
|
Ok(found.into_iter().next())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn find_ordered_with_limit<'a, E>(
|
pub async fn find_ordered_with_limit<'a, E>(
|
||||||
executor: E,
|
executor: E,
|
||||||
conditions: Vec<::entity_changes::prelude::Condition<'a>>,
|
filters: Vec<::sqlx_record::prelude::Filter<'a>>,
|
||||||
index: Option<&str>,
|
index: Option<&str>,
|
||||||
order_by: Vec<(&str, bool)>,
|
order_by: Vec<(&str, bool)>,
|
||||||
offset_limit: Option<(u32, u32)>,
|
offset_limit: Option<(u32, u32)>,
|
||||||
) -> Result<Vec<Self>, sqlx::Error>
|
) -> Result<Vec<Self>, sqlx::Error>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database=sqlx::MySql>,
|
E: sqlx::Executor<'a, Database=#db>,
|
||||||
{
|
{
|
||||||
use ::entity_changes::prelude::{Condition, Value, bind_as_values};
|
use ::sqlx_record::prelude::{Filter, Value, bind_as_values};
|
||||||
|
|
||||||
let (where_conditions, values) = Condition::build_where_clause(&conditions);
|
let (where_conditions, values) = Filter::build_where_clause(&filters);
|
||||||
let where_clause = if !where_conditions.is_empty() {
|
let where_clause = if !where_conditions.is_empty() {
|
||||||
format!("WHERE {}", where_conditions)
|
format!("WHERE {}", where_conditions)
|
||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Index hints are MySQL-specific
|
||||||
|
#[cfg(feature = "mysql")]
|
||||||
let index_clause = index
|
let index_clause = index
|
||||||
.map(|idx| format!("USE INDEX ({})", idx))
|
.map(|idx| format!("USE INDEX ({})", idx))
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
#[cfg(not(feature = "mysql"))]
|
||||||
|
let index_clause = { let _ = index; String::new() };
|
||||||
|
|
||||||
//Filter order_by fields to only those managed
|
//Filter order_by fields to only those managed
|
||||||
let fields = Self::select_fields().into_iter().collect::<::std::collections::HashSet<_>>();
|
let fields = Self::select_fields().into_iter().collect::<::std::collections::HashSet<_>>();
|
||||||
|
|
@ -568,16 +636,16 @@ fn generate_get_impl(
|
||||||
};
|
};
|
||||||
|
|
||||||
let query = format!(
|
let query = format!(
|
||||||
r#"SELECT DISTINCT {} FROM {} {} {} {} {}"#,
|
r#"SELECT DISTINCT {} FROM {}{}{} {} {} {} {}"#,
|
||||||
#select_fields_str,
|
#select_fields_str,
|
||||||
#table_name,
|
#tq, #table_name, #tq,
|
||||||
index_clause,
|
index_clause,
|
||||||
where_clause,
|
where_clause,
|
||||||
order_by_clause,
|
order_by_clause,
|
||||||
offset_limit.map(|(offset, limit)| format!("LIMIT {}, {}", offset, limit)).unwrap_or_default(),
|
offset_limit.map(|(offset, limit)| format!("LIMIT {} OFFSET {}", limit, offset)).unwrap_or_default(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut db_query = sqlx::query_as(&query);
|
let db_query = sqlx::query_as(&query);
|
||||||
|
|
||||||
// Bind values to the query
|
// Bind values to the query
|
||||||
let results = match bind_as_values(db_query, &values)
|
let results = match bind_as_values(db_query, &values)
|
||||||
|
|
@ -598,40 +666,54 @@ fn generate_get_impl(
|
||||||
|
|
||||||
pub async fn count<'a, E>(
|
pub async fn count<'a, E>(
|
||||||
executor: E,
|
executor: E,
|
||||||
conditions: Vec<::entity_changes::prelude::Condition<'a>>,
|
filters: Vec<::sqlx_record::prelude::Filter<'a>>,
|
||||||
index: Option<&str>,
|
index: Option<&str>,
|
||||||
) -> Result<u64, sqlx::Error>
|
) -> Result<u64, sqlx::Error>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database=sqlx::MySql>,
|
E: sqlx::Executor<'a, Database=#db>,
|
||||||
{
|
{
|
||||||
use ::entity_changes::prelude::{Condition, Value, bind_scalar_values};
|
use ::sqlx_record::prelude::{Filter, Value, bind_scalar_values};
|
||||||
|
|
||||||
let (where_conditions, values) = Condition::build_where_clause(&conditions);
|
let (where_conditions, values) = Filter::build_where_clause(&filters);
|
||||||
let where_clause = if !where_conditions.is_empty() {
|
let where_clause = if !where_conditions.is_empty() {
|
||||||
format!("WHERE {}", where_conditions)
|
format!("WHERE {}", where_conditions)
|
||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Index hints are MySQL-specific
|
||||||
|
#[cfg(feature = "mysql")]
|
||||||
let index_clause = index
|
let index_clause = index
|
||||||
.map(|idx| format!("USE INDEX ({})", idx))
|
.map(|idx| format!("USE INDEX ({})", idx))
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
#[cfg(not(feature = "mysql"))]
|
||||||
|
let index_clause = { let _ = index; String::new() };
|
||||||
|
|
||||||
|
// Use database-appropriate COUNT syntax
|
||||||
|
#[cfg(feature = "postgres")]
|
||||||
|
let count_expr = format!("COUNT({})::BIGINT", #pk_db_field_name);
|
||||||
|
#[cfg(feature = "sqlite")]
|
||||||
|
let count_expr = format!("COUNT({})", #pk_db_field_name);
|
||||||
|
#[cfg(feature = "mysql")]
|
||||||
|
let count_expr = format!("CAST(COUNT({}) AS SIGNED)", #pk_db_field_name);
|
||||||
|
#[cfg(not(any(feature = "mysql", feature = "postgres", feature = "sqlite")))]
|
||||||
|
let count_expr = format!("COUNT({})", #pk_db_field_name);
|
||||||
|
|
||||||
let query = format!(
|
let query = format!(
|
||||||
r#"SELECT CAST(COUNT({}) AS UNSIGNED) FROM {} {} {}"#,
|
r#"SELECT {} FROM {}{}{} {} {}"#,
|
||||||
#pk_db_field_name,
|
count_expr,
|
||||||
#table_name,
|
#tq, #table_name, #tq,
|
||||||
index_clause,
|
index_clause,
|
||||||
where_clause,
|
where_clause,
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut db_query = sqlx::query_scalar::<_, u64>(&query);
|
let db_query = sqlx::query_scalar::<_, i64>(&query);
|
||||||
|
|
||||||
// Bind values to the query
|
// Bind values to the query
|
||||||
let count = match bind_scalar_values(db_query, &values)
|
let count = match bind_scalar_values(db_query, &values)
|
||||||
.fetch_optional(executor)
|
.fetch_optional(executor)
|
||||||
.await {
|
.await {
|
||||||
Ok(count) => count.unwrap_or(0),
|
Ok(count) => count.unwrap_or(0) as u64,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
tracing::error!(r#"Error executing
|
tracing::error!(r#"Error executing
|
||||||
Query: {}
|
Query: {}
|
||||||
|
|
@ -665,6 +747,8 @@ fn generate_update_impl(
|
||||||
let field_idents: Vec<_> = update_fields.iter().map(|f| &f.ident).collect();
|
let field_idents: Vec<_> = update_fields.iter().map(|f| &f.ident).collect();
|
||||||
let field_types: Vec<_> = update_fields.iter().map(|f| &f.ty).collect();
|
let field_types: Vec<_> = update_fields.iter().map(|f| &f.ty).collect();
|
||||||
let db_names: Vec<_> = update_fields.iter().map(|f| &f.db_name).collect();
|
let db_names: Vec<_> = update_fields.iter().map(|f| &f.db_name).collect();
|
||||||
|
let db = db_type();
|
||||||
|
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);
|
||||||
|
|
@ -695,42 +779,27 @@ fn generate_update_impl(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// This code should replace the version_increment code in generate_update_impl
|
// Version increment - use CASE WHEN for cross-database compatibility
|
||||||
let version_increment = if let Some(vfield) = version_field {
|
let version_increment = if let Some(vfield) = version_field {
|
||||||
let version_db_name = &vfield.db_name;
|
let version_db_name = &vfield.db_name;
|
||||||
let version_type = &vfield.ty;
|
let version_type = &vfield.ty;
|
||||||
|
|
||||||
// Handle different integer types with appropriate max values for wrapping
|
// Handle different integer types with appropriate max values for wrapping
|
||||||
match version_type {
|
let max_val = match version_type {
|
||||||
Type::Path(type_path) if type_path.path.is_ident("u32") => {
|
Type::Path(type_path) if type_path.path.is_ident("u32") => "4294967295",
|
||||||
// u32 max: 4,294,967,295
|
Type::Path(type_path) if type_path.path.is_ident("u64") => "18446744073709551615",
|
||||||
quote! {
|
Type::Path(type_path) if type_path.path.is_ident("i32") => "2147483647",
|
||||||
parts.push(format!("{} = IF({} = 4294967295, 0, {} + 1)", #version_db_name, #version_db_name, #version_db_name));
|
Type::Path(type_path) if type_path.path.is_ident("i64") => "9223372036854775807",
|
||||||
}
|
_ => "",
|
||||||
},
|
};
|
||||||
Type::Path(type_path) if type_path.path.is_ident("u64") => {
|
|
||||||
// u64 max: 18,446,744,073,709,551,615
|
if max_val.is_empty() {
|
||||||
quote! {
|
|
||||||
parts.push(format!("{} = IF({} = 18446744073709551615, 0, {} + 1)", #version_db_name, #version_db_name, #version_db_name));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Type::Path(type_path) if type_path.path.is_ident("i32") => {
|
|
||||||
// i32 max: 2,147,483,647
|
|
||||||
quote! {
|
|
||||||
parts.push(format!("{} = IF({} = 2147483647, 0, {} + 1)", #version_db_name, #version_db_name, #version_db_name));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Type::Path(type_path) if type_path.path.is_ident("i64") => {
|
|
||||||
// i64 max: 9,223,372,036,854,775,807
|
|
||||||
quote! {
|
|
||||||
parts.push(format!("{} = IF({} = 9223372036854775807, 0, {} + 1)", #version_db_name, #version_db_name, #version_db_name));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ => {
|
|
||||||
// Default increment for any other type
|
|
||||||
quote! {
|
quote! {
|
||||||
parts.push(format!("{} = {} + 1", #version_db_name, #version_db_name));
|
parts.push(format!("{} = {} + 1", #version_db_name, #version_db_name));
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
quote! {
|
||||||
|
parts.push(format!("{} = CASE WHEN {} = {} THEN 0 ELSE {} + 1 END", #version_db_name, #version_db_name, #max_val, #version_db_name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -760,9 +829,11 @@ fn generate_update_impl(
|
||||||
|
|
||||||
pub fn update_stmt(&self) -> String {
|
pub fn update_stmt(&self) -> String {
|
||||||
let mut parts = Vec::new();
|
let mut parts = Vec::new();
|
||||||
|
let mut idx = 1usize;
|
||||||
#(
|
#(
|
||||||
if self.#field_idents.is_some() {
|
if self.#field_idents.is_some() {
|
||||||
parts.push(format!("{} = ?", #db_names));
|
parts.push(format!("{} = {}", #db_names, ::sqlx_record::prelude::placeholder(idx)));
|
||||||
|
idx += 1;
|
||||||
}
|
}
|
||||||
)*
|
)*
|
||||||
|
|
||||||
|
|
@ -771,8 +842,8 @@ fn generate_update_impl(
|
||||||
parts.join(", ")
|
parts.join(", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bind_form_values<'q>(&'q self, mut query: sqlx::query::Query<'q, sqlx::MySql, sqlx::mysql::MySqlArguments>)
|
pub fn bind_form_values<'q>(&'q self, mut query: sqlx::query::Query<'q, #db, #db_args>)
|
||||||
-> sqlx::query::Query<'q, sqlx::MySql, sqlx::mysql::MySqlArguments>
|
-> sqlx::query::Query<'q, #db, #db_args>
|
||||||
{
|
{
|
||||||
#(
|
#(
|
||||||
if let Some(ref value) = self.#field_idents {
|
if let Some(ref value) = self.#field_idents {
|
||||||
|
|
@ -818,6 +889,10 @@ fn generate_diff_impl(
|
||||||
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);
|
||||||
|
|
||||||
|
let db = db_type();
|
||||||
|
let db_args = db_arguments();
|
||||||
|
let tq = table_quote();
|
||||||
|
|
||||||
quote! {
|
quote! {
|
||||||
impl #impl_generics #update_form_name #ty_generics #where_clause {
|
impl #impl_generics #update_form_name #ty_generics #where_clause {
|
||||||
pub fn model_diff(&self, other: &#name #ty_generics) -> serde_json::Value {
|
pub fn model_diff(&self, other: &#name #ty_generics) -> serde_json::Value {
|
||||||
|
|
@ -846,7 +921,10 @@ fn generate_diff_impl(
|
||||||
serde_json::Value::Object(changes)
|
serde_json::Value::Object(changes)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn db_diff<'q>(&self, #pk_field: &#pk_type, executor: impl sqlx::MySqlExecutor<'q>) -> Result<serde_json::Value, sqlx::Error> {
|
pub async fn db_diff<'q, E>(&self, #pk_field: &#pk_type, executor: E) -> Result<serde_json::Value, sqlx::Error>
|
||||||
|
where
|
||||||
|
E: sqlx::Executor<'q, Database=#db>,
|
||||||
|
{
|
||||||
use sqlx::Row;
|
use sqlx::Row;
|
||||||
let fields_to_fetch: Vec<String> = vec![
|
let fields_to_fetch: Vec<String> = vec![
|
||||||
#(
|
#(
|
||||||
|
|
@ -863,9 +941,10 @@ fn generate_diff_impl(
|
||||||
}
|
}
|
||||||
|
|
||||||
let query = format!(
|
let query = format!(
|
||||||
"SELECT DISTINCT {} FROM {} WHERE {} = ?",
|
"SELECT DISTINCT {} FROM {}{}{} WHERE {} = {}",
|
||||||
fields_to_fetch.join(", "),
|
fields_to_fetch.join(", "),
|
||||||
Self::table_name(), #pk_db_name,
|
#tq, Self::table_name(), #tq, #pk_db_name,
|
||||||
|
::sqlx_record::prelude::placeholder(1)
|
||||||
);
|
);
|
||||||
|
|
||||||
let row = sqlx::query(&query)
|
let row = sqlx::query(&query)
|
||||||
|
|
@ -910,7 +989,7 @@ fn generate_diff_impl(
|
||||||
fn #bind_form_values_func(self, form: &'f #update_form_name) -> Self;
|
fn #bind_form_values_func(self, form: &'f #update_form_name) -> Self;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'q, 'f> #bind_form_values_trait<'q, 'f> for sqlx::query::Query<'q, sqlx::MySql, sqlx::mysql::MySqlArguments>
|
impl<'q, 'f> #bind_form_values_trait<'q, 'f> for sqlx::query::Query<'q, #db, #db_args>
|
||||||
where 'f: 'q {
|
where 'f: 'q {
|
||||||
fn #bind_form_values_func(self, form: &'f #update_form_name) -> Self {
|
fn #bind_form_values_func(self, form: &'f #update_form_name) -> Self {
|
||||||
form.bind_form_values(self)
|
form.bind_form_values(self)
|
||||||
|
|
@ -920,17 +999,18 @@ fn generate_diff_impl(
|
||||||
impl #impl_generics #name #ty_generics #where_clause {
|
impl #impl_generics #name #ty_generics #where_clause {
|
||||||
pub async fn update<'a, E>(&self, executor: E, form: #update_form_name) -> Result<(), sqlx::Error>
|
pub async fn update<'a, E>(&self, executor: E, form: #update_form_name) -> Result<(), sqlx::Error>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database=sqlx::MySql>,
|
E: sqlx::Executor<'a, Database=#db>,
|
||||||
{
|
{
|
||||||
|
// Count parameters in the update statement
|
||||||
|
let update_stmt = form.update_stmt();
|
||||||
|
let param_count = update_stmt.matches(::sqlx_record::prelude::placeholder(1).chars().next().unwrap_or('?')).count();
|
||||||
|
|
||||||
let query_str = format!(
|
let query_str = format!(
|
||||||
r#"
|
r#"UPDATE {}{}{} SET {} WHERE {} = {}"#,
|
||||||
UPDATE `{}`
|
#tq, Self::table_name(), #tq,
|
||||||
SET {}
|
update_stmt,
|
||||||
WHERE {} = ?
|
#pk_db_name,
|
||||||
"#,
|
::sqlx_record::prelude::placeholder(param_count + 1)
|
||||||
Self::table_name(),
|
|
||||||
form.update_stmt(),
|
|
||||||
#pk_db_name
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let _q = match sqlx::query(&query_str)
|
let _q = match sqlx::query(&query_str)
|
||||||
|
|
@ -952,17 +1032,18 @@ fn generate_diff_impl(
|
||||||
|
|
||||||
pub async fn #update_by_func<'a, E>(executor: E, #pk_field: &#pk_type, form: #update_form_name) -> Result<(), sqlx::Error>
|
pub async fn #update_by_func<'a, E>(executor: E, #pk_field: &#pk_type, form: #update_form_name) -> Result<(), sqlx::Error>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database=sqlx::MySql>,
|
E: sqlx::Executor<'a, Database=#db>,
|
||||||
{
|
{
|
||||||
|
// Count parameters in the update statement
|
||||||
|
let update_stmt = form.update_stmt();
|
||||||
|
let param_count = update_stmt.matches(::sqlx_record::prelude::placeholder(1).chars().next().unwrap_or('?')).count();
|
||||||
|
|
||||||
let query_str = format!(
|
let query_str = format!(
|
||||||
r#"
|
r#"UPDATE {}{}{} SET {} WHERE {} = {}"#,
|
||||||
UPDATE `{}`
|
#tq, Self::table_name(), #tq,
|
||||||
SET {}
|
update_stmt,
|
||||||
WHERE {} = ?
|
#pk_db_name,
|
||||||
"#,
|
::sqlx_record::prelude::placeholder(param_count + 1)
|
||||||
Self::table_name(),
|
|
||||||
form.update_stmt(),
|
|
||||||
#pk_db_name
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let _q = match sqlx::query(&query_str)
|
let _q = match sqlx::query(&query_str)
|
||||||
|
|
@ -984,25 +1065,23 @@ fn generate_diff_impl(
|
||||||
|
|
||||||
pub async fn #multi_update_by_func<'a, E>(executor: E, #multi_pk_field: &Vec<#pk_type>, form: #update_form_name) -> Result<(), sqlx::Error>
|
pub async fn #multi_update_by_func<'a, E>(executor: E, #multi_pk_field: &Vec<#pk_type>, form: #update_form_name) -> Result<(), sqlx::Error>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database=sqlx::MySql>,
|
E: sqlx::Executor<'a, Database=#db>,
|
||||||
{
|
{
|
||||||
if #multi_pk_field.is_empty() {
|
if #multi_pk_field.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let placeholders = (0..#multi_pk_field.len())
|
let update_stmt = form.update_stmt();
|
||||||
.map(|_| "?")
|
let form_param_count = update_stmt.matches(::sqlx_record::prelude::placeholder(1).chars().next().unwrap_or('?')).count();
|
||||||
|
let placeholders: String = (1..=#multi_pk_field.len())
|
||||||
|
.map(|i| ::sqlx_record::prelude::placeholder(form_param_count + i))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(",");
|
.join(",");
|
||||||
|
|
||||||
let query_str = format!(
|
let query_str = format!(
|
||||||
r#"
|
r#"UPDATE {}{}{} SET {} WHERE {} IN ({})"#,
|
||||||
UPDATE `{}`
|
#tq, Self::table_name(), #tq,
|
||||||
SET {}
|
update_stmt,
|
||||||
WHERE {} IN ({})
|
|
||||||
"#,
|
|
||||||
Self::table_name(),
|
|
||||||
form.update_stmt(),
|
|
||||||
#pk_db_name, placeholders,
|
#pk_db_name, placeholders,
|
||||||
);
|
);
|
||||||
|
|
||||||
176
src/condition.rs
176
src/condition.rs
|
|
@ -1,176 +0,0 @@
|
||||||
use crate::prelude::Value;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub enum Condition<'a> {
|
|
||||||
Equal(&'a str, Value),
|
|
||||||
NotEqual(&'a str, Value),
|
|
||||||
GreaterThan(&'a str, Value),
|
|
||||||
GreaterThanOrEqual(&'a str, Value),
|
|
||||||
LessThan(&'a str, Value),
|
|
||||||
LessThanOrEqual(&'a str, Value),
|
|
||||||
Like(&'a str, Value),
|
|
||||||
ILike(&'a str, Value),
|
|
||||||
NotLike(&'a str, Value),
|
|
||||||
In(&'a str, Vec<Value>),
|
|
||||||
NotIn(&'a str, Vec<Value>),
|
|
||||||
IsNull(&'a str),
|
|
||||||
IsNotNull(&'a str),
|
|
||||||
And(Vec<Condition<'a>>),
|
|
||||||
Or(Vec<Condition<'a>>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, T: Into<Value>> From<(&'a str, T)> for Condition<'a> {
|
|
||||||
fn from((field, value): (&'a str, T)) -> Self {
|
|
||||||
Condition::Equal(field, value.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! condition_or {
|
|
||||||
($x:expr) => (
|
|
||||||
$crate::prelude::Condition::Or(vec![<$crate::prelude::Condition<'_>>::from($x)])
|
|
||||||
);
|
|
||||||
($($x:expr),+ $(,)?) => (
|
|
||||||
$crate::prelude::Condition::Or(vec![$(<$crate::prelude::Condition<'_>>::from($x)),+])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! condition_and {
|
|
||||||
($x:expr) => (
|
|
||||||
$crate::prelude::Condition::And(vec![<$crate::prelude::Condition<'_>>::from($x)])
|
|
||||||
);
|
|
||||||
($($x:expr),+ $(,)?) => (
|
|
||||||
$crate::prelude::Condition::And(vec![$(<$crate::prelude::Condition<'_>>::from($x)),+])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! conditions {
|
|
||||||
() => {
|
|
||||||
vec![]
|
|
||||||
};
|
|
||||||
($x:expr) => {
|
|
||||||
vec![<$crate::prelude::Condition<'_>>::from($x)]
|
|
||||||
};
|
|
||||||
($($x:expr),+ $(,)?) => {
|
|
||||||
vec![$(<$crate::prelude::Condition<'_>>::from($x)),+]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait ConditionOps<Rhs = Self> {
|
|
||||||
type Output;
|
|
||||||
fn gt(self, rhs: Rhs) -> Self::Output;
|
|
||||||
fn ge(self, rhs: Rhs) -> Self::Output;
|
|
||||||
fn lt(self, rhs: Rhs) -> Self::Output;
|
|
||||||
fn le(self, rhs: Rhs) -> Self::Output;
|
|
||||||
fn eq(self, rhs: Rhs) -> Self::Output;
|
|
||||||
fn ne(self, rhs: Rhs) -> Self::Output;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For types that can be converted into Value
|
|
||||||
impl<T: Into<&'static str>, V: Into<Value>> ConditionOps<V> for T {
|
|
||||||
type Output = Condition<'static>;
|
|
||||||
|
|
||||||
fn gt(self, rhs: V) -> Self::Output {
|
|
||||||
Condition::GreaterThan(self.into(), rhs.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ge(self, rhs: V) -> Self::Output {
|
|
||||||
Condition::GreaterThanOrEqual(self.into(), rhs.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn lt(self, rhs: V) -> Self::Output {
|
|
||||||
Condition::LessThan(self.into(), rhs.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn le(self, rhs: V) -> Self::Output {
|
|
||||||
Condition::LessThanOrEqual(self.into(), rhs.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn eq(self, rhs: V) -> Self::Output {
|
|
||||||
Condition::Equal(self.into(), rhs.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ne(self, rhs: V) -> Self::Output {
|
|
||||||
Condition::NotEqual(self.into(), rhs.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
impl Condition<'_> {
|
|
||||||
pub fn build_where_clause(conditions: &[Condition]) -> (String, Vec<Value>) {
|
|
||||||
let mut values = Vec::new();
|
|
||||||
let conditions: Vec<String> = conditions
|
|
||||||
.iter()
|
|
||||||
.map(|condition| {
|
|
||||||
match condition {
|
|
||||||
Condition::Equal(field, value) => {
|
|
||||||
values.push(value.clone());
|
|
||||||
format!("{} = ?", field)
|
|
||||||
}
|
|
||||||
Condition::NotEqual(field, value) => {
|
|
||||||
values.push(value.clone());
|
|
||||||
format!("{} != ?", field)
|
|
||||||
}
|
|
||||||
Condition::GreaterThan(field, value) => {
|
|
||||||
values.push(value.clone());
|
|
||||||
format!("{} > ?", field)
|
|
||||||
}
|
|
||||||
Condition::GreaterThanOrEqual(field, value) => {
|
|
||||||
values.push(value.clone());
|
|
||||||
format!("{} >= ?", field)
|
|
||||||
}
|
|
||||||
Condition::LessThan(field, value) => {
|
|
||||||
values.push(value.clone());
|
|
||||||
format!("{} < ?", field)
|
|
||||||
}
|
|
||||||
Condition::LessThanOrEqual(field, value) => {
|
|
||||||
values.push(value.clone());
|
|
||||||
format!("{} <= ?", field)
|
|
||||||
}
|
|
||||||
Condition::Like(field, value) => {
|
|
||||||
values.push(value.clone());
|
|
||||||
format!("{} LIKE ?", field)
|
|
||||||
}
|
|
||||||
Condition::ILike(field, value) => {
|
|
||||||
values.push(value.clone());
|
|
||||||
format!("{} ILIKE ?", field)
|
|
||||||
}
|
|
||||||
Condition::NotLike(field, value) => {
|
|
||||||
values.push(value.clone());
|
|
||||||
format!("{} NOT LIKE ?", field)
|
|
||||||
}
|
|
||||||
Condition::In(field, value_vec) => {
|
|
||||||
let placeholders = vec!["?"; value_vec.len()].join(", ");
|
|
||||||
values.extend(value_vec.clone());
|
|
||||||
format!("{} IN ({})", field, placeholders)
|
|
||||||
}
|
|
||||||
Condition::NotIn(field, value_vec) => {
|
|
||||||
let placeholders = vec!["?"; value_vec.len()].join(", ");
|
|
||||||
values.extend(value_vec.clone());
|
|
||||||
format!("{} NOT IN ({})", field, placeholders)
|
|
||||||
}
|
|
||||||
Condition::IsNull(field) => {
|
|
||||||
format!("{} IS NULL", field)
|
|
||||||
}
|
|
||||||
Condition::IsNotNull(field) => {
|
|
||||||
format!("{} IS NOT NULL", field)
|
|
||||||
}
|
|
||||||
Condition::And(nested_conditions) => {
|
|
||||||
let (nested_clause, nested_values) = Self::build_where_clause(nested_conditions);
|
|
||||||
values.extend(nested_values);
|
|
||||||
format!("({})", nested_clause)
|
|
||||||
}
|
|
||||||
Condition::Or(nested_conditions) => {
|
|
||||||
let (nested_clause, nested_values) = Self::build_where_clause(nested_conditions);
|
|
||||||
values.extend(nested_values);
|
|
||||||
format!("({})", nested_clause.split(" AND ").collect::<Vec<_>>().join(" OR "))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
(conditions.join(" AND "), values)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,231 @@
|
||||||
|
use crate::prelude::Value;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum Filter<'a> {
|
||||||
|
Equal(&'a str, Value),
|
||||||
|
NotEqual(&'a str, Value),
|
||||||
|
GreaterThan(&'a str, Value),
|
||||||
|
GreaterThanOrEqual(&'a str, Value),
|
||||||
|
LessThan(&'a str, Value),
|
||||||
|
LessThanOrEqual(&'a str, Value),
|
||||||
|
Like(&'a str, Value),
|
||||||
|
ILike(&'a str, Value),
|
||||||
|
NotLike(&'a str, Value),
|
||||||
|
In(&'a str, Vec<Value>),
|
||||||
|
NotIn(&'a str, Vec<Value>),
|
||||||
|
IsNull(&'a str),
|
||||||
|
IsNotNull(&'a str),
|
||||||
|
And(Vec<Filter<'a>>),
|
||||||
|
Or(Vec<Filter<'a>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: Into<Value>> From<(&'a str, T)> for Filter<'a> {
|
||||||
|
fn from((field, value): (&'a str, T)) -> Self {
|
||||||
|
Filter::Equal(field, value.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! filter_or {
|
||||||
|
($x:expr) => (
|
||||||
|
$crate::prelude::Filter::Or(vec![<$crate::prelude::Filter<'_>>::from($x)])
|
||||||
|
);
|
||||||
|
($($x:expr),+ $(,)?) => (
|
||||||
|
$crate::prelude::Filter::Or(vec![$(<$crate::prelude::Filter<'_>>::from($x)),+])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! filter_and {
|
||||||
|
($x:expr) => (
|
||||||
|
$crate::prelude::Filter::And(vec![<$crate::prelude::Filter<'_>>::from($x)])
|
||||||
|
);
|
||||||
|
($($x:expr),+ $(,)?) => (
|
||||||
|
$crate::prelude::Filter::And(vec![$(<$crate::prelude::Filter<'_>>::from($x)),+])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! filters {
|
||||||
|
() => {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
($x:expr) => {
|
||||||
|
vec![<$crate::prelude::Filter<'_>>::from($x)]
|
||||||
|
};
|
||||||
|
($($x:expr),+ $(,)?) => {
|
||||||
|
vec![$(<$crate::prelude::Filter<'_>>::from($x)),+]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait FilterOps<Rhs = Self> {
|
||||||
|
type Output;
|
||||||
|
fn gt(self, rhs: Rhs) -> Self::Output;
|
||||||
|
fn ge(self, rhs: Rhs) -> Self::Output;
|
||||||
|
fn lt(self, rhs: Rhs) -> Self::Output;
|
||||||
|
fn le(self, rhs: Rhs) -> Self::Output;
|
||||||
|
fn eq(self, rhs: Rhs) -> Self::Output;
|
||||||
|
fn ne(self, rhs: Rhs) -> Self::Output;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For types that can be converted into Value
|
||||||
|
impl<T: Into<&'static str>, V: Into<Value>> FilterOps<V> for T {
|
||||||
|
type Output = Filter<'static>;
|
||||||
|
|
||||||
|
fn gt(self, rhs: V) -> Self::Output {
|
||||||
|
Filter::GreaterThan(self.into(), rhs.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ge(self, rhs: V) -> Self::Output {
|
||||||
|
Filter::GreaterThanOrEqual(self.into(), rhs.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lt(self, rhs: V) -> Self::Output {
|
||||||
|
Filter::LessThan(self.into(), rhs.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn le(self, rhs: V) -> Self::Output {
|
||||||
|
Filter::LessThanOrEqual(self.into(), rhs.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eq(self, rhs: V) -> Self::Output {
|
||||||
|
Filter::Equal(self.into(), rhs.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ne(self, rhs: V) -> Self::Output {
|
||||||
|
Filter::NotEqual(self.into(), rhs.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the appropriate placeholder for the database backend
|
||||||
|
#[inline]
|
||||||
|
pub fn placeholder(index: usize) -> String {
|
||||||
|
#[cfg(feature = "postgres")]
|
||||||
|
{
|
||||||
|
format!("${}", index)
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "postgres"))]
|
||||||
|
{
|
||||||
|
let _ = index;
|
||||||
|
"?".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Filter<'_> {
|
||||||
|
pub fn build_where_clause(filters: &[Filter]) -> (String, Vec<Value>) {
|
||||||
|
Self::build_where_clause_with_offset(filters, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_where_clause_with_offset(filters: &[Filter], start_index: usize) -> (String, Vec<Value>) {
|
||||||
|
let mut values = Vec::new();
|
||||||
|
let mut current_index = start_index;
|
||||||
|
|
||||||
|
let conditions: Vec<String> = filters
|
||||||
|
.iter()
|
||||||
|
.map(|filter| {
|
||||||
|
match filter {
|
||||||
|
Filter::Equal(field, value) => {
|
||||||
|
values.push(value.clone());
|
||||||
|
let ph = placeholder(current_index);
|
||||||
|
current_index += 1;
|
||||||
|
format!("{} = {}", field, ph)
|
||||||
|
}
|
||||||
|
Filter::NotEqual(field, value) => {
|
||||||
|
values.push(value.clone());
|
||||||
|
let ph = placeholder(current_index);
|
||||||
|
current_index += 1;
|
||||||
|
format!("{} != {}", field, ph)
|
||||||
|
}
|
||||||
|
Filter::GreaterThan(field, value) => {
|
||||||
|
values.push(value.clone());
|
||||||
|
let ph = placeholder(current_index);
|
||||||
|
current_index += 1;
|
||||||
|
format!("{} > {}", field, ph)
|
||||||
|
}
|
||||||
|
Filter::GreaterThanOrEqual(field, value) => {
|
||||||
|
values.push(value.clone());
|
||||||
|
let ph = placeholder(current_index);
|
||||||
|
current_index += 1;
|
||||||
|
format!("{} >= {}", field, ph)
|
||||||
|
}
|
||||||
|
Filter::LessThan(field, value) => {
|
||||||
|
values.push(value.clone());
|
||||||
|
let ph = placeholder(current_index);
|
||||||
|
current_index += 1;
|
||||||
|
format!("{} < {}", field, ph)
|
||||||
|
}
|
||||||
|
Filter::LessThanOrEqual(field, value) => {
|
||||||
|
values.push(value.clone());
|
||||||
|
let ph = placeholder(current_index);
|
||||||
|
current_index += 1;
|
||||||
|
format!("{} <= {}", field, ph)
|
||||||
|
}
|
||||||
|
Filter::Like(field, value) => {
|
||||||
|
values.push(value.clone());
|
||||||
|
let ph = placeholder(current_index);
|
||||||
|
current_index += 1;
|
||||||
|
format!("{} LIKE {}", field, ph)
|
||||||
|
}
|
||||||
|
Filter::ILike(field, value) => {
|
||||||
|
values.push(value.clone());
|
||||||
|
let ph = placeholder(current_index);
|
||||||
|
current_index += 1;
|
||||||
|
// PostgreSQL has native ILIKE, others use LOWER()
|
||||||
|
#[cfg(feature = "postgres")]
|
||||||
|
{
|
||||||
|
format!("{} ILIKE {}", field, ph)
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "postgres"))]
|
||||||
|
{
|
||||||
|
format!("LOWER({}) LIKE LOWER({})", field, ph)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Filter::NotLike(field, value) => {
|
||||||
|
values.push(value.clone());
|
||||||
|
let ph = placeholder(current_index);
|
||||||
|
current_index += 1;
|
||||||
|
format!("{} NOT LIKE {}", field, ph)
|
||||||
|
}
|
||||||
|
Filter::In(field, value_vec) => {
|
||||||
|
let placeholders: Vec<String> = value_vec.iter().map(|_| {
|
||||||
|
let ph = placeholder(current_index);
|
||||||
|
current_index += 1;
|
||||||
|
ph
|
||||||
|
}).collect();
|
||||||
|
values.extend(value_vec.clone());
|
||||||
|
format!("{} IN ({})", field, placeholders.join(", "))
|
||||||
|
}
|
||||||
|
Filter::NotIn(field, value_vec) => {
|
||||||
|
let placeholders: Vec<String> = value_vec.iter().map(|_| {
|
||||||
|
let ph = placeholder(current_index);
|
||||||
|
current_index += 1;
|
||||||
|
ph
|
||||||
|
}).collect();
|
||||||
|
values.extend(value_vec.clone());
|
||||||
|
format!("{} NOT IN ({})", field, placeholders.join(", "))
|
||||||
|
}
|
||||||
|
Filter::IsNull(field) => {
|
||||||
|
format!("{} IS NULL", field)
|
||||||
|
}
|
||||||
|
Filter::IsNotNull(field) => {
|
||||||
|
format!("{} IS NOT NULL", field)
|
||||||
|
}
|
||||||
|
Filter::And(nested_filters) => {
|
||||||
|
let (nested_clause, nested_values) = Self::build_where_clause_with_offset(nested_filters, current_index);
|
||||||
|
current_index += nested_values.len();
|
||||||
|
values.extend(nested_values);
|
||||||
|
format!("({})", nested_clause)
|
||||||
|
}
|
||||||
|
Filter::Or(nested_filters) => {
|
||||||
|
let (nested_clause, nested_values) = Self::build_where_clause_with_offset(nested_filters, current_index);
|
||||||
|
current_index += nested_values.len();
|
||||||
|
values.extend(nested_values);
|
||||||
|
format!("({})", nested_clause.split(" AND ").collect::<Vec<_>>().join(" OR "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
(conditions.join(" AND "), values)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! update_entity_func {
|
macro_rules! update_entity_func {
|
||||||
($form_type:ident, $func_name:ident) => { // Changed $form_type:ty to $form_type: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=sqlx::MySql>,
|
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(
|
||||||
|
|
|
||||||
14
src/lib.rs
14
src/lib.rs
|
|
@ -2,19 +2,19 @@ pub mod models;
|
||||||
pub mod repositories;
|
pub mod repositories;
|
||||||
mod helpers;
|
mod helpers;
|
||||||
mod value;
|
mod value;
|
||||||
mod condition;
|
mod filter;
|
||||||
|
|
||||||
// Re-export the entity_update_derive module on feature flag
|
// Re-export the sqlx_record_derive module on feature flag
|
||||||
#[cfg(feature = "derive")]
|
#[cfg(feature = "derive")]
|
||||||
pub use entity_update_derive::{Entity, Update};
|
pub use sqlx_record_derive::{Entity, Update};
|
||||||
|
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
pub use crate::value::*;
|
pub use crate::value::*;
|
||||||
pub use crate::condition::*;
|
pub use crate::filter::*;
|
||||||
pub use crate::{condition_or, condition_and, conditions, update_entity_func};
|
pub use crate::{filter_or, filter_and, filters, update_entity_func};
|
||||||
pub use crate::{condition_or as or, condition_and as and};
|
pub use crate::{filter_or as or, filter_and as and};
|
||||||
pub use crate::values;
|
pub use crate::values;
|
||||||
|
|
||||||
#[cfg(feature = "derive")]
|
#[cfg(feature = "derive")]
|
||||||
pub use entity_update_derive::{Entity, Update};
|
pub use sqlx_record_derive::{Entity, Update};
|
||||||
}
|
}
|
||||||
|
|
@ -1,19 +1,57 @@
|
||||||
use sqlx::{Error, MySqlExecutor};
|
use sqlx::Error;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use crate::models::EntityChange;
|
use crate::models::EntityChange;
|
||||||
|
|
||||||
|
#[cfg(feature = "mysql")]
|
||||||
|
use sqlx::MySqlExecutor as Executor;
|
||||||
|
|
||||||
|
#[cfg(feature = "postgres")]
|
||||||
|
use sqlx::PgExecutor as Executor;
|
||||||
|
|
||||||
|
#[cfg(feature = "sqlite")]
|
||||||
|
use sqlx::SqliteExecutor as Executor;
|
||||||
|
|
||||||
|
/// Returns the appropriate placeholder for the current database backend
|
||||||
|
#[inline]
|
||||||
|
fn ph(index: usize) -> String {
|
||||||
|
#[cfg(feature = "postgres")]
|
||||||
|
{
|
||||||
|
format!("${}", index)
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "postgres"))]
|
||||||
|
{
|
||||||
|
let _ = index;
|
||||||
|
"?".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the table quote character for the current database
|
||||||
|
#[inline]
|
||||||
|
fn table_quote() -> &'static str {
|
||||||
|
#[cfg(feature = "mysql")]
|
||||||
|
{ "`" }
|
||||||
|
#[cfg(feature = "postgres")]
|
||||||
|
{ "\"" }
|
||||||
|
#[cfg(feature = "sqlite")]
|
||||||
|
{ "\"" }
|
||||||
|
#[cfg(not(any(feature = "mysql", feature = "postgres", feature = "sqlite")))]
|
||||||
|
{ "" }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
|
||||||
pub async fn create_entity_change<'q>(
|
pub async fn create_entity_change<'q>(
|
||||||
conn: impl MySqlExecutor<'q>,
|
conn: impl Executor<'q>,
|
||||||
table_name: &str,
|
table_name: &str,
|
||||||
change: &EntityChange,
|
change: &EntityChange,
|
||||||
) -> Result<(), sqlx::Error> {
|
) -> Result<(), sqlx::Error> {
|
||||||
|
let q = table_quote();
|
||||||
let query = format!(
|
let query = format!(
|
||||||
r#"INSERT INTO `{}` (
|
r#"INSERT INTO {q}{}{q} (
|
||||||
id, entity_id, action, changed_at, actor_id,
|
id, entity_id, action, changed_at, actor_id,
|
||||||
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));
|
||||||
|
|
||||||
sqlx::query(&query)
|
sqlx::query(&query)
|
||||||
.bind(&change.id)
|
.bind(&change.id)
|
||||||
|
|
@ -30,9 +68,11 @@ pub async fn create_entity_change<'q>(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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 MySqlExecutor<'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 query = format!(
|
let query = format!(
|
||||||
r#"SELECT
|
r#"SELECT
|
||||||
id,
|
id,
|
||||||
|
|
@ -43,8 +83,8 @@ pub async fn get_entity_changes_by_id<'q>(
|
||||||
session_id,
|
session_id,
|
||||||
change_set_id,
|
change_set_id,
|
||||||
new_value
|
new_value
|
||||||
FROM `{}` WHERE id = ?"#,
|
FROM {q}{}{q} WHERE id = {}"#,
|
||||||
table_name);
|
table_name, ph(1));
|
||||||
|
|
||||||
let changes = sqlx::query_as::<_, EntityChange>(&query)
|
let changes = sqlx::query_as::<_, EntityChange>(&query)
|
||||||
.bind(id)
|
.bind(id)
|
||||||
|
|
@ -54,9 +94,11 @@ pub async fn get_entity_changes_by_id<'q>(
|
||||||
Ok(changes)
|
Ok(changes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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 MySqlExecutor<'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 query = format!(
|
let query = format!(
|
||||||
r#"SELECT
|
r#"SELECT
|
||||||
id,
|
id,
|
||||||
|
|
@ -67,8 +109,8 @@ pub async fn get_entity_changes_by_entity<'q>(
|
||||||
session_id,
|
session_id,
|
||||||
change_set_id,
|
change_set_id,
|
||||||
new_value
|
new_value
|
||||||
FROM `{}` WHERE entity_id = ?"#,
|
FROM {q}{}{q} WHERE entity_id = {}"#,
|
||||||
table_name);
|
table_name, ph(1));
|
||||||
|
|
||||||
let changes = sqlx::query_as::<_, EntityChange>(&query)
|
let changes = sqlx::query_as::<_, EntityChange>(&query)
|
||||||
.bind(entity_id)
|
.bind(entity_id)
|
||||||
|
|
@ -78,10 +120,12 @@ pub async fn get_entity_changes_by_entity<'q>(
|
||||||
Ok(changes)
|
Ok(changes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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 MySqlExecutor<'q>,
|
conn: impl Executor<'q>,
|
||||||
table_name: &str, session_id: &Uuid,
|
table_name: &str, session_id: &Uuid,
|
||||||
) -> Result<Vec<EntityChange>, Error>{
|
) -> Result<Vec<EntityChange>, Error>{
|
||||||
|
let q = table_quote();
|
||||||
let query = format!(
|
let query = format!(
|
||||||
r#"SELECT
|
r#"SELECT
|
||||||
id,
|
id,
|
||||||
|
|
@ -92,8 +136,8 @@ pub async fn get_entity_changes_session<'q>(
|
||||||
session_id,
|
session_id,
|
||||||
change_set_id,
|
change_set_id,
|
||||||
new_value
|
new_value
|
||||||
FROM `{}` WHERE session_id = ?"#,
|
FROM {q}{}{q} WHERE session_id = {}"#,
|
||||||
table_name);
|
table_name, ph(1));
|
||||||
|
|
||||||
let changes = sqlx::query_as::<_, EntityChange>(&query)
|
let changes = sqlx::query_as::<_, EntityChange>(&query)
|
||||||
.bind(session_id)
|
.bind(session_id)
|
||||||
|
|
@ -103,9 +147,11 @@ pub async fn get_entity_changes_session<'q>(
|
||||||
Ok(changes)
|
Ok(changes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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 MySqlExecutor<'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 query = format!(
|
let query = format!(
|
||||||
r#"SELECT
|
r#"SELECT
|
||||||
id,
|
id,
|
||||||
|
|
@ -116,8 +162,8 @@ pub async fn get_entity_changes_actor<'q>(
|
||||||
session_id,
|
session_id,
|
||||||
change_set_id,
|
change_set_id,
|
||||||
new_value
|
new_value
|
||||||
FROM `{}` WHERE actor_id = ?"#,
|
FROM {q}{}{q} WHERE actor_id = {}"#,
|
||||||
table_name);
|
table_name, ph(1));
|
||||||
|
|
||||||
let changes = sqlx::query_as::<_, EntityChange>(&query)
|
let changes = sqlx::query_as::<_, EntityChange>(&query)
|
||||||
.bind(actor_id)
|
.bind(actor_id)
|
||||||
|
|
@ -127,9 +173,11 @@ pub async fn get_entity_changes_actor<'q>(
|
||||||
Ok(changes)
|
Ok(changes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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 MySqlExecutor<'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 query = format!(
|
let query = format!(
|
||||||
r#"SELECT
|
r#"SELECT
|
||||||
id,
|
id,
|
||||||
|
|
@ -140,8 +188,8 @@ pub async fn get_entity_changes_by_change_set<'q>(
|
||||||
session_id,
|
session_id,
|
||||||
change_set_id,
|
change_set_id,
|
||||||
new_value
|
new_value
|
||||||
FROM `{}` WHERE change_set_id = ?"#,
|
FROM {q}{}{q} WHERE change_set_id = {}"#,
|
||||||
table_name);
|
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)
|
||||||
|
|
@ -151,8 +199,10 @@ pub async fn get_entity_changes_by_change_set<'q>(
|
||||||
Ok(changes)
|
Ok(changes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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 MySqlExecutor<'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 query = format!(
|
let query = format!(
|
||||||
r#"SELECT
|
r#"SELECT
|
||||||
id,
|
id,
|
||||||
|
|
@ -163,8 +213,8 @@ pub async fn get_entity_changes_by_entity_and_actor<'q>(
|
||||||
session_id,
|
session_id,
|
||||||
change_set_id,
|
change_set_id,
|
||||||
new_value
|
new_value
|
||||||
FROM `{}` WHERE entity_id = ? AND actor_id = ?"#,
|
FROM {q}{}{q} WHERE entity_id = {} AND actor_id = {}"#,
|
||||||
table_name);
|
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)
|
||||||
|
|
|
||||||
49
src/value.rs
49
src/value.rs
|
|
@ -1,9 +1,26 @@
|
||||||
use sqlx::MySql;
|
|
||||||
use sqlx::mysql::MySqlArguments;
|
|
||||||
use sqlx::query::{Query, QueryAs, QueryScalar};
|
use sqlx::query::{Query, QueryAs, QueryScalar};
|
||||||
use sqlx::types::chrono::{NaiveDate, NaiveDateTime};
|
use sqlx::types::chrono::{NaiveDate, NaiveDateTime};
|
||||||
|
|
||||||
pub type DB = MySql;
|
// Database type alias based on enabled feature
|
||||||
|
#[cfg(feature = "mysql")]
|
||||||
|
pub type DB = sqlx::MySql;
|
||||||
|
|
||||||
|
#[cfg(feature = "postgres")]
|
||||||
|
pub type DB = sqlx::Postgres;
|
||||||
|
|
||||||
|
#[cfg(feature = "sqlite")]
|
||||||
|
pub type DB = sqlx::Sqlite;
|
||||||
|
|
||||||
|
// Arguments type alias
|
||||||
|
#[cfg(feature = "mysql")]
|
||||||
|
pub type Arguments = sqlx::mysql::MySqlArguments;
|
||||||
|
|
||||||
|
#[cfg(feature = "postgres")]
|
||||||
|
pub type Arguments = sqlx::postgres::PgArguments;
|
||||||
|
|
||||||
|
#[cfg(feature = "sqlite")]
|
||||||
|
pub type Arguments = sqlx::sqlite::SqliteArguments<'static>;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Value {
|
pub enum Value {
|
||||||
Int8(i8),
|
Int8(i8),
|
||||||
|
|
@ -27,6 +44,7 @@ pub enum Updater<'a> {
|
||||||
Increment(&'a str, Value),
|
Increment(&'a str, Value),
|
||||||
Decrement(&'a str, Value),
|
Decrement(&'a str, Value),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[deprecated(since = "0.1.0", note = "Please use Value instead")]
|
#[deprecated(since = "0.1.0", note = "Please use Value instead")]
|
||||||
pub type SqlValue = Value;
|
pub type SqlValue = Value;
|
||||||
|
|
||||||
|
|
@ -52,7 +70,8 @@ macro_rules! bind_value {
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bind_values<'q>(query: Query<'q, DB, MySqlArguments>, values: &'q [Value]) -> Query<'q, DB, MySqlArguments> {
|
#[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> {
|
||||||
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);
|
||||||
|
|
@ -60,14 +79,15 @@ pub fn bind_values<'q>(query: Query<'q, DB, MySqlArguments>, values: &'q [Value]
|
||||||
query
|
query
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
|
||||||
pub fn bind_as_values<'q, O>(query: QueryAs<'q, DB, O, MySqlArguments>, values: &'q [Value]) -> QueryAs<'q, DB, O, MySqlArguments> {
|
pub fn bind_as_values<'q, O>(query: QueryAs<'q, DB, O, Arguments>, values: &'q [Value]) -> QueryAs<'q, DB, O, Arguments> {
|
||||||
values.into_iter().fold(query, |query, value| {
|
values.into_iter().fold(query, |query, value| {
|
||||||
bind_value!(query, value)
|
bind_value!(query, value)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bind_scalar_values<'q, O>(query: QueryScalar<'q, DB, O, MySqlArguments>, values: &'q [Value]) -> QueryScalar<'q, DB, O, MySqlArguments> {
|
#[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> {
|
||||||
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);
|
||||||
|
|
@ -159,8 +179,9 @@ pub trait BindValues<'q> {
|
||||||
fn bind_values(self, values: &'q [Value]) -> Self::Output;
|
fn bind_values(self, values: &'q [Value]) -> Self::Output;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'q> BindValues<'q> for Query<'q, DB, MySqlArguments> {
|
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
|
||||||
type Output = Query<'q, DB, MySqlArguments>;
|
impl<'q> BindValues<'q> for Query<'q, DB, Arguments> {
|
||||||
|
type Output = Query<'q, DB, Arguments>;
|
||||||
|
|
||||||
fn bind_values(self, values: &'q [Value]) -> Self::Output {
|
fn bind_values(self, values: &'q [Value]) -> Self::Output {
|
||||||
let mut query = self;
|
let mut query = self;
|
||||||
|
|
@ -171,8 +192,9 @@ impl<'q> BindValues<'q> for Query<'q, DB, MySqlArguments> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'q, O> BindValues<'q> for QueryAs<'q, DB, O, MySqlArguments> {
|
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
|
||||||
type Output = QueryAs<'q, DB, O, MySqlArguments>;
|
impl<'q, O> BindValues<'q> for QueryAs<'q, DB, O, Arguments> {
|
||||||
|
type Output = QueryAs<'q, DB, O, Arguments>;
|
||||||
|
|
||||||
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.into_iter().fold(self, |query, value| {
|
||||||
|
|
@ -181,8 +203,9 @@ impl<'q, O> BindValues<'q> for QueryAs<'q, DB, O, MySqlArguments> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'q, O> BindValues<'q> for QueryScalar<'q, DB, O, MySqlArguments> {
|
#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
|
||||||
type Output = QueryScalar<'q, DB, O, MySqlArguments>;
|
impl<'q, O> BindValues<'q> for QueryScalar<'q, DB, O, Arguments> {
|
||||||
|
type Output = QueryScalar<'q, DB, O, Arguments>;
|
||||||
|
|
||||||
fn bind_values(self, values: &'q [Value]) -> Self::Output {
|
fn bind_values(self, values: &'q [Value]) -> Self::Output {
|
||||||
let mut query = self;
|
let mut query = self;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue