Import entity-changes project
Initial import of the entity-changes codebase as starting point for sqlx-record. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
92d559574e
commit
e9079834c7
|
|
@ -0,0 +1,5 @@
|
|||
/target
|
||||
/entity-update_derive/target
|
||||
/entity-changes-ctl/target
|
||||
.idea
|
||||
/Cargo.lock
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## 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.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Workspace Structure
|
||||
- **Main library** (`src/`): Core entity change tracking functionality
|
||||
- **entity-update_derive** (`entity-update_derive/`): Procedural macro crate for deriving Entity and Update traits
|
||||
- **entity-changes-ctl** (`entity-changes-ctl/`): Command-line utility for the library
|
||||
|
||||
### Core Components
|
||||
- **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)
|
||||
- **value.rs**: Type-safe value system supporting MySQL 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.)
|
||||
- **helpers.rs**: Utility functions and macros for the library
|
||||
|
||||
### Features
|
||||
- `derive`: Enables procedural macro support for Entity and Update traits
|
||||
- `static-validation`: Enables static SQLx validation during compilation
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Building
|
||||
```bash
|
||||
cargo build
|
||||
```
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
cargo test
|
||||
```
|
||||
|
||||
### Building with all features
|
||||
```bash
|
||||
cargo build --all-features
|
||||
```
|
||||
|
||||
### Working with workspace members
|
||||
```bash
|
||||
# Build specific workspace member
|
||||
cargo build -p entity-update_derive
|
||||
cargo build -p entity-changes-ctl
|
||||
|
||||
# Test specific workspace member
|
||||
cargo test -p entity-changes
|
||||
```
|
||||
|
||||
### Releasing
|
||||
The project uses a Makefile for tagging releases:
|
||||
```bash
|
||||
make tag
|
||||
```
|
||||
This creates a git tag based on the version in Cargo.toml and pushes it to the remote repository.
|
||||
|
||||
## Important Notes
|
||||
|
||||
- The library is designed specifically for MySQL databases via SQLx
|
||||
- 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
|
||||
- Procedural macros are optional and gated behind the "derive" feature
|
||||
- The library uses UUIDs for all entity identifiers
|
||||
- Query conditions support both simple field-value pairs and complex nested And/Or logic
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
[package]
|
||||
name = "entity-changes"
|
||||
version = "0.1.37"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
entity-update_derive = { path = "entity-update_derive", optional = true }
|
||||
sqlx = { version = "0.8", features = ["runtime-tokio", "mysql", "uuid", "chrono", "json"] }
|
||||
serde_json = "1.0"
|
||||
uuid = { version = "1", features = ["v4"]}
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
"entity-update_derive",
|
||||
"entity-changes-ctl"
|
||||
]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
derive = ["dep:entity-update_derive"]
|
||||
static-validation = ["entity-update_derive?/static-validation"]
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
TAG ?= $(shell cargo pkgid --offline | cut -d\# -f2 | cut -d: -f2)
|
||||
|
||||
tag: # Tag current release
|
||||
#git commit -am "Release $(TAG)"
|
||||
#git push
|
||||
git tag "v$(TAG)" -a -m "Release $(TAG)" && git push --tags
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,13 @@
|
|||
[package]
|
||||
name = "entity-changes-ctl"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
chrono = "0.4"
|
||||
dotenv = "0.15"
|
||||
sqlx = { version = "0.8", features = ["mysql", "runtime-tokio-native-tls"] }
|
||||
tokio = { version = "1", features = ["rt", "macros", "time", "net", "rt-multi-thread"] }
|
||||
clap = { version = "4.1", features = ["derive"] }
|
||||
url = "2"
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
CREATE TABLE entity_changes_metadata (
|
||||
table_name VARCHAR(255) PRIMARY KEY,
|
||||
is_auditable BOOLEAN NOT NULL DEFAULT TRUE
|
||||
);
|
||||
|
||||
-- INSERT INTO entity_changes_metadata (table_name) VALUES ('my_table');
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
DELIMITER $$
|
||||
|
||||
CREATE PROCEDURE DropEntityChangeTables()
|
||||
BEGIN
|
||||
DECLARE done INT DEFAULT 0;
|
||||
DECLARE table_name VARCHAR(255);
|
||||
DECLARE drop_stmt VARCHAR(512);
|
||||
|
||||
DECLARE cur CURSOR FOR
|
||||
SELECT CONCAT('DROP TABLE IF EXISTS entity_changes_', table_name)
|
||||
FROM entity_changes_metadata
|
||||
WHERE is_auditable = TRUE;
|
||||
|
||||
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
|
||||
|
||||
OPEN cur;
|
||||
|
||||
read_loop: LOOP
|
||||
FETCH cur INTO drop_stmt;
|
||||
IF done THEN
|
||||
LEAVE read_loop;
|
||||
END IF;
|
||||
|
||||
SET @s = drop_stmt;
|
||||
PREPARE stmt FROM @s;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
END LOOP;
|
||||
|
||||
CLOSE cur;
|
||||
END$$
|
||||
|
||||
DELIMITER ;
|
||||
|
||||
|
||||
CALL DropEntityChangeTables();
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
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
|
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "entity-update_derive"
|
||||
version = "0.1.6"
|
||||
edition = "2021"
|
||||
|
||||
|
||||
[dependencies]
|
||||
syn = "2.0"
|
||||
quote = "1.0"
|
||||
proc-macro2 = "1.0"
|
||||
sqlx = { version = "0.8", features = ["macros"] }
|
||||
futures = "0.3"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
static-validation = []
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
# Nandie Software Proprietary License
|
||||
|
||||
Copyright (c) 2024 Nandie Software. All rights reserved.
|
||||
|
||||
## License Grant
|
||||
|
||||
Nandie Software grants you a non-exclusive, non-transferable, revocable license to use the Update Macro for SQLx ("the Software") solely for your internal business purposes, subject to the terms and conditions of this license.
|
||||
|
||||
## Restrictions
|
||||
|
||||
You may not:
|
||||
1. Modify, adapt, alter, translate, or create derivative works of the Software.
|
||||
2. Reverse engineer, decompile, disassemble, or otherwise attempt to derive the source code of the Software.
|
||||
3. Remove, alter, or obscure any proprietary notices on the Software.
|
||||
4. Use the Software for any unlawful purpose.
|
||||
5. Sublicense, rent, lease, loan, or distribute the Software to any third party.
|
||||
6. Use the Software to create a competing product.
|
||||
|
||||
## Ownership
|
||||
|
||||
Nandie Software retains all right, title, and interest in and to the Software, including all intellectual property rights therein.
|
||||
|
||||
## Termination
|
||||
|
||||
This license is effective until terminated. Nandie Software may terminate this license at any time if you fail to comply with any term of this license. Upon termination, you must cease all use of the Software and destroy all copies.
|
||||
|
||||
## Warranty Disclaimer
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. NANDIE SOFTWARE DISCLAIMS ALL WARRANTIES, WHETHER EXPRESS, IMPLIED, OR STATUTORY, INCLUDING WITHOUT LIMITATION ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.
|
||||
|
||||
## Limitation of Liability
|
||||
|
||||
IN NO EVENT SHALL NANDIE SOFTWARE BE LIABLE FOR ANY SPECIAL, INCIDENTAL, INDIRECT, OR CONSEQUENTIAL DAMAGES WHATSOEVER ARISING OUT OF OR RELATED TO YOUR USE OR INABILITY TO USE THE SOFTWARE.
|
||||
|
||||
## Governing Law
|
||||
|
||||
This license shall be governed by and construed in accordance with the laws of [Insert Jurisdiction], without regard to its conflict of law provisions.
|
||||
|
||||
## Contact Information
|
||||
|
||||
If you have any questions about this license, please contact:
|
||||
|
||||
Nandie Software
|
||||
Bryanston, Johannesburg
|
||||
michael@nandie.com
|
||||
|
||||
By using the Software, you acknowledge that you have read this license, understand it, and agree to be bound by its terms and conditions.
|
||||
|
|
@ -0,0 +1,218 @@
|
|||
# Update Macro for SQLx
|
||||
|
||||
## Overview
|
||||
|
||||
This derive macro provides a convenient way to generate update forms and related methods for database entities using SQLx. It's designed to work with MySQL databases and provides flexibility in naming conventions for tables and fields. Additionally, it now supports the use of `id`, `code`, or any custom primary key field through the `primary_key` attribute.
|
||||
|
||||
## Features
|
||||
|
||||
- Generates an update form struct with optional fields
|
||||
- Creates methods for generating SQL update statements
|
||||
- Implements a builder pattern for setting update values
|
||||
- Provides methods for comparing the update form with both the original model and the database record, returning JSON objects
|
||||
- Allows custom table names and field names through optional attributes
|
||||
- Supports specifying custom primary keys with the `primary_key` attribute
|
||||
- Generates a method to get the table name for use in SQL queries
|
||||
- Includes an `initial_diff` method to get a JSON representation of all fields in the model
|
||||
- Supports database transactions for atomic operations
|
||||
- Optimized for better performance with large structs
|
||||
|
||||
## Installation
|
||||
|
||||
Add the following to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
entity_update_derive = { path = "path/to/entity_update_derive" }
|
||||
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "mysql", "json"] }
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage with Default Primary Key (`id` or `code`)
|
||||
|
||||
```rust
|
||||
use entity_update_derive::Update;
|
||||
use sqlx::FromRow;
|
||||
|
||||
#[derive(Update, FromRow, Debug)]
|
||||
struct User {
|
||||
id: i32,
|
||||
name: String,
|
||||
email: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
This will generate a `UserUpdateForm` struct and associated methods, with `id` as the primary key. If your struct uses `code` as the primary key, the macro will automatically detect and use it.
|
||||
|
||||
### Specifying a Custom Primary Key
|
||||
|
||||
You can specify a custom primary key field using the `primary_key` attribute:
|
||||
|
||||
```rust
|
||||
#[derive(Update, FromRow, Debug)]
|
||||
struct User {
|
||||
#[primary_key]
|
||||
code: String,
|
||||
name: String,
|
||||
email: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Table Name
|
||||
|
||||
You can specify a custom table name using the `table_name` attribute:
|
||||
|
||||
```rust
|
||||
#[derive(Update, FromRow, Debug)]
|
||||
#[table_name("customers")]
|
||||
struct User {
|
||||
id: i32,
|
||||
name: String,
|
||||
email: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Field Names
|
||||
|
||||
You can specify custom field names for the database using the `rename` attribute:
|
||||
|
||||
```rust
|
||||
#[derive(Update, FromRow, Debug)]
|
||||
struct User {
|
||||
id: i32,
|
||||
#[rename("user_name")]
|
||||
name: String,
|
||||
#[rename("user_email")]
|
||||
email: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
### Using the Generated Update Form with Transactions
|
||||
|
||||
```rust
|
||||
use sqlx::mysql::MySqlPool;
|
||||
use entity_update_derive::Update;
|
||||
use sqlx::FromRow;
|
||||
|
||||
#[derive(Update, FromRow, Debug)]
|
||||
struct User {
|
||||
id: i32,
|
||||
name: String,
|
||||
email: Option<String>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), sqlx::Error> {
|
||||
let pool = MySqlPool::connect("mysql://username:password@localhost/database_name").await?;
|
||||
|
||||
let form = User::update_form()
|
||||
.with_name("new_username".to_string())
|
||||
.with_email(Some("new_email@example.com".to_string()));
|
||||
|
||||
let user_id = 1;
|
||||
|
||||
let mut tx = pool.begin().await?;
|
||||
|
||||
form.execute_update(&user_id, &mut tx).await?;
|
||||
let diff = form.transactional_db_diff(&user_id, &mut tx).await?;
|
||||
println!("Diff: {:?}", diff);
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
println!("Update completed successfully");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## Generated Methods
|
||||
|
||||
### `update_form()`
|
||||
|
||||
Creates a new instance of the update form.
|
||||
|
||||
```rust
|
||||
let form = User::update_form();
|
||||
```
|
||||
|
||||
### `with_field_name()`
|
||||
|
||||
Builder method for each field in the struct.
|
||||
|
||||
```rust
|
||||
let form = User::update_form()
|
||||
.with_name("New Name")
|
||||
.with_email(Some("new_email@example.com"));
|
||||
```
|
||||
|
||||
### `update_stmt()`
|
||||
|
||||
Generates the SQL SET clause for the update statement.
|
||||
|
||||
```rust
|
||||
let stmt = form.update_stmt();
|
||||
```
|
||||
|
||||
### `bind_values()`
|
||||
|
||||
Binds the update values to a SQLx query.
|
||||
|
||||
```rust
|
||||
let query = sqlx::query("UPDATE users SET ...");
|
||||
let query = form.bind_values(query);
|
||||
```
|
||||
|
||||
### `model_diff()`
|
||||
|
||||
Compares the update form with an instance of the original struct and returns a JSON object.
|
||||
|
||||
```rust
|
||||
let diff: serde_json::Value = form.model_diff(&original_user);
|
||||
```
|
||||
|
||||
### `db_diff()`
|
||||
|
||||
Compares the update form with the current database record and returns a JSON object.
|
||||
|
||||
```rust
|
||||
let diff: serde_json::Value = form.db_diff(&user_id, &pool).await?;
|
||||
```
|
||||
|
||||
### `table_name()`
|
||||
|
||||
Returns the table name as a static string.
|
||||
|
||||
```rust
|
||||
let table_name = UserUpdateForm::table_name();
|
||||
```
|
||||
|
||||
### `initial_diff()`
|
||||
|
||||
Returns a JSON object containing all fields of the original model and their current values.
|
||||
|
||||
```rust
|
||||
let user = User {
|
||||
id: 1,
|
||||
name: "John Doe".to_string(),
|
||||
email: Some("john@example.com".to_string()),
|
||||
};
|
||||
let initial_diff: serde_json::Value = user.initial_diff();
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- This macro now supports custom primary key fields using the `primary_key` attribute. If not specified, it defaults to checking for an `id` or `code` field.
|
||||
- The macro assumes you're using MySQL. Modifications may be needed for other database types.
|
||||
- Error handling is minimal in these examples. In a production environment, you should implement proper error handling and logging.
|
||||
- The `model_diff()`, `db_diff()`, and `initial_diff()` methods return `serde_json::Value` objects, which are easy to work with when dealing with JSON data.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the Nandie Software Proprietary License. See the LICENSE file in the project root for the full license text.
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,158 @@
|
|||
pub(crate) fn to_snake_case(name: &str) -> String {
|
||||
let mut result = String::with_capacity(name.len() + 4);
|
||||
let mut chars = name.chars().peekable();
|
||||
|
||||
while let Some(current) = chars.next() {
|
||||
if let Some(_next) = chars.peek() {
|
||||
if current.is_uppercase() {
|
||||
if !result.is_empty() {
|
||||
result.push('_');
|
||||
}
|
||||
result.push(current.to_lowercase().next().unwrap());
|
||||
} else {
|
||||
result.push(current);
|
||||
}
|
||||
} else {
|
||||
// Last character
|
||||
result.push(current.to_lowercase().next().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
pluralize(&result)
|
||||
}
|
||||
pub(crate) fn pluralize(word: &str) -> String {
|
||||
if word.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
// Handle possessives and existing plurals
|
||||
if word.ends_with("'s") || word.ends_with("'") ||
|
||||
word.ends_with("s's") || word.ends_with("s'") {
|
||||
return word.to_string();
|
||||
}
|
||||
|
||||
// Compound words (handle last word)
|
||||
if word.contains(" of ") {
|
||||
let parts: Vec<&str> = word.split(" of ").collect();
|
||||
return format!("{} of {}", pluralize(parts[0]), parts[1]);
|
||||
}
|
||||
|
||||
// Compound words with hyphens
|
||||
if word.contains('-') {
|
||||
let parts: Vec<&str> = word.split('-').collect();
|
||||
return format!("{}-{}",
|
||||
pluralize(parts[0]),
|
||||
parts[1..].join("-")
|
||||
);
|
||||
}
|
||||
|
||||
// Invariant words (same singular and plural)
|
||||
match word.to_lowercase().as_str() {
|
||||
"sheep" | "deer" | "moose" | "swine" | "buffalo" | "fish" | "trout" |
|
||||
"salmon" | "pike" | "aircraft" | "series" | "species" | "means" |
|
||||
"crossroads" | "swiss" | "portuguese" | "vietnamese" | "japanese" |
|
||||
"chinese" | "chassis" | "corps" | "headquarters" | "diabetes" |
|
||||
"news" | "odds" | "innings" => return word.to_string(),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Irregular plurals
|
||||
match word.to_lowercase().as_str() {
|
||||
// People
|
||||
"person" => "people",
|
||||
"man" => "men",
|
||||
"woman" => "women",
|
||||
"child" => "children",
|
||||
"tooth" => "teeth",
|
||||
"foot" => "feet",
|
||||
"mouse" => "mice",
|
||||
"louse" => "lice",
|
||||
"goose" => "geese",
|
||||
"ox" => "oxen",
|
||||
|
||||
// Latin/Greek endings
|
||||
"alumnus" => "alumni",
|
||||
"alga" => "algae",
|
||||
"larva" => "larvae",
|
||||
"vertex" => "vertices",
|
||||
"index" => "indices",
|
||||
"matrix" => "matrices",
|
||||
"criterion" => "criteria",
|
||||
"phenomenon" => "phenomena",
|
||||
"datum" => "data",
|
||||
"medium" => "media",
|
||||
"analysis" => "analyses",
|
||||
"thesis" => "theses",
|
||||
"crisis" => "crises",
|
||||
"appendix" => "appendices",
|
||||
"stimulus" => "stimuli",
|
||||
"radius" => "radii",
|
||||
"axis" => "axes",
|
||||
"hypothesis" => "hypotheses",
|
||||
"basis" => "bases",
|
||||
"diagnosis" => "diagnoses",
|
||||
"formula" => "formulae",
|
||||
"fungus" => "fungi",
|
||||
"nucleus" => "nuclei",
|
||||
"syllabus" => "syllabi",
|
||||
"focus" => "foci",
|
||||
"cactus" => "cacti",
|
||||
"bacterium" => "bacteria",
|
||||
"curriculum" => "curricula",
|
||||
"memorandum" => "memoranda",
|
||||
"millennium" => "millennia",
|
||||
|
||||
_ => return apply_general_rules(word),
|
||||
}.to_string()
|
||||
}
|
||||
|
||||
fn apply_general_rules(word: &str) -> String {
|
||||
// Words ending in -o
|
||||
if word.ends_with('o') {
|
||||
match word.to_lowercase().as_str() {
|
||||
// -o → -oes
|
||||
w if matches!(w, "hero" | "potato" | "tomato" | "echo" |
|
||||
"tornado" | "torpedo" | "veto" | "mosquito" |
|
||||
"volcano" | "buffalo" | "domino" | "embargo") => {
|
||||
return format!("{}es", word);
|
||||
}
|
||||
// -o → -os
|
||||
_ => return format!("{}s", word),
|
||||
}
|
||||
}
|
||||
|
||||
// Words ending in -f or -fe
|
||||
if word.ends_with('f') {
|
||||
match word.to_lowercase().as_str() {
|
||||
w if matches!(w, "roof" | "belief" | "chief" | "reef") => {
|
||||
return format!("{}s", word);
|
||||
}
|
||||
_ => return format!("{}ves", &word[..word.len() - 1]),
|
||||
}
|
||||
}
|
||||
if word.ends_with("fe") {
|
||||
return format!("{}ves", &word[..word.len() - 2]);
|
||||
}
|
||||
|
||||
// Words ending in -y
|
||||
if word.ends_with('y') {
|
||||
let chars: Vec<char> = word.chars().collect();
|
||||
if chars.len() > 1 {
|
||||
let before_y = chars[chars.len() - 2];
|
||||
if !matches!(before_y, 'a' | 'e' | 'i' | 'o' | 'u') {
|
||||
return format!("{}ies", &word[..word.len() - 1]);
|
||||
}
|
||||
}
|
||||
return format!("{}s", word);
|
||||
}
|
||||
|
||||
// Words ending in sibilants (-s, -ss, -sh, -ch, -x, -z)
|
||||
if word.ends_with('s') || word.ends_with("ss") ||
|
||||
word.ends_with("sh") || word.ends_with("ch") ||
|
||||
word.ends_with('x') || word.ends_with('z') {
|
||||
return format!("{}es", word);
|
||||
}
|
||||
|
||||
// Default case: add 's'
|
||||
format!("{}s", word)
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Fetch the version from Cargo.toml
|
||||
version=$(grep '^version =' Cargo.toml | sed -E 's/version = "(.*)"/\1/')
|
||||
|
||||
# Check if the version is retrieved
|
||||
if [ -z "$version" ]; then
|
||||
echo "Version not found in Cargo.toml"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create a git tag with the version
|
||||
git tag -a "v$version" -m "Version $version"
|
||||
|
||||
# Push the tag to the remote repository
|
||||
git push origin "v$version"
|
||||
|
||||
echo "Tagged the current commit with version v$version"
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
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,29 @@
|
|||
#[macro_export]
|
||||
macro_rules! update_entity_func {
|
||||
($form_type:ident, $func_name:ident) => { // Changed $form_type:ty to $form_type:ident
|
||||
pub async fn $func_name<'a, E>(executor: E, id: &Uuid, form: $form_type) -> Result<(), RepositoryError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database=sqlx::MySql>,
|
||||
{
|
||||
//If the section exists, we update it
|
||||
let result = sqlx::query(
|
||||
format!(r#"
|
||||
UPDATE {}
|
||||
SET {}
|
||||
WHERE id = ?
|
||||
"#,
|
||||
$form_type::table_name(),
|
||||
form.update_stmt()).as_str())
|
||||
.bind_form_values(form)
|
||||
.bind(id)
|
||||
.execute(executor)
|
||||
.await;
|
||||
|
||||
if let Err(err) = result {
|
||||
tracing::error!("Error updating entity: {:?}", err);
|
||||
return Err(RepositoryError::from(err));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
pub mod models;
|
||||
pub mod repositories;
|
||||
mod helpers;
|
||||
mod value;
|
||||
mod condition;
|
||||
|
||||
// Re-export the entity_update_derive module on feature flag
|
||||
#[cfg(feature = "derive")]
|
||||
pub use entity_update_derive::{Entity, Update};
|
||||
|
||||
pub mod prelude {
|
||||
pub use crate::value::*;
|
||||
pub use crate::condition::*;
|
||||
pub use crate::{condition_or, condition_and, conditions, update_entity_func};
|
||||
pub use crate::{condition_or as or, condition_and as and};
|
||||
pub use crate::values;
|
||||
|
||||
#[cfg(feature = "derive")]
|
||||
pub use entity_update_derive::{Entity, Update};
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
use std::fmt::Display;
|
||||
use sqlx::FromRow;
|
||||
use uuid::Uuid;
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Debug, FromRow)]
|
||||
pub struct EntityChange {
|
||||
pub id: Uuid,
|
||||
pub entity_id: Uuid,
|
||||
pub action: String,
|
||||
pub changed_at: i64,
|
||||
pub actor_id: Uuid,
|
||||
pub session_id: Uuid,
|
||||
pub change_set_id: Uuid,
|
||||
pub new_value: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Action {
|
||||
Insert,
|
||||
Update,
|
||||
Delete,
|
||||
Restore,
|
||||
HardDelete,
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl From<String> for Action {
|
||||
fn from(value: String) -> Self {
|
||||
match &value.to_lowercase()[..] {
|
||||
"insert" => Action::Insert,
|
||||
"update" => Action::Update,
|
||||
"delete" => Action::Delete,
|
||||
"restore" => Action::Restore,
|
||||
"hard-delete" => Action::HardDelete,
|
||||
_ => Action::Unknown(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Action {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let str = match self {
|
||||
Action::Insert => "insert".to_string(),
|
||||
Action::Update => "update".to_string(),
|
||||
Action::Delete => "delete".to_string(),
|
||||
Action::Restore => "restore".to_string(),
|
||||
Action::HardDelete => "hard-delete".to_string(),
|
||||
Action::Unknown(value) => value.to_string(),
|
||||
};
|
||||
write!(f, "{}", str)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
use sqlx::{Error, MySqlExecutor};
|
||||
use uuid::Uuid;
|
||||
use crate::models::EntityChange;
|
||||
|
||||
|
||||
pub async fn create_entity_change<'q>(
|
||||
conn: impl MySqlExecutor<'q>,
|
||||
table_name: &str,
|
||||
change: &EntityChange,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
let query = format!(
|
||||
r#"INSERT INTO `{}` (
|
||||
id, entity_id, action, changed_at, actor_id,
|
||||
session_id, change_set_id, new_value)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)"#,
|
||||
table_name);
|
||||
|
||||
sqlx::query(&query)
|
||||
.bind(&change.id)
|
||||
.bind(&change.entity_id)
|
||||
.bind(&change.action)
|
||||
.bind(&change.changed_at)
|
||||
.bind(&change.actor_id)
|
||||
.bind(&change.session_id)
|
||||
.bind(&change.change_set_id)
|
||||
.bind(&change.new_value)
|
||||
.execute(conn)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_entity_changes_by_id<'q>(
|
||||
conn: impl MySqlExecutor<'q>,
|
||||
table_name: &str, id: &Uuid) -> Result<Vec<EntityChange>, Error> {
|
||||
let query = format!(
|
||||
r#"SELECT
|
||||
id,
|
||||
entity_id,
|
||||
action,
|
||||
changed_at,
|
||||
actor_id,
|
||||
session_id,
|
||||
change_set_id,
|
||||
new_value
|
||||
FROM `{}` WHERE id = ?"#,
|
||||
table_name);
|
||||
|
||||
let changes = sqlx::query_as::<_, EntityChange>(&query)
|
||||
.bind(id)
|
||||
.fetch_all(conn)
|
||||
.await?;
|
||||
|
||||
Ok(changes)
|
||||
}
|
||||
|
||||
pub async fn get_entity_changes_by_entity<'q>(
|
||||
conn: impl MySqlExecutor<'q>,
|
||||
table_name: &str, entity_id: &Uuid) -> Result<Vec<EntityChange>, Error> {
|
||||
let query = format!(
|
||||
r#"SELECT
|
||||
id,
|
||||
entity_id,
|
||||
action,
|
||||
changed_at,
|
||||
actor_id,
|
||||
session_id,
|
||||
change_set_id,
|
||||
new_value
|
||||
FROM `{}` WHERE entity_id = ?"#,
|
||||
table_name);
|
||||
|
||||
let changes = sqlx::query_as::<_, EntityChange>(&query)
|
||||
.bind(entity_id)
|
||||
.fetch_all(conn)
|
||||
.await?;
|
||||
|
||||
Ok(changes)
|
||||
}
|
||||
|
||||
pub async fn get_entity_changes_session<'q>(
|
||||
conn: impl MySqlExecutor<'q>,
|
||||
table_name: &str, session_id: &Uuid,
|
||||
) -> Result<Vec<EntityChange>, Error>{
|
||||
let query = format!(
|
||||
r#"SELECT
|
||||
id,
|
||||
entity_id,
|
||||
action,
|
||||
changed_at,
|
||||
actor_id,
|
||||
session_id,
|
||||
change_set_id,
|
||||
new_value
|
||||
FROM `{}` WHERE session_id = ?"#,
|
||||
table_name);
|
||||
|
||||
let changes = sqlx::query_as::<_, EntityChange>(&query)
|
||||
.bind(session_id)
|
||||
.fetch_all(conn)
|
||||
.await?;
|
||||
|
||||
Ok(changes)
|
||||
}
|
||||
|
||||
pub async fn get_entity_changes_actor<'q>(
|
||||
conn: impl MySqlExecutor<'q>,
|
||||
table_name: &str, actor_id: &Uuid) -> Result<Vec<EntityChange>, Error>{
|
||||
let query = format!(
|
||||
r#"SELECT
|
||||
id,
|
||||
entity_id,
|
||||
action,
|
||||
changed_at,
|
||||
actor_id,
|
||||
session_id,
|
||||
change_set_id,
|
||||
new_value
|
||||
FROM `{}` WHERE actor_id = ?"#,
|
||||
table_name);
|
||||
|
||||
let changes = sqlx::query_as::<_, EntityChange>(&query)
|
||||
.bind(actor_id)
|
||||
.fetch_all(conn)
|
||||
.await?;
|
||||
|
||||
Ok(changes)
|
||||
}
|
||||
|
||||
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>
|
||||
{
|
||||
let query = format!(
|
||||
r#"SELECT
|
||||
id,
|
||||
entity_id,
|
||||
action,
|
||||
changed_at,
|
||||
actor_id,
|
||||
session_id,
|
||||
change_set_id,
|
||||
new_value
|
||||
FROM `{}` WHERE change_set_id = ?"#,
|
||||
table_name);
|
||||
|
||||
let changes = sqlx::query_as::<_, EntityChange>(&query)
|
||||
.bind(change_set_id)
|
||||
.fetch_all(conn)
|
||||
.await?;
|
||||
|
||||
Ok(changes)
|
||||
}
|
||||
|
||||
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>{
|
||||
let query = format!(
|
||||
r#"SELECT
|
||||
id,
|
||||
entity_id,
|
||||
action,
|
||||
changed_at,
|
||||
actor_id,
|
||||
session_id,
|
||||
change_set_id,
|
||||
new_value
|
||||
FROM `{}` WHERE entity_id = ? AND actor_id = ?"#,
|
||||
table_name);
|
||||
|
||||
let changes = sqlx::query_as::<_, EntityChange>(&query)
|
||||
.bind(entity_id)
|
||||
.bind(actor_id)
|
||||
.fetch_all(conn)
|
||||
.await?;
|
||||
|
||||
Ok(changes)
|
||||
}
|
||||
|
|
@ -0,0 +1,208 @@
|
|||
use sqlx::MySql;
|
||||
use sqlx::mysql::MySqlArguments;
|
||||
use sqlx::query::{Query, QueryAs, QueryScalar};
|
||||
use sqlx::types::chrono::{NaiveDate, NaiveDateTime};
|
||||
|
||||
pub type DB = MySql;
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Value {
|
||||
Int8(i8),
|
||||
Uint8(u8),
|
||||
Int16(i16),
|
||||
Uint16(u16),
|
||||
Int32(i32),
|
||||
Uint32(u32),
|
||||
Int64(i64),
|
||||
Uint64(u64),
|
||||
VecU8(Vec<u8>),
|
||||
String(String),
|
||||
Bool(bool),
|
||||
Uuid(uuid::Uuid),
|
||||
NaiveDate(NaiveDate),
|
||||
NaiveDateTime(NaiveDateTime),
|
||||
}
|
||||
|
||||
pub enum Updater<'a> {
|
||||
Set(&'a str, Value),
|
||||
Increment(&'a str, Value),
|
||||
Decrement(&'a str, Value),
|
||||
}
|
||||
#[deprecated(since = "0.1.0", note = "Please use Value instead")]
|
||||
pub type SqlValue = Value;
|
||||
|
||||
macro_rules! bind_value {
|
||||
($query:expr, $value: expr) => {{
|
||||
let query = match $value {
|
||||
Value::Int8(v) => $query.bind(v),
|
||||
Value::Uint8(v) => $query.bind(v),
|
||||
Value::Int16(v) => $query.bind(v),
|
||||
Value::Uint16(v) => $query.bind(v),
|
||||
Value::Int32(v) => $query.bind(v),
|
||||
Value::Uint32(v) => $query.bind(v),
|
||||
Value::Int64(v) => $query.bind(v),
|
||||
Value::Uint64(v) => $query.bind(v),
|
||||
Value::VecU8(v) => $query.bind(v),
|
||||
Value::String(v) => $query.bind(v),
|
||||
Value::Bool(v) => $query.bind(v),
|
||||
Value::Uuid(v) => $query.bind(v),
|
||||
Value::NaiveDate(v) => $query.bind(v),
|
||||
Value::NaiveDateTime(v) => $query.bind(v),
|
||||
};
|
||||
query
|
||||
}};
|
||||
}
|
||||
|
||||
pub fn bind_values<'q>(query: Query<'q, DB, MySqlArguments>, values: &'q [Value]) -> Query<'q, DB, MySqlArguments> {
|
||||
let mut query = query;
|
||||
for value in values {
|
||||
query = bind_value!(query, value);
|
||||
}
|
||||
query
|
||||
}
|
||||
|
||||
|
||||
pub fn bind_as_values<'q, O>(query: QueryAs<'q, DB, O, MySqlArguments>, values: &'q [Value]) -> QueryAs<'q, DB, O, MySqlArguments> {
|
||||
values.into_iter().fold(query, |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> {
|
||||
let mut query = query;
|
||||
for value in values {
|
||||
query = bind_value!(query, value);
|
||||
}
|
||||
query
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn query_fields(fields: Vec<&str>) -> String {
|
||||
fields.iter().filter_map(|e| e.split(" ").next())
|
||||
.collect::<Vec<_>>().join(", ")
|
||||
}
|
||||
|
||||
impl From<String> for Value {
|
||||
fn from(value: String) -> Self {
|
||||
Value::String(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i32> for Value {
|
||||
fn from(value: i32) -> Self {
|
||||
Value::Int32(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i64> for Value {
|
||||
fn from(value: i64) -> Self {
|
||||
Value::Int64(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for Value {
|
||||
fn from(value: bool) -> Self {
|
||||
Value::Bool(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<uuid::Uuid> for Value {
|
||||
fn from(value: uuid::Uuid) -> Self {
|
||||
Value::Uuid(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Value {
|
||||
fn from(value: &str) -> Self {
|
||||
Value::String(value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&i32> for Value {
|
||||
fn from(value: &i32) -> Self {
|
||||
Value::Int32(*value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&i64> for Value {
|
||||
fn from(value: &i64) -> Self {
|
||||
Value::Int64(*value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&bool> for Value {
|
||||
fn from(value: &bool) -> Self {
|
||||
Value::Bool(*value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&uuid::Uuid> for Value {
|
||||
fn from(value: &uuid::Uuid) -> Self {
|
||||
Value::Uuid(*value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NaiveDate> for Value {
|
||||
fn from(value: NaiveDate) -> Self {
|
||||
Value::NaiveDate(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NaiveDateTime> for Value {
|
||||
fn from(value: NaiveDateTime) -> Self {
|
||||
Value::NaiveDateTime(value)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait BindValues<'q> {
|
||||
type Output;
|
||||
|
||||
fn bind_values(self, values: &'q [Value]) -> Self::Output;
|
||||
}
|
||||
|
||||
impl<'q> BindValues<'q> for Query<'q, DB, MySqlArguments> {
|
||||
type Output = Query<'q, DB, MySqlArguments>;
|
||||
|
||||
fn bind_values(self, values: &'q [Value]) -> Self::Output {
|
||||
let mut query = self;
|
||||
for value in values {
|
||||
query = bind_value!(query, value);
|
||||
}
|
||||
query
|
||||
}
|
||||
}
|
||||
|
||||
impl<'q, O> BindValues<'q> for QueryAs<'q, DB, O, MySqlArguments> {
|
||||
type Output = QueryAs<'q, DB, O, MySqlArguments>;
|
||||
|
||||
fn bind_values(self, values: &'q [Value]) -> Self::Output {
|
||||
values.into_iter().fold(self, |query, value| {
|
||||
bind_value!(query, value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'q, O> BindValues<'q> for QueryScalar<'q, DB, O, MySqlArguments> {
|
||||
type Output = QueryScalar<'q, DB, O, MySqlArguments>;
|
||||
|
||||
fn bind_values(self, values: &'q [Value]) -> Self::Output {
|
||||
let mut query = self;
|
||||
for value in values {
|
||||
query = bind_value!(query, value);
|
||||
}
|
||||
query
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! values {
|
||||
() => {
|
||||
vec![]
|
||||
};
|
||||
($x:expr) => {
|
||||
vec![<$crate::prelude::Value>::from($x)]
|
||||
};
|
||||
($($x:expr),+ $(,)?) => {
|
||||
vec![$(<$crate::prelude::Value>::from($x)),+]
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue