Migration that locks a table or breaks a live deploy
Schema changes that lock or rewrite a table, or drop something the running app still reads.
Why it matters
Agents write migrations the way a tutorial does: against an empty local database, in one step, with the code change in the same commit. Locally every statement finishes instantly, so there is no signal that CREATE INDEX blocks writes or that a type change rewrites every row.
In production the table has data and traffic. A lock held for the length of an index build or a table rewrite stalls every insert and update behind it. Dropping or renaming a column in the same release as the code that stops reading it breaks the old version of the app that is still serving requests during a rolling deploy.
Example
+-- 20260924_add_public_ids.sql
+BEGIN;
+
+ALTER TABLE orders ADD COLUMN public_id uuid DEFAULT gen_random_uuid();
+CREATE INDEX orders_customer_id_idx ON orders (customer_id);
+ALTER TABLE orders DROP COLUMN legacy_status;
+
+COMMIT;
What it detects
- Dropped or renamed columns and tables, in SQL and in ORM helpers such as op.drop_column, RemoveField, removeColumn and rename_column. The finding says so when the same change also removes the code that read them.
- CREATE INDEX without CONCURRENTLY, and CREATE INDEX CONCURRENTLY inside a transaction, which Postgres rejects.
- Column type changes, ADD COLUMN with a computed default such as now() or gen_random_uuid(), SET NOT NULL, CHECK and foreign key constraints without NOT VALID, new primary key or unique constraints, and UPDATE or DELETE with no WHERE.
- A migration that takes one of these locks without setting lock_timeout.
- Lock-taking statements on a table created in the same migration are not flagged, because that table is empty.
What to do when it fires
Split destructive changes across deploys: ship the code that no longer uses the column first, then drop it in a later migration once no running version reads it. Build indexes with CONCURRENTLY in their own non-transactional migration.
For new columns and constraints, add them in the cheap form (no computed default, NOT VALID), backfill in batches, then validate or set the default separately. Set a short lock_timeout at the top so a blocked migration fails fast instead of queueing traffic behind it.