zanith

migrate / audit

01 — Five tables

History as data, not log lines.

Five real Postgres tables — no sidecar service. Every applied migration writes rows you can query with plain SQL, replay with migrate history --verbose, or diff across releases.

audit query — history --verbose
Applied migrations (3):
replay · query with plain SQL · diff across releases
5 tables · 0 sidecar servicesper-step audit · schema snapshotscrash-resume checkpoints
02Five shapes

Each table has its own shape.

Narrow tables for the high-volume rows, wide tables for the ones that capture detail. All five created idempotently when the runner first connects.
_zanith_migrations

Applied migration IDs and applied_at. The ledger of what ran.

2 columns
_zanith_migration_steps

Per-op audit: status, SQL, risk level + score, error. The black box.

10 columns
_zanith_schema_snapshots

Full SchemaGraph JSON after each migration (`after` phase). Replay the shape at any point.

4 columns
_zanith_migration_artifacts

Recovery artifacts: type, source, physical name, checksum, recovery SQL, expiry.

13 columns
_zanith_migration_checkpoints

Resumable backfill cursor + processed rows + status. Crash-safe progress.

7 columns
_zanith_migrations
1 / 5 · idempotent on first connect
CREATE TABLE _zanith_migrations (
id text PRIMARY KEY,
applied_at timestamptz NOT NULL DEFAULT now()
)
03History + replay

Reconstruct any past state.

migrate history --verbose reads across these tables. Every applied migration leaves a row, a per-step audit, and a schema snapshot. Reconstruct the shape from any point in time without restoring a backup.

snapshot · after apply

1,000 models · 3,967 relation edges

proof suite large-schema stress (RESULTS.md §1)

Each migration prints per-step sigils (✓ ✗ ↻), risk level, and snapshot: N model(s) from _zanith_schema_snapshots. Snapshots are stored as JSON — diff any two to see exactly what changed between releases.

migrate history --verbose
$ zanith migrate history --verbose
Applied migrations (3):
▸ 20260328_add_index
✓ step 1: [low] addIndex
04Questions answered

Six questions your audit tables answer.

No query language to learn — it's just SQL against real Postgres tables in the same database your app uses.

What broke this week?

Failed steps in the last seven days — migration id, op kind, and the refusal reason.

post-mortem.sql
SELECT migration_id, operation_kind, error, finished_at
FROM _zanith_migration_steps
WHERE status = 'failed'
AND finished_at > now() - interval '7 days'
ORDER BY finished_at DESC;
migration_idoperation_kinderror
20260402_drop_legacy_uuiddropColumnrefused — max risk 60

Which op kinds are slowest?

Average duration per operation kind on this database — spot the expensive patterns.

post-mortem.sql
SELECT operation_kind,
round(avg(extract(epoch FROM finished_at - started_at) * 1000)) AS avg_ms,
count(*) AS runs
FROM _zanith_migration_steps
WHERE status = 'done'
GROUP BY operation_kind
ORDER BY avg_ms DESC
LIMIT 10;
operation_kindavg_msruns
addIndex61214
backfill1786
addColumn8422

What's still recoverable?

Live artifacts with no expiry — the rows recover list reads.

post-mortem.sql
SELECT id, artifact_type, source_table, row_count, created_at
FROM _zanith_migration_artifacts
WHERE expires_at IS NULL
ORDER BY created_at DESC;
idartifact_typerow_count
…::archive_column::p5_legacy.legacy_codearchive_column5