Skip to main content
Deploy a newer CTRLRun over an existing database and it migrates at open, forward only, one transaction per migration. You run no command and set no flag, because there is no flag: a database that opened un-migrated would be a database serving reads it half understands.

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

So a rolling deploy is safe in the forward direction and stops in the backward one. Plan a rollback as a database restore, not as a redeploy of the previous image, and take the backup before the first new process starts.

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, effects and inspect against a postgresql:// 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.
Verified by 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