Why data changes are riskier than code changes

There is an asymmetry at the centre of software operations that most teams' processes do not reflect.
A bad code deployment is a ninety-second problem. The previous version still exists, in the registry, in the artefact store, in git. You roll back, the old behaviour returns exactly, and you diagnose in the morning.
A bad data change is permanent. The column you dropped is gone. The rows your backfill overwrote are gone. Rolling back the deployment does nothing at all, because the deployment was never the thing that changed.
Everybody knows this when asked directly. Few pipelines act like it.
The rollback that is not a rollback
Most migration frameworks give you an "up" and a "down". The down migration is presented as the reverse of the up, and that framing is where the trouble starts, because reversibility is a property of the specific change rather than of the mechanism.
Some changes genuinely reverse. Adding a nullable column, adding an index, creating a table: the down migration removes what the up migration added, and the world is exactly as it was.
Some changes reverse in shape only. Dropping a column is the clearest case. The down migration recreates the column, correctly typed, with the right name, holding nothing. You have restored the structure and lost the information, which is the part anyone cared about.
Some changes do not reverse in any useful sense at all. A backfill that normalises a free-text field into an enumeration, running once over four years of accumulated data, has destroyed the original strings. The down migration can drop the enum column. It cannot tell you what was in the text field.
When a rollback plan says "revert the migration", the useful question is which of those three it means. Often nobody has asked.
Where generated migrations go wrong
This asymmetry matters more now, not less, and the reason is about how review works.
Reviewers rely on surface cues to decide how carefully to read. Unfamiliar style, awkward naming, a comment that trails off: these are the signals that make a person slow down. Generated code has none of them. A migration produced by a model is fluent, conventionally structured, correctly named, and reads exactly like something a careful engineer wrote after checking.
The checking is the part that may not have happened. A generated migration will confidently drop a column on the reasonable-looking assumption that nothing reads it, add a NOT NULL constraint on the assumption that no existing row is null, or write a backfill in a single transaction on the assumption that the table is small. Each assumption is plausible, each is stated in clean code, and none of them is verifiable by reading the diff. They are claims about the data, and the data is not in the pull request.
Veracode's spring 2026 testing across more than 150 models found syntax correctness above 95 percent while the security pass rate sat near 55 percent and had been flat for two years. The shape of that gap is the shape of the problem here. Producing code that is well-formed turns out to be a different skill from producing code that is safe against conditions the author cannot see. We covered the broader version of this in why AI-generated code fails code review.
Four controls that make data changes survivable
Separate the data change from the deployment
Ship them as two events, with two decisions and two blast radii. A deployment that also runs a migration has one failure mode wrapped around another, and when something goes wrong at 2am the first thing anyone has to work out is which one broke.
Separating them also makes the deployment genuinely rollback-able again, which is the property you wanted in the first place.
Never destroy in the same step that stops using
The expand-and-contract sequence, and it is worth the extra deployments.
Stop writing to the column. Ship it. Let it run for long enough that any weekly job, monthly report, or quarterly export has had a chance to touch it. Confirm from real query logs that nothing reads it. Then, in a change that does nothing else, drop it.
Each step is individually reversible. The pause is not ceremony: it is the window in which you find the reader nobody remembered, which is almost always a scheduled job or somebody's dashboard rather than application code.
Run it against production volume before it matters
A migration tested against 200 rows in a development database has been tested for correctness and not for anything else.
The behaviours that cause incidents are functions of size. A table alteration that takes 40 milliseconds locally may hold an exclusive lock for eleven minutes against 11 million rows, during which every write blocks and the application is effectively down. A backfill that runs in one transaction locally may exhaust the transaction log in production. None of that is visible at small scale.
Restore a recent production backup into a scratch environment and run the migration there, with a timer on it. The number you get is the number you plan around. Anything else is a guess with a decimal point on it.
Measure recovery instead of assuming it
"We have backups" and "we restored one last month, it took 40 minutes, and the application started cleanly against it" are different statements, and only the second one is a control.
Restore on a schedule, into an isolated environment, and record the elapsed time. The things that go wrong are mundane and all of them are discovered the same way: the retention window is shorter than anyone thought, the encryption key was rotated, the backup is of a schema version the current code cannot read, or the restore takes six hours and the recovery objective says one.
An incident is the worst moment to learn any of these. It is also, in most organisations, the only moment they get learned. Why observability matters more with generated code makes a related argument about measuring rather than assuming.
A different review standard
Code review asks whether a change is correct and well designed. Data change review has to ask five more questions, and they do not come up when reviewing a user interface change, which is why one shared checklist covers them badly.
What information does this destroy, and can it come back? Name the specific columns, rows, or files. "Nothing" is an acceptable answer and should be stated rather than assumed.
How long does it run at production volume, and what does it lock? With a measured number, from a real run against real data sizes.
Does the application work with both schemas during the rollout? For any deployment that is not instantaneous, and none of them are, old and new code run simultaneously. Both must work.
What is the recovery procedure, and when was it last performed? The date matters as much as the procedure.
Can this be split into smaller reversible steps? If the answer is no, that deserves a sentence explaining why, because the answer is usually yes.
The habit worth building
None of this is sophisticated. It is a sequencing discipline and a willingness to spend three deployments on something that could technically be done in one.
What makes it hard is that the cost is visible and immediate while the benefit is invisible and deferred. Nobody gets credit for the outage that did not happen. The team that splits every destructive change across three deploys looks slower than the team that does it in one, right up until the afternoon the second team drops a column that a quarterly finance export reads, and finds out eleven weeks later when the export runs.
Code changes are experiments you can undo. Data changes are decisions you cannot. Treating them the same way is the mistake, and it is one that stays invisible for a long time before it stops being invisible all at once.
Sources
- Veracode 2026 GenAI Code Security Report: testing across more than 150 large language models found syntax correctness above 95 percent while the security pass rate remained near 55 percent, roughly flat over two years.
- 2025 DORA report announcement, Google Cloud: drawn from roughly 5,000 respondents; AI adoption relates positively to throughput and negatively to delivery stability.


