Database migrations with AI coding agents: fine in dev, a lock in production
The task was small: add a status column to the orders table, default it
to pending, backfill the rows that predate the feature. The agent's diff
was four lines. The migration ran clean against the seed data, and the
test suite went green in under a second. It reads like the safest change
you shipped all week.
Then it runs against the real orders table: eleven million rows, live traffic all day. Adding a column with a default can rewrite every row in place, and depending on your database and version, reads and writes on that table block until the rewrite finishes. The four-line diff that looked trivial in review just became your outage.
This isn't the same problem as migrating a large codebase to a new framework version. That's a file-by-file rewrite you can checkpoint and roll back branch by branch. A database migration runs once, against live data, and the rollback, if one even exists, has to reverse something that already happened to rows a real user is reading right now.
Why a correct migration can still be dangerous
A migration can be functionally correct, the SQL does exactly what you asked, and still be dangerous, because correctness and locking behavior are different questions. An agent, and most test suites, only ever answers the first one.
strong_migrations, an open-source gem built specifically to catch this gap, documents the patterns worth knowing even if you never install it. Adding a column with a default value used to force a full table rewrite on Postgres, MySQL and MariaDB alike. Postgres 11, MySQL 8.0.12 and MariaDB 10.3.2 all optimized the plain case, but the safe path breaks again the moment the default is anything volatile, like a function call. Adding an auto-incrementing column, changing a column's type, or adding a stored generated column still force a full rewrite today, and reads and writes block in Postgres for as long as it takes. Adding a check constraint blocks reads and writes while it validates every existing row against the new rule.
None of that shows up in a review of the diff. It only shows up against a table the size of your real one.
Migration patterns that read fine and lock a table
Renaming a column or a table. The migration succeeds instantly. Every process that still references the old name, including another agent session mid-task, or an app instance one deploy behind, starts throwing errors the moment the rename commits, not when the code finally catches up.
Removing a column. Plenty of ORMs cache the schema they saw at boot. A drop that looks isolated to one migration file can break any process that hasn't restarted yet.
A down() that isn't real. Ask an agent to "add a rollback" and
you can get a down migration that runs without error and still doesn't
reverse the change, for example dropping a column entirely when the
up() only changed its default. The rollback exists in the file, not
in reality.
A backfill written as one giant update. The output is correct and the execution is the problem: a single statement touching every row holds its lock for as long as the update runs, and on a large table that's minutes, not milliseconds.
Expand, migrate, contract: split the one-way door into three
Fowler's parallel change pattern predates AI agents by a decade and solves exactly this. Instead of one step that breaks the old shape the instant it runs, you get three: expand (add the new column or index alongside the old one, nothing removed yet), migrate (move readers and writers over while both shapes still work), and contract (drop the old shape once nothing references it anymore).
Applied to the earlier rename: instead of asking an agent to rename a column in one step, ask for three separate, reviewable diffs. Add the new column as a copy. Update the code to write both and read the new one. Drop the old column in a later migration once you've confirmed nothing still needs it. Each step is small enough to read in a minute, which is exactly the kind of one-way door plan mode exists to gate before it runs.
Where AI-written migrations go wrong, and what it really costs
Dev data hides the cost. A migration tested against a seed database with a hundred rows tells you almost nothing about what the same statement does against a hundred million. A green suite doesn't mean a safe migration, it means the SQL ran once, quickly, on data too small to reveal a lock.
Staging usually isn't a fair test either. Unless staging is seeded at production scale, which costs real storage and real anonymization work, "it ran fine in staging" mostly proves the syntax is valid.
Parallel agents collide on shared migration history. Two sessions generating migrations against the same dev database is a different coordination problem than a code merge conflict: the files can merge cleanly in git and still apply in the wrong order, or claim the same version.
A lock during business hours is an outage with a different name. Nobody wrote a bug. The migration did exactly what it said. The cost shows up as a queue of blocked queries and a support channel filling up, not as a stack trace.
Give the migration a review lane before it touches anything real
SanuDesk is a desktop app built around exactly that gap between a diff that reads fine and a change that's safe to run. Your Claude Code, Codex and Gemini sessions tile into one grid, each scoped to its own project folder, so two agents writing migrations at the same time are panes you watch side by side instead of one shared terminal you're guessing about. Work arrives through a Kanban board: "add the status column" becomes a card, it deploys to a fresh session, and once the migration is written, the card parks in a review lane with the diff attached, one more checkpoint before it runs anywhere that matters. Because SanuDesk is bring-your-own-model, that review can run on a different model than the one that wrote the migration, cheap insurance for a one-way door. A recurring sweep, checking new migration files against the patterns above before they merge, becomes a Loop, journaled per run instead of a step everyone means to remember. The grid and board are in the free plan (see pricing).
Check the row count before you write the next one
Pull every migration file your agents wrote in the last two weeks. For
each one, answer two questions the test suite never asked: how many
rows does this table actually hold in production, and does the
down() genuinely reverse the up(), or does it just run without
erroring.
If you don't know the row count, that's the finding. You've been trusting migrations against a number you've never checked. Get the count, then decide whether the change belongs in one step or three.
Download SanuDesk free to give every migration a review lane before it runs, or see how the grid, the board and Loops fit together on the features page.