Migrations
Banks ship database changes the same way they ship code: a reviewable SQL diff, recorded in source control, applied once, never edited after the fact. CrateStack’s migration runner enforces that contract.Shape
A migration is a struct, not a file convention — banks integrate it into whatever build tooling they already use:idis sortable —YYYYMMDDHHMMSS_<slug>is canonicaldescriptionis short and human-readableupis the SQL applied forward; multiple statements are split on;and run in one transactiondownis recorded but never executed by the runner — irreversible-by-default is the safe banking posture
Running
- compares each input migration against
cratestack_migrations - skips already-applied rows whose checksum matches
- aborts with
CoolError::Internalif an applied row’s checksum has drifted - for each pending row: opens a transaction, executes every statement in
up, inserts the record intocratestack_migrations, commits
CREATE TABLE rolls back with the failed CREATE INDEX, and
cratestack_migrations does not record the partial attempt.
Checksum drift
Each migration’s checksum isSHA-256(id || \0 || description || \0 || up).
Editing an already-applied migration in source control changes the
checksum:
--force flag. Restoring the original SQL or rolling forward with a
new migration are the two acceptable resolutions.
Inspecting state
status(&pool, &migrations) returns one MigrationState per input:
Multi-statement scripts
Postgres prepared statements accept exactly one command per round-trip, so the runner splitsup on ; and executes each non-empty statement
sequentially inside the same transaction. Common patterns this enables:
INSERT rolls the
CREATE TABLE and CREATE INDEX back together.
What the runner is not
- not a
down/rollback engine —downis recorded for audit but never run - not a parallel-applier — migrations are sequential and serialized through the tracking table
- not a long-running-migration coordinator — banks executing a 6-hour
ALTER TABLEuse their own backfill tooling and record the migration as a no-op when the backfill is done
Generating migrations from .cstack
The runner consumes SQL migrations identically whether they are hand-written or generated. CrateStack ships a separate schema diff generator that produces those migrations from .cstack against a committed schema snapshot — see ADR 0004 for the full design.
Three commands cover the lifecycle:
cratestack migrate diff— offline. Diffs the current.cstackagainstmigrations/<backend>/schema.snapshot.jsonand writes a new migration directory.cratestack migrate verify— CI gate. Replays the full migration history against an ephemeral DB and checks the result matches the snapshot.cratestack migrate drift— ops tool. Reports differences between the snapshot and a live database. Read-only.
.cstack to SQL. Destructive operations (column drop, lossy type change) still require explicit opt-in, and renames still require an explicit @rename annotation.
Hand-written migration steps coexist with generated ones via optional up.pre.sql / up.post.sql files inside the migration directory; the generator never overwrites them. Use these for backfills, lookup-table seeds, materialized-view refreshes, and any transform the diff engine cannot infer.
Schema
cratestack::MIGRATIONS_TABLE_DDL and applied
idempotently by ensure_migrations_table.
Read Next
- ADR 0004: Schema diff and migration generation — how
.cstackchanges turn into the SQL this runner applies - Audit log — banks frequently land
@@auditretroactively via a migration - Soft delete —
deleted_atcolumns are typically added by a follow-up migration on existing models