sqlx-record/CLAUDE.md

10 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, bind functions
│   ├── filter.rs             # Filter enum for query conditions
│   └── 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

# 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:

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 or lazily acquire from pool:

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:

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,
}

Generated Methods

Insert:

  • insert(&pool) -> Result<PkType, Error>

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>

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<serde_json::Value, Error>
  • 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<Option<VersionType>, Error>
  • get_versions(&pool, &[pk]) -> Result<HashMap<PkType, VersionType>, Error>

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):

  • u8i16, u16i32, u32i64, u64i64

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