Skip to content

RS-DEPLOY-003 — Dropping NOT NULL withdraws a guarantee running code relies on

Namespace: RS-DEPLOY · Code: RS-DEPLOY-003

The migration is instant and safe for the database. The hazard is the contract: application code, ORM models, serializers and downstream schemas were written against a column that could never be null, and none of them are re-checked when that guarantee is withdrawn. Nothing fails at migration time — the first null arrives later, at runtime, somewhere else. This is the reverse of RS-DATA-001, which covers adding the constraint.

Code written against a NOT NULL column does not check for NULL. Once the constraint is gone, the first NULL written, by a new code path, an import or a backfill, surfaces as an error or a wrong answer somewhere else, long after the migration that allowed it.

Production shape: public.events has 20,000,000 rows and note has never been NULL. This migration produces RS-DEPLOY-003 against it:

ALTER TABLE public.events ALTER COLUMN note DROP NOT NULL;
The fixture this example is checked against
rowshape_fixture: "1"
meta: {id: example, engine: {name: postgres, version: "16"}, privacy: standard, profile: {mode: fast, escalated: []}}
tables:
public.events:
rows: {value: 20000000, confidence: exact}
columns:
id: {type: bigint, nullable: false, unique: {value: true, confidence: exact, via: probe}}
note: {type: text, nullable: false}
created_at: {type: timestamp with time zone, nullable: false}
indexes: [{name: events_note_idx, columns: [note], bytes: 400000000}]

Ship the code that TOLERATES a null first, then relax the constraint in a later migration. That is the same expand/contract ordering a rename needs, in the other direction: make the readers safe before the writers are allowed to produce the new shape. If the column is exposed through an API or a downstream schema, check those contracts too — a nullable column is a breaking change to a consumer that declared it required.

For the example above. Ship the application code that tolerates a NULL note first, including any API schema or downstream consumer that declared the field required. Then run the same migration.

-- Only after every reader of events.note handles NULL:
SET lock_timeout = '3s';
ALTER TABLE public.events ALTER COLUMN note DROP NOT NULL;
  • RS-DATA-001 — SET NOT NULL against existing NULLs
  • RS-DEPLOY-001 — Renaming a column or table breaks running code that still uses the old name

This page is generated from the same catalog rowshape explain RS-DEPLOY-003 reads, so the remediation here is byte-identical to the one a verdict carries — they cannot drift. An agent can read it with the explain_finding MCP tool. The example is run against PostgreSQL 16 by rowshape’s test suite: the first migration must produce RS-DEPLOY-003 and the fix must not fail.