The five shapes, and what each does
Let known be the migration ids your binary ships, and applied be what the database records.
Those five are the shapes a database that already records a schema version can be in, and they
are exhaustive: the store classifies into exactly one, and anything it cannot place is refused
rather than opened. Three more sit in front of them. An empty database is migrated to head.
A database with CTRLRun’s tables and no version record — anything from before v0.6 — is
adopted, given the record it never had, and migrated; that is the case an upgrade actually meets.
A database with foreign tables and no effects table is refused, naming what it found.
The fourth row is the direction that gets forgotten and the one that corrupts. A binary that
reads a table it half understands does not fail. It succeeds, with the columns it knows, silently
dropping the ones it does not — and a receipt row whose chain fields it never wrote is a gap in
the chain nobody caused deliberately. Refusing to start is the cheap failure, and it is
SchemaMismatch, raised before a row is read from any other table.
Nothing half-applies
Each migration runs in one transaction, and both engines have transactional DDL, so a migration that raises part way rolls back to the previous version — including the row that would have recorded it. That is a claim about database engines, so the test suite ships a deliberately broken migration whose second statement raises, and proves the store refuses to open, the version is unchanged, and every row is intact. Two rules follow for anyone writing one: a migration does nothing outside its transaction — no file writes, no network, no concurrent index build — and it reads and writes columns rather than your Python objects.Rolling out
What this does not do
- There is no down migration. Forward only. Reverting means restoring a backup.
- There is no flag, keyword or environment variable that opens a database without migrating it. A test searches the CLI, the constructor and the environment for one.
- A migration is not a data repair. It moves the schema forward and preserves every row; it does not correct rows that were already wrong.
- A read command does not migrate a shared store.
ctrlrun receipts,effectsandinspectagainst apostgresql://URL refuse unless the schema is already at head, naming what to do instead. One operator reading evidence must not apply a migration to a database every other host is still running against.
T147 — a database built by the previous release’s own code, migrated, and
every row compared by content rather than counted — T148 for the refusal that names both
versions, T149 for the broken migration that rolls back, T150 for re-opening applying nothing,
T151 and T152 for the divergent and adoption shapes from four earlier releases, and T152b
for the flag that does not exist.
Next
- SQLite or Postgres: where the schema lives.
- Operations: what to check after a deploy.
- Get started · Why.