13 KiB
CLAUDE.md
This file provides guidance to Claude Code when working with this repository.
Project Overview
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.
Repository: https://git.awesomike.com/pub/sqlx-record.git
Architecture
Workspace Structure
sqlx-record/
├── src/ # Core library
│ ├── lib.rs # Public API exports, prelude, lookup macros, new_uuid
│ ├── models.rs # EntityChange struct, Action enum
│ ├── repositories.rs # Database query functions for entity changes
│ ├── value.rs # Type-safe Value enum, UpdateExpr, bind functions
│ ├── filter.rs # Filter enum for query conditions
│ ├── conn_provider.rs # ConnProvider for flexible connection management
│ ├── pagination.rs # Page<T> and PageRequest structs
│ ├── transaction.rs # transaction! macro
│ └── helpers.rs # Utility macros
├── sqlx-record-derive/ # Procedural macro crate
│ └── src/
│ ├── lib.rs # #[derive(Entity, Update)] implementation
│ └── string_utils.rs # Pluralization helpers
├── sqlx-record-ctl/ # CLI tool for audit table management
│ └── src/main.rs
├── mcp/ # MCP server for documentation/code generation
│ └── src/main.rs # sqlx-record-expert executable
├── .claude/skills/sqlx-record/ # Claude Code skills documentation
│ ├── sqlx-record.md # Overview and quick reference
│ ├── sqlx-entity.md # #[derive(Entity)] detailed guide
│ ├── sqlx-filters.md # Filter system guide
│ ├── sqlx-audit.md # Audit trail guide
│ ├── sqlx-lookup.md # Lookup tables guide
│ ├── sqlx-values.md # Value types guide
│ └── sqlx-conn-provider.md # Connection provider guide
└── Cargo.toml # Workspace root
Feature Flags
derive: Enables#[derive(Entity, Update)]procedural macrosstatic-validation: Enables compile-time SQLx query validationmysql: MySQL database supportpostgres: PostgreSQL database supportsqlite: SQLite database support
Note: You must enable at least one database feature.
Development Commands
# Build with MySQL
cargo build --features mysql
# Build with derive macros
cargo build --features "mysql,derive"
# Build CLI tool
cargo build -p sqlx-record-ctl --features mysql
# Build MCP server
cargo build -p sqlx-record-mcp
# Test
cargo test --features mysql
# Release tag
make tag
MCP Server (sqlx-record-expert)
The mcp/ directory contains an MCP server providing:
Tools:
generate_entity- Generate Entity struct codegenerate_filter- Generate filter expressionsgenerate_lookup- Generate lookup_table!/lookup_options! codeexplain_feature- Get detailed documentation
Resources:
sqlx-record://docs/overview- Library overviewsqlx-record://docs/derive- Derive macro docssqlx-record://docs/filters- Filter system docssqlx-record://docs/values- Value types docssqlx-record://docs/lookup- Lookup tables docssqlx-record://docs/audit- Audit trail docssqlx-record://docs/examples- Complete examples
Usage:
cargo build -p sqlx-record-mcp
./target/debug/sqlx-record-expert
Lookup Macros
lookup_table!
Creates database-backed lookup entity with code enum:
lookup_table!(OrderStatus, "pending", "shipped", "delivered");
// Generates: struct OrderStatus, enum OrderStatusCode, constants
lookup_options!
Creates code enum without database entity:
lookup_options!(PaymentMethod, "credit-card", "paypal", "bank-transfer");
// Generates: enum PaymentMethodCode, struct PaymentMethod (constants only)
Time-Ordered UUIDs
use sqlx_record::new_uuid;
let id = new_uuid(); // Timestamp prefix (8 bytes) + random (8 bytes)
Connection Provider
Flexible connection management - borrow existing connection, lazily acquire from pool, or use a transaction:
use sqlx_record::prelude::ConnProvider;
// From borrowed connection
let mut provider = ConnProvider::from_ref(&mut conn);
// From pool (lazy acquisition)
let mut provider = ConnProvider::from_pool(pool.clone());
// From transaction (operations participate in the transaction)
let mut tx = pool.begin().await?;
let mut provider = ConnProvider::from_tx(&mut tx);
// ... use provider ...
tx.commit().await?;
// Get connection (acquires on first call for Owned variant)
let conn = provider.get_conn().await?;
UpdateExpr - Advanced Update Operations
Beyond simple value updates, use eval_* methods for arithmetic and conditional updates:
use sqlx_record::prelude::*;
// Arithmetic operations
let form = User::update_form()
.with_name("Alice") // Simple value
.eval_count(UpdateExpr::Add(1.into())) // count = count + 1
.eval_score(UpdateExpr::Sub(10.into())); // score = score - 10
// CASE/WHEN conditional
let form = User::update_form()
.eval_status(UpdateExpr::Case {
branches: vec![
("score".gt(100), "vip".into()),
("score".gt(50), "premium".into()),
],
default: "standard".into(),
});
// Conditional increment
let form = User::update_form()
.eval_balance(UpdateExpr::AddIf {
condition: "is_premium".eq(true),
value: 100.into(),
});
// Raw SQL escape hatch
let form = User::update_form()
.raw("computed", "COALESCE(a, 0) + COALESCE(b, 0)")
.raw_with_values("adjusted", "value * ? + ?", values![1.5, 10]);
UpdateExpr Variants
| Variant | SQL Generated |
|---|---|
Set(value) |
column = ? |
Add(value) |
column = column + ? |
Sub(value) |
column = column - ? |
Mul(value) |
column = column * ? |
Div(value) |
column = column / ? |
Mod(value) |
column = column % ? |
Case { branches, default } |
column = CASE WHEN ... THEN ... ELSE ... END |
AddIf { condition, value } |
column = CASE WHEN cond THEN column + ? ELSE column END |
SubIf { condition, value } |
column = CASE WHEN cond THEN column - ? ELSE column END |
Coalesce(value) |
column = COALESCE(column, ?) |
Greatest(value) |
column = GREATEST(column, ?) |
Least(value) |
column = LEAST(column, ?) |
Raw { sql, values } |
column = {sql} (escape hatch) |
Derive Macro API
Entity Attributes
#[derive(Entity, FromRow)]
#[table_name = "users"] // or #[table_name("users")]
struct User {
#[primary_key] // Mark primary key field
id: Uuid,
#[rename("user_name")] // Map to different DB column
name: String,
#[version] // Auto-increment on update
version: u32,
#[field_type("BIGINT")] // SQLx type hint
count: i64,
#[soft_delete] // Enables delete/restore/hard_delete methods
is_deleted: bool,
#[created_at] // Auto-set on insert (milliseconds)
created_at: i64,
#[updated_at] // Auto-set on update (milliseconds)
updated_at: i64,
}
Generated Methods
Insert:
insert(&pool) -> Result<PkType, Error>insert_many(&pool, &[entities]) -> Result<Vec<PkType>, Error>- Batch insertupsert(&pool) -> Result<PkType, Error>- Insert or update on PK conflictinsert_or_update(&pool) -> Result<PkType, Error>- Alias for upsert
Get:
get_by_{pk}(&pool, &pk) -> Result<Option<Self>, Error>get_by_{pks}(&pool, &[pk]) -> Result<Vec<Self>, Error>get_by_primary_key(&pool, &pk) -> Result<Option<Self>, Error>
Find:
find(&pool, filters, index) -> Result<Vec<Self>, Error>find_one(&pool, filters, index) -> Result<Option<Self>, Error>find_ordered(&pool, filters, index, order_by) -> Result<Vec<Self>, Error>find_ordered_with_limit(&pool, filters, index, order_by, offset_limit) -> Result<Vec<Self>, Error>count(&pool, filters, index) -> Result<u64, Error>paginate(&pool, filters, index, order_by, page_request) -> Result<Page<Self>, Error>find_partial(&pool, &[fields], filters, index) -> Result<Vec<Row>, Error>- Select specific columns
Update:
update(&self, &pool, form) -> Result<(), Error>update_by_{pk}(&pool, &pk, form) -> Result<(), Error>update_by_{pks}(&pool, &[pk], form) -> Result<(), Error>update_form() -> UpdateForm- Creates builder
Diff:
model_diff(&form, &model) -> serde_json::Valuedb_diff(&form, &pk, &pool) -> Result<serde_json::Value, Error>diff_modify(&mut form, &model) -> serde_json::Valueto_update_form(&self) -> UpdateForminitial_diff(&self) -> serde_json::Value
Metadata:
table_name() -> &'static strentity_key(&pk) -> Stringentity_changes_table_name() -> Stringprimary_key_field() -> &'static strprimary_key_db_field() -> &'static strprimary_key(&self) -> &PkTypeselect_fields() -> Vec<&'static str>
Version (if #[version] field exists):
get_version(&pool, &pk) -> Result<Option<VersionType>, Error>get_versions(&pool, &[pk]) -> Result<HashMap<PkType, VersionType>, Error>
Soft Delete (if #[soft_delete] field exists):
delete(&pool) -> Result<(), Error>- Sets soft_delete to truedelete_by_{pk}(&pool, &pk) -> Result<(), Error>hard_delete(&pool) -> Result<(), Error>- Permanently removes rowhard_delete_by_{pk}(&pool, &pk) -> Result<(), Error>restore(&pool) -> Result<(), Error>- Sets soft_delete to falserestore_by_{pk}(&pool, &pk) -> Result<(), Error>soft_delete_field() -> &'static str- Returns field name
Pagination
use sqlx_record::prelude::*;
// Create page request (1-indexed pages)
let page_request = PageRequest::new(1, 20); // page 1, 20 items
// Get paginated results
let page = User::paginate(&pool, filters![], None, vec![("name", true)], page_request).await?;
// Page<T> properties
page.items // Vec<T> - items for this page
page.total_count // u64 - total matching records
page.page // u32 - current page (1-indexed)
page.page_size // u32 - items per page
page.total_pages() // u32 - calculated total pages
page.has_next() // bool
page.has_prev() // bool
page.is_empty() // bool
page.len() // usize - items on this page
Transaction Helper
use sqlx_record::transaction;
// Automatically commits on success, rolls back on error
let result = transaction!(&pool, |tx| {
user.insert(&mut *tx).await?;
order.insert(&mut *tx).await?;
Ok::<_, sqlx::Error>(order.id)
}).await?;
Filter API
use sqlx_record::prelude::*;
// Simple filters
let f = filters![("active", true), ("role", "admin")];
// Compound filters
let f = filter_or![("status", "active"), ("status", "pending")];
let f = filter_and![("age", 18), ("verified", true)];
// Filter enum variants
Filter::Equal("field", value)
Filter::NotEqual("field", value)
Filter::GreaterThan("field", value)
Filter::LessThan("field", value)
Filter::Like("field", pattern)
Filter::ILike("field", pattern) // Case-insensitive
Filter::In("field", vec![values])
Filter::NotIn("field", vec![values])
Filter::IsNull("field")
Filter::IsNotNull("field")
Filter::And(vec![filters])
Filter::Or(vec![filters])
Database Differences
| Feature | MySQL | PostgreSQL | SQLite |
|---|---|---|---|
| Placeholder | ? |
$1, $2, ... |
? |
| Table quote | ` |
" |
" |
| UUID type | BINARY(16) |
UUID |
BLOB |
| JSON type | JSON |
JSONB |
TEXT |
| ILIKE | LOWER() LIKE LOWER() |
Native | LOWER() LIKE LOWER() |
| Index hints | USE INDEX() |
N/A | N/A |
| Unsigned ints | Native | Cast to signed | Cast to signed |
Unsigned integer conversion (PostgreSQL/SQLite):
u8→i16,u16→i32,u32→i64,u64→i64
Value Types
The Value enum supports:
- Integers:
Int8,Uint8,Int16,Uint16,Int32,Uint32,Int64,Uint64 String,Bool,VecU8UuidNaiveDate,NaiveDateTime
Entity Changes (Audit Trail)
The EntityChange struct tracks:
id: Change record UUIDentity_id: Target entity UUIDaction: insert/update/delete/restore/hard-deletechanged_at: Timestamp (milliseconds)actor_id: Who made the changesession_id: Session contextchange_set_id: Transaction groupingnew_value: JSON payload of changes
Important Notes
- Always enable a database feature (
mysql,postgres, orsqlite) - The
preludemodule exports commonly used items includingplaceholder()function - Query filters use
Filter::build_where_clause()internally - Version fields auto-increment with overflow wrapping
- The CLI tool requires a
entity_changes_metadatatable withtable_nameandis_auditablecolumns