# 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 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/ # Claude Code skills documentation │ ├── sqlx-record.md # Overview and quick reference │ ├── sqlx-entity.md # #[derive(Entity)] detailed guide │ ├── sqlx-filters.md # Filter system guide │ ├── sqlx-audit.md # Audit trail guide │ ├── sqlx-lookup.md # Lookup tables guide │ └── sqlx-values.md # Value types guide └── Cargo.toml # Workspace root ``` ### Feature Flags - `derive`: Enables `#[derive(Entity, Update)]` procedural macros - `static-validation`: Enables compile-time SQLx query validation - `mysql`: MySQL database support - `postgres`: PostgreSQL database support - `sqlite`: SQLite database support **Note:** You must enable at least one database feature. ## Development Commands ```bash # 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 code - `generate_filter` - Generate filter expressions - `generate_lookup` - Generate lookup_table!/lookup_options! code - `explain_feature` - Get detailed documentation **Resources:** - `sqlx-record://docs/overview` - Library overview - `sqlx-record://docs/derive` - Derive macro docs - `sqlx-record://docs/filters` - Filter system docs - `sqlx-record://docs/values` - Value types docs - `sqlx-record://docs/lookup` - Lookup tables docs - `sqlx-record://docs/audit` - Audit trail docs - `sqlx-record://docs/examples` - Complete examples **Usage:** ```bash cargo build -p sqlx-record-mcp ./target/debug/sqlx-record-expert ``` ## Lookup Macros ### lookup_table! Creates database-backed lookup entity with code enum: ```rust lookup_table!(OrderStatus, "pending", "shipped", "delivered"); // Generates: struct OrderStatus, enum OrderStatusCode, constants ``` ### lookup_options! Creates code enum without database entity: ```rust lookup_options!(PaymentMethod, "credit-card", "paypal", "bank-transfer"); // Generates: enum PaymentMethodCode, struct PaymentMethod (constants only) ``` ## Time-Ordered UUIDs ```rust use sqlx_record::new_uuid; let id = new_uuid(); // Timestamp prefix (8 bytes) + random (8 bytes) ``` ## Connection Provider Flexible connection management - borrow existing or lazily acquire from pool: ```rust use sqlx_record::prelude::ConnProvider; // From borrowed connection let mut provider = ConnProvider::from_ref(&mut conn); // From pool (lazy acquisition) let mut provider = ConnProvider::from_pool(pool.clone()); // Get connection (acquires on first call for Owned variant) let conn = provider.get_conn().await?; ``` ## UpdateExpr - Advanced Update Operations Beyond simple value updates, use `eval_*` methods for arithmetic and conditional updates: ```rust use sqlx_record::prelude::*; // Arithmetic operations let form = User::update_form() .with_name("Alice") // Simple value .eval_count(UpdateExpr::Add(1.into())) // count = count + 1 .eval_score(UpdateExpr::Sub(10.into())); // score = score - 10 // CASE/WHEN conditional let form = User::update_form() .eval_status(UpdateExpr::Case { branches: vec![ ("score".gt(100), "vip".into()), ("score".gt(50), "premium".into()), ], default: "standard".into(), }); // Conditional increment let form = User::update_form() .eval_balance(UpdateExpr::AddIf { condition: "is_premium".eq(true), value: 100.into(), }); // Raw SQL escape hatch let form = User::update_form() .raw("computed", "COALESCE(a, 0) + COALESCE(b, 0)") .raw_with_values("adjusted", "value * ? + ?", values![1.5, 10]); ``` ### UpdateExpr Variants | Variant | SQL Generated | |---------|--------------| | `Set(value)` | `column = ?` | | `Add(value)` | `column = column + ?` | | `Sub(value)` | `column = column - ?` | | `Mul(value)` | `column = column * ?` | | `Div(value)` | `column = column / ?` | | `Mod(value)` | `column = column % ?` | | `Case { branches, default }` | `column = CASE WHEN ... THEN ... ELSE ... END` | | `AddIf { condition, value }` | `column = CASE WHEN cond THEN column + ? ELSE column END` | | `SubIf { condition, value }` | `column = CASE WHEN cond THEN column - ? ELSE column END` | | `Coalesce(value)` | `column = COALESCE(column, ?)` | | `Greatest(value)` | `column = GREATEST(column, ?)` | | `Least(value)` | `column = LEAST(column, ?)` | | `Raw { sql, values }` | `column = {sql}` (escape hatch) | ## Derive Macro API ### Entity Attributes ```rust #[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` - `insert_many(&pool, &[entities]) -> Result, Error>` - Batch insert - `upsert(&pool) -> Result` - Insert or update on PK conflict - `insert_or_update(&pool) -> Result` - Alias for upsert **Get:** - `get_by_{pk}(&pool, &pk) -> Result, Error>` - `get_by_{pks}(&pool, &[pk]) -> Result, Error>` - `get_by_primary_key(&pool, &pk) -> Result, Error>` **Find:** - `find(&pool, filters, index) -> Result, Error>` - `find_one(&pool, filters, index) -> Result, Error>` - `find_ordered(&pool, filters, index, order_by) -> Result, Error>` - `find_ordered_with_limit(&pool, filters, index, order_by, offset_limit) -> Result, Error>` - `count(&pool, filters, index) -> Result` - `paginate(&pool, filters, index, order_by, page_request) -> Result, Error>` - `find_partial(&pool, &[fields], filters, index) -> Result, 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::Value` - `db_diff(&form, &pk, &pool) -> Result` - `diff_modify(&mut form, &model) -> serde_json::Value` - `to_update_form(&self) -> UpdateForm` - `initial_diff(&self) -> serde_json::Value` **Metadata:** - `table_name() -> &'static str` - `entity_key(&pk) -> String` - `entity_changes_table_name() -> String` - `primary_key_field() -> &'static str` - `primary_key_db_field() -> &'static str` - `primary_key(&self) -> &PkType` - `select_fields() -> Vec<&'static str>` **Version (if #[version] field exists):** - `get_version(&pool, &pk) -> Result, Error>` - `get_versions(&pool, &[pk]) -> Result, Error>` **Soft Delete (if #[soft_delete] field exists):** - `delete(&pool) -> Result<(), Error>` - Sets soft_delete to true - `delete_by_{pk}(&pool, &pk) -> Result<(), Error>` - `hard_delete(&pool) -> Result<(), Error>` - Permanently removes row - `hard_delete_by_{pk}(&pool, &pk) -> Result<(), Error>` - `restore(&pool) -> Result<(), Error>` - Sets soft_delete to false - `restore_by_{pk}(&pool, &pk) -> Result<(), Error>` - `soft_delete_field() -> &'static str` - Returns field name ## Pagination ```rust 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 properties page.items // Vec - 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 ```rust 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 ```rust 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`, `VecU8` - `Uuid` - `NaiveDate`, `NaiveDateTime` ## Entity Changes (Audit Trail) The `EntityChange` struct tracks: - `id`: Change record UUID - `entity_id`: Target entity UUID - `action`: insert/update/delete/restore/hard-delete - `changed_at`: Timestamp (milliseconds) - `actor_id`: Who made the change - `session_id`: Session context - `change_set_id`: Transaction grouping - `new_value`: JSON payload of changes ## Important Notes - Always enable a database feature (`mysql`, `postgres`, or `sqlite`) - The `prelude` module exports commonly used items including `placeholder()` function - Query filters use `Filter::build_where_clause()` internally - Version fields auto-increment with overflow wrapping - The CLI tool requires a `entity_changes_metadata` table with `table_name` and `is_auditable` columns