Checking migrations for zero-downtime safety
The checkmigrations command flags destructive or non-atomic operations before you deploy without a maintenance window.
Checking migrations for zero-downtime safety
When you deploy multiple times a day, a migration runs while the old code is still serving traffic. Some schema changes are fine in that window (adding a nullable column); others break it (dropping a column the old code still reads) or aren't atomic with the code rollout (renaming a table). umbral checkmigrations walks every pending operation and tags it so you know which is which before you ship.
Running it
umbral checkmigrationsChecking 4 operation(s) across 2 pending migration(s)... UNSAFE (1): [DROP COL] app/0007_drop_legacy: drops column `order.legacy_total` and its data; old code reading it breaks. Expand-contract: stop writing it, deploy, then drop WARNING (1): [RENAME COL] app/0006_rename: renames column `order.total` -> `amount`; old code references `total`. Expand-contract: add `amount`, backfill, switch reads, then drop `total` Summary: 2 safe, 1 warning, 1 unsafe.The command exits non-zero when any UNSAFE operation is present, so it fails a CI pipeline by default. Add --strict to also fail on WARNING-tier operations (e.g. when even a column rename must be reviewed by a human first):
umbral checkmigrations --strictThe three tiers
SAFE: additive and backward-compatible
CREATE TABLE, a new M2M join table, and adding a nullable column (or a NOT NULL column with a default). Old code keeps working untouched.
WARNING: review before deploying
RENAME TABLE / RENAME COLUMN (not atomic with a code deploy, since one code version references the missing name), ALTER COLUMN (a type change rewrites the column and locks a large table; a NOT NULL tightening fails on existing NULLs), and adding a NOT NULL column with no default (old inserts that omit it fail).
UNSAFE: destructive or irreversible
The expand-contract pattern
The notes on each non-safe operation point at expand-contract, the standard way to make a breaking change in two safe deploys instead of one breaking one:
- Expand. Add the new shape alongside the old (add the new column, dual-write to both). This migration is
SAFE. - Migrate the data and switch reads to the new shape, then deploy the code that only uses it.
- Contract. Drop the old shape in a later migration, once no running code references it. This migration is
UNSAFE, but by now it's safe in practice because nothing reads the old surface.
Using it programmatically
The classification is a pure function, so a plugin or a custom command can gate its own deploys:
use umbral::migrate::{check_pending_safety, classify_operation, OpSafety}; let pending = check_pending_safety().await?;let blockers = pending.iter().filter(|c| c.safety.is_unsafe()).count();if blockers > 0 { // refuse to proceed, page a human, etc.}See also
- Adding NOT NULL columns to existing tables: the safe backfill path for the WARNING case.
- Recovering from migration drift: when the tracking table and disk disagree.
crates/umbral-core/src/migrate.rs(classify_operation) for the full rule set.