The derive fills `updated_at` on an update whenever the form leaves it unset, and it filled it with `timestamp_millis()`. Every hand-written caller stamps these columns from a seconds clock, and `created_at` is not auto-filled at all, so a single INSERT-then-UPDATE left one row carrying `created_at` in seconds and `updated_at` in milliseconds. Nothing errors - the column is a bare i64 - so the rows quietly stop being comparable, and anything ordering, diffing or syncing on `updated_at` reads an auto-stamped row as roughly 56,000 years in the future. Found in Leptin on 2026-09-06 across 9 tables and 4152 rows, including 33 of 34 meals, and only because two columns on one row disagreed. Michael: "make this a default on sqlx-record-derive, seconds should be default." Seconds is the DEFAULT, not an override: a caller that sets updated_at itself still wins, so paths that already stamp deliberately are unaffected. Guarded by tests over the emitting source - the bug is a unit, not a behaviour, and a DB-backed test would need a fixture for something a string comparison settles exactly. Verified non-vacuous. Consumers should repair existing rows: any updated_at > 1e11 is milliseconds and divides by 1000. |
||
|---|---|---|
| .claude | ||
| mcp | ||
| scripts | ||
| sqlx-record-ctl | ||
| sqlx-record-derive | ||
| src | ||
| .gitignore | ||
| CLAUDE.md | ||
| Cargo.toml | ||
| Makefile | ||
| README.md | ||
README.md
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:
[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
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