See exactly what changed in MySQL after a web-app action.
dbtrace is a developer CLI for tracing database behavior. Take a snapshot, perform an action in your app, take another snapshot, and get a grouped report of inserted, updated, and deleted values.
When working with an unfamiliar or legacy application, a single action can quietly affect several tables.
Instead of manually inspecting the database or tracing application code:
dbtrace before
# perform the action
dbtrace after
dbtrace tells you what changed.
- Reverse-engineering legacy applications
- Debugging unexpected side effects
- Understanding unfamiliar codebases
- Checking what a form submission actually changes
- Testing integrations
- Validating business workflows
- Discovering audit/history tables
- Investigating database writes during development
dbtrace does not modify application data. It reads database state to build and compare snapshots.
Build release binaries:
./scripts/build.shThe build script uses CGO_ENABLED=0 and produces:
dist/dbtrace-darwin-amd64
dist/dbtrace-darwin-arm64
dist/dbtrace-windows-amd64.exe
If your system Go is older than required, scripts/go.sh downloads a workspace-local Go toolchain into .tools/.
Manual build commands:
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o dist/dbtrace-darwin-amd64 ./cmd/dbtrace
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o dist/dbtrace-darwin-arm64 ./cmd/dbtrace
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o dist/dbtrace-windows-amd64.exe ./cmd/dbtraceRun tests:
./scripts/test.shRun MySQL integration tests only when you have a disposable database available:
MYSQL_DSN='user:pass@tcp(localhost:3306)/app_db' ./scripts/test.shIf you have an app that uses MySQL:
- Build the tool:
./scripts/build.sh- In your app's project folder, create
dbtrace.yaml:
/path/to/dbtrace/dist/dbtrace-darwin-arm64 init --dsn="user:pass@tcp(localhost:3306)/your_app_db"Use dbtrace-darwin-amd64 on Intel Mac, or dbtrace-windows-amd64.exe on Windows.
- Take a before snapshot:
/path/to/dbtrace/dist/dbtrace-darwin-arm64 before- Use your app normally:
Click button, submit form, run workflow, etc.
- Take the after snapshot:
/path/to/dbtrace/dist/dbtrace-darwin-arm64 afterIt will automatically print what changed in MySQL:
► Table: users
* Updates (1)
id=1 last_login: 2025-06-02 07:26:58 → 2025-06-15 09:22:35
For repeated testing:
/path/to/dbtrace/dist/dbtrace-darwin-arm64 watchThen press Enter after each app action.
By default, snapshot progress is quiet:
Scanning...
Complete.
Use --verbose when you want per-table scan details:
/path/to/dbtrace/dist/dbtrace-darwin-arm64 before --verbose
/path/to/dbtrace/dist/dbtrace-darwin-arm64 after --verbose
/path/to/dbtrace/dist/dbtrace-darwin-arm64 watch --verboseVerbose mode prints lines like:
Scanning: users
Scanned: users (120 rows)
Create a config automatically:
dbtrace init --dsn="user:pass@tcp(localhost:3306)/app_db"Or copy dbtrace.yaml.example:
database:
dsn: "user:pass@tcp(localhost:3306)/app_db"
snapshot:
workers: 4
chunk_size: 10000
output_dir: ".dbtrace/snapshots"
report:
max_lines_per_operation: 50
max_value_length: 200
keys:
# Optional for legacy tables without primary keys.
# legacy_data_table:
# - project_id
# - record
# - field_name
# - event_id
# - instance
ignore:
tables:
- sessions
- cache
- jobs
columns:
- created_at
- updated_at
- last_seendbtrace init can also detect Laravel-style .env values such as DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, and DB_PASSWORD.
On Windows, quote DSNs because shells treat characters like @, &, %, and parentheses differently:
.\dbtrace-windows-amd64.exe init --dsn "user:pass@tcp(localhost:3306)/app_db"For passwords with special characters, prefer .env variables or URL-escaped password values.
dbtrace chooses row identity in this order:
- configured
keys:entries - database primary keys, including composite primary keys
- the smallest unique index
- synthetic row identity from the row hash
For older applications or legacy-style schemas, configure logical keys explicitly:
keys:
legacy_data:
- project_id
- record
- field_name
- event_id
- instanceComposite key output looks like:
project_id=1|record=1001|field_name=dob value: 1980-01-01 → 1980-02-01
Synthetic identity is a last resort. It can detect inserts/deletes, but an update may appear as one deleted row and one inserted row because there is no stable database identity.
dbtrace before
# perform action in the app
dbtrace afterdbtrace after captures the second snapshot and runs the diff automatically.
You can diff existing snapshots without taking a new snapshot:
dbtrace diffFor repeated manual testing:
dbtrace watchWatch mode takes an initial before snapshot, waits for Enter, captures after, prints the diff, rotates after into before, and waits again.
RESULT:
3 tables changed
► Table: users
* Updates (1)
id=1 last_login: 2025-06-02 07:26:58 → 2025-06-15 09:22:35
► Table: orders
* Updates (2)
id=10 status: pending → paid
id=10 paid_at: NULL → 2026-06-15 09:22:35
► Table: payments
* Inserts (1)
id=991 id: NULL → 991
id=991 amount: NULL → 49.00
Use ignored tables for noisy or irrelevant data:
ignore:
tables:
- sessions
- cacheUse ignored columns for values that change often but do not explain the action:
ignore:
columns:
- created_at
- updated_at
- last_seenIgnored columns are excluded from row hashes and stored row values.
dbtrace is designed for large tables:
- streams rows instead of loading tables into memory
- uses keyset pagination, not
OFFSET - scans tables with parallel workers
- stores snapshots in SQLite
- diffs snapshots with indexed SQLite joins
- compares row hashes first, then reads full values only for changed rows
- synthetic identity is less precise than configured keys or database keys
- duplicate rows in no-key tables may collapse under synthetic identity
- very large column values may be truncated in terminal output
- full column values remain in SQLite snapshots for future export/reporting
Release builds target:
- macOS amd64
- macOS arm64
- Windows amd64
The project uses a pure-Go SQLite driver and filepath-safe paths so builds and snapshot paths work on macOS and Windows.