Upgrades
Migrations are expand-only, so rolling back is deploying the old image and nothing else.
Migrations are expand-only: they never drop, rename, retype, narrow, or add
NOT NULL without a default. A test enforces it rather than review. The
previous build therefore keeps working against a newer schema, which is what
makes rolling back "deploy the old image" and nothing else.
That last sentence is now demonstrated rather than argued. make verify-rollback in the repository migrates a database with the current build,
then starts the published 0.1.0 and 0.2.0 images against it and requires them
to serve. It checks the failure direction too: mark a migration breaking and
the older build refuses to start, which is the behaviour the guarantee depends
on.
What it does not cover is every query an older build might make against newer columns: it exercises startup and health. Expand-only is what makes the rest safe, and that is enforced per migration.
cardinal-server migrate # a separate step, not a side effect of starting
cardinal-server migrate -status
Applying the schema while other replicas still serve the old one is how rolling deploys break, so it is a decision rather than something that happens.
At startup Cardinal checks: schema behind means refuse and tell you to migrate,
schema ahead means warn and serve, and a migration that declared itself
breaking means refuse: an older build can tell, because the reason is written
into schema_migrations when applied.
Partitions are created ahead, and reported
The journal and the decision log are partitioned by time, and the partitions
are created by migration rather than appearing on their own. The schema ships
them to the end of 2035, with a DEFAULT partition behind them so running out
slows things down instead of stopping them.
The server warns at startup when a table has under two years of partitions left, and warns differently once rows are landing in the backstop, which is the point at which adding the proper partition means moving them first.
Do not drop those partitions to "undo" a migration. Every mutation writes its journal entry in the same transaction as the change it records, so a row that routes nowhere fails the change with it.
Moving between PostgreSQL releases
Cardinal needs PostgreSQL 18 or newer. Releases up to 0.4.0 required 19 and ran on its betas. Moving the database to another release is not a restart: a major release, or a 19 beta, cannot open a data directory another one created. The database moves by dump and restore:
# with the old PostgreSQL still running, and Cardinal stopped
pg_dump -Fc -d cardinal > cardinal.pgc
# start the new PostgreSQL on an empty data directory, then
pg_restore --exit-on-error -d cardinal cardinal.pgc
cardinal-server migrate # from the build you are moving to
cardinal audit verify # the journal's chain, end to end
The audit journal survives the move, and a test checks it: it writes a
journal, dumps it, restores it into a second PostgreSQL and requires the hash
chain to verify and end on the same hash. It held from a 19 beta to 18 and
from 18 to a 19 beta. cardinal audit verify is the same check against your
own journal, and the one to run after a move.
Rolling back across the move means moving the database back: releases up to 0.4.0 refuse to start on anything older than 19.
Before you upgrade
Read the changelog. It is written by hand precisely so that "this one is a security fix" is something it can say.