Rules
32 ways a migration can hurt production.
Every check ShadowDB runs on a pull request, why it matters in PostgreSQL, and the safer way to write it. Critical and High findings block the merge by default; each project can change that.
- [critical]
- Data loss or a broken deploy is likely.
- [high]
- Blocks reads or writes on a busy table, or fails on real data.
- [medium]
- Risky at scale or easy to get wrong; worth a look.
- [low]
- Good practice; rarely a problem on its own.
- [info]
- For your information; never affects the result.
Destructive
Statements that delete data for good. They're often intended, so ShadowDB asks you to confirm them rather than guess.
- SDB101Table droppedDropping a table that already exists deletes its data.[critical]
- SDB102Column droppedDropping a column deletes its data and breaks clients that still select it.[high]
- SDB103Schema droppedDropping a schema removes every object inside it.[critical]
- SDB104Table truncatedTRUNCATE deletes every row of a table that already exists.[critical]
- SDB105UPDATE or DELETE without WHEREThe statement touches every row of an existing table.[medium]
Compatibility
Renames and removals that break application code still running against the old names during a deploy.
Indexes
Building or dropping an index the plain way locks the table; the CONCURRENTLY forms don't block writes.
Rewrites
Changes that make PostgreSQL copy the whole table while it's locked. Time grows with table size.
- SDB301Column type change rewrites the tableChanging a column type usually rewrites the whole table under an ACCESS EXCLUSIVE lock.[high]
- SDB302Column added with a volatile defaultA default like random() or clock_timestamp() forces a table rewrite.[high]
- SDB303Table switched between LOGGED and UNLOGGEDSET LOGGED / SET UNLOGGED rewrites the table.[high]
- SDB304Stored generated column addedA STORED generated column is computed for every existing row.[high]
- SDB305Identity column added to an existing tableAdding an identity column rewrites the table.[high]
Constraints
New constraints are checked against every existing row while a lock is held, unless added NOT VALID and validated later.
Locking
A statement that queues for a strong lock blocks every query behind it. A lock_timeout makes it give up instead.
Data-dependent
Changes that succeed or fail depending on the rows already in production (duplicates, NULLs).
- SDB501Unique constraint added to existing dataAdding UNIQUE can fail on duplicates and builds its index under a lock.[high]
- SDB502SET NOT NULL on an existing columnSET NOT NULL scans the table under an ACCESS EXCLUSIVE lock and fails on any NULL.[high]
- SDB503NOT NULL column added without a defaultFails on any table that already has rows.[high]
Transactions
How the migration tool wraps each file in a transaction, and statements that can't run inside one.
History
Migrations already applied in production must not change, and new ones must sort after them.
Execution evidence
What ShadowDB measured when it ran the migration on a seeded copy: locks held, rewrites, timings and errors.
- SDB801Table rewritten during executionShadowDB observed the table's storage being rewritten.[high]
- SDB802ACCESS EXCLUSIVE lock heldA lock that blocks all reads and writes was held on an existing table.[high]
- SDB803Slow statementA statement ran longer than the threshold at the seeded row count.[medium]
- SDB804Statement failedPostgreSQL returned an error while executing the migration.[critical]
Engine
Files ShadowDB couldn't fully analyse, and suppression comments it couldn't apply.