What 40 agent-written migrations got wrong A developer ran 40 first-draft Postgres migrations generated by a coding agent through an 18-rule safety linter, finding 13 clean files, 25 warnings, and 2 critical flags. The agent reliably avoided the classic NOT NULL DEFAULT pitfall and produced syntactically valid SQL, but consistently chose locally optimal forms that block production, such as plain CREATE INDEX instead of CONCURRENTLY and SET NOT NULL without a prior validated CHECK constraint. The two critical flags were a TRUNCATE and a VACUUM FULL that the agent executed as literally requested without pushback. A small experiment. We gave a current coding agent the role of "helpful assistant on a Postgres web app" and asked it, in forty separate requests, for the migrations a team writes in a normal quarter: add a column, add an index, add a foreign key, change a type, rename, drop, backfill, truncate, vacuum. No hints about safety, no linter in the loop, first draft only. Then we ran the eighteen rules the free CLI bundles over the forty files. | Result | Files | |---|---| | Clean | 13 | | Flagged, warning | 25 | | Flagged, critical | 2 | | Flagged, note only | 1 | | Rule | Files | What it is | |---|---|---| | BV003 https://bolvrk.com/rules/bv003 | 10 | Index created without CONCURRENTLY | | BV004 https://bolvrk.com/rules/bv004 | 4 | Column type change that rewrites the table | | BV005 https://bolvrk.com/rules/bv005 | 4 | Rename or drop inside the deploy window | | BV011 https://bolvrk.com/rules/bv011 | 4 | SET NOT NULL scanning the table under an exclusive lock | | BV008 https://bolvrk.com/rules/bv008 | 2 | Foreign key added without NOT VALID | | BV013 https://bolvrk.com/rules/bv013 | 1 | TRUNCATE in a migration | | BV015 https://bolvrk.com/rules/bv015 | 1 | VACUUM FULL under a full lock | | BV014 https://bolvrk.com/rules/bv014 | 1 | Unbounded UPDATE | | BV016 https://bolvrk.com/rules/bv016 | 1 | DROP INDEX without CONCURRENTLY | This is the part worth reading before the part about what it got wrong. Every time the agent was asked for a required column, it added a default: ADD COLUMN status text NOT NULL DEFAULT , 'open' ADD COLUMN email verified boolean NOT NULL DEFAULT false . That is the correct modern form, it is instant on Postgres 11 and later, and BV002 https://bolvrk.com/rules/bv002 never fired once. The single most famous migration mistake did not appear. Models have read a lot of blog posts about that one. It also wrote a correct USING clause on every type change, used string to array sensibly for the tags split, and wrote a clean CREATE TABLE with a foreign key when asked for a new table. The SQL was never malformed. Not one of the forty files had a syntax error. Ten of the forty files create an index, and all ten do it the plain way. Not one CONCURRENTLY . On a dev database the plain form is faster and simpler, and the agent has no reason to prefer the form that only matters when the table has fifty million rows and writes going to it. This is the shape of most of the flagged files: the agent picks the form that is locally best, and the form that is locally best is the one that blocks production. The four SET NOT NULL statements are the same story. The agent even did the right thing first, backfilling with an UPDATE before setting the constraint, and then set the constraint the way that scans the whole table under an exclusive lock, because that is the short way to write it. The two foreign keys were added without NOT VALID , so they validate every existing row while holding locks on both tables. The pattern is consistent enough that you could predict which files would be flagged from the request alone. The two criticals were a TRUNCATE and a VACUUM FULL , both of which were literally what was asked for. That is a fair point in the agent's defence and it is also the point: the request was reckless, the agent did not push back, and the only thing standing between the request and production was whether anyone was checking. Every flagged file has a one or two line correction. The four that account for twenty of the twenty-seven: php -- BV003: the agent wrote - the fix CREATE INDEX idx ON orders customer id ; CREATE INDEX CONCURRENTLY idx ON orders customer id ; -- BV011: backfilled, then ALTER TABLE users ALTER COLUMN email SET NOT NULL; -- fix: prove it under a weak lock first, then the SET NOT NULL is a catalog change PG12+ ALTER TABLE users ADD CONSTRAINT users email nn CHECK email IS NOT NULL NOT VALID; ALTER TABLE users VALIDATE CONSTRAINT users email nn; ALTER TABLE users ALTER COLUMN email SET NOT NULL; ALTER TABLE users DROP CONSTRAINT users email nn; -- BV004: a type change rewrites the table under ACCESS EXCLUSIVE ALTER TABLE orders ALTER COLUMN total TYPE bigint; -- fix: new column, batched copy, swap, drop expand / migrate / contract ALTER TABLE orders ADD COLUMN total new bigint; UPDATE orders SET total new = total WHERE total new IS NULL AND id BETWEEN 1 AND 50000; -- repeat -- ... switch the application, then rename and drop in a later release -- BV008: validates every row while locking both tables ALTER TABLE payments ADD CONSTRAINT payments order fk FOREIGN KEY order id REFERENCES orders id ; -- fix: record now, validate under a lock that lets writes through ALTER TABLE payments ADD CONSTRAINT payments order fk FOREIGN KEY order id REFERENCES orders id NOT VALID; ALTER TABLE payments VALIDATE CONSTRAINT payments order fk; Thirteen of forty clean is not a verdict on the agent. It is a measurment of what "looks right" means when the thing that makes a migration wrong is a table the author will never see. The fix is not a better prompt. It is the same check, run the same way, on every file, before it merges. Originally published at bolvrk.com https://bolvrk.com/blog/what-40-agent-written-migrations-got-wrong . Bolvrk is a deterministic checker for database migrations and committed credentials: free CLI, GitHub App and Action https://bolvrk.com/?utm source=crosspost&utm medium=blog .