← All scenarios

Scenario · Migrations & Releases

Bad nullable column rollout

A sandboxed PostgreSQL incident — investigate with your own tools, submit a fix, and get deterministic Detect / Fix / Trap scoring.

L3 · 10–15 min · runs locally in Docker

Launch

Start this scenario

Boot it in a real PostgreSQL sandbox and investigate with psql, EXPLAIN and pg_stat_statements.

ride postgres start stage-09/04-bad-nullable-column-rollout

Part of these paths

Show the postmortem & investigation hints spoilers
Bad nullable column rollout
Type: incident simulation · Topic: Migrations & Releases · Level: L3 · Duration: 10–15 min
Launch: ride postgres start stage-09/04-bad-nullable-column-rollout

POSTMORTEM (root cause · how it was found · the fix · lesson)
Root cause: a release added `users.plan` in app_db and shipped the new app at the
same time, but the migration only ran `ALTER TABLE users ADD COLUMN plan text`.
Existing rows stayed NULL, the column had no default and was still nullable, so the
new app — which assumes every user has a plan — broke for all existing users.

How it was found: `SELECT count(*) FROM users WHERE plan IS NULL` showed thousands
of NULLs, and information_schema.columns confirmed is_nullable = YES with no
column_default.

The fix (safe rollout): backfill, default, then harden:
  UPDATE users SET plan = 'free' WHERE plan IS NULL;
  ALTER TABLE users ALTER COLUMN plan SET DEFAULT 'free';
  ALTER TABLE users ALTER COLUMN plan SET NOT NULL;

Lesson: adding a column is not a rollout. Backfill existing rows, set a default so
new inserts can't reintroduce NULLs, and only then SET NOT NULL. On a large table,
prefer a `CHECK (plan IS NOT NULL) NOT VALID` → `VALIDATE CONSTRAINT` → `SET NOT
NULL` sequence to avoid a long table-rewrite lock. An index fixes nothing here.

INVESTIGATION HINTS (the staged path to diagnose and fix)
1. A release added users.plan in app_db, but the new app expects every user to have a plan and some rows are NULL. Confirm: SELECT count(*) FROM users WHERE plan IS NULL; in app_db.
2. Adding a column isn't a rollout. Existing rows kept NULL, there's no default, and the column is still nullable — inspect information_schema.columns (is_nullable, column_default) for users.plan.
3. Finish the rollout in app_db: backfill (UPDATE users SET plan='free' WHERE plan IS NULL), set a default (ALTER ... SET DEFAULT 'free'), then ALTER ... SET NOT NULL once there are no NULLs. Don't add an index — that's not the problem.