---
title: "What Parse Without Executing Actually Covers in a Rollback Script"
canonical: https://dxdev.com/blog/the-safety-check-i-trusted-wasnt-one/
datePublished: 2026-07-24
---
We had just finished a cleanup on a live database: a gate to stop a background scanner from creating ownerless records, plus a one-time pass to clean up the ones that had already piled up. Standard discipline says a change like that gets a paired rollback script, written and staged before the real thing runs, in case any part of it needs undoing later. We wrote one. Then we made a mistake trying to prove the rollback script itself was safe.

## The dry run that ran

Before running the rollback script for real, we wanted to confirm its syntax was correct without executing it. So we switched on a mode built for exactly that: parse the statements, don't run them. We pointed it at the live database and treated it as a harmless no-op.

It is a no-op for statements inside a stored procedure body. It is not a no-op for plain schema and data statements sitting outside one, and most of a rollback script is exactly that: drop this constraint, drop this table, restore these rows.

So the "safety check" ran the script. For real. A constraint we'd just added got dropped. A small reference table the cleanup depended on got dropped with it. The rows we'd just finished cleaning reverted to their messy prior state, as if the cleanup pass had never run.

## Noticing it right away

This stayed a non-event because we were already running verification queries as part of the same validation pass, and they came back wrong immediately. A constraint that should exist didn't, and a table that should have rows in it didn't exist at all. There was no gap between causing the problem and seeing it.

We restored the constraint with its correct definition and recreated and reseeded the reference table. Then we ran the cleanup step again, since it was written to be safe to repeat, and checked the end state against what the cleanup was supposed to produce. It matched, with one exception: the audit log now carries a duplicate pair of removal entries for those rows. We left them in place because that log is never edited after the fact. Nothing reached anyone using the actual product. The whole arc happened inside a validation step meant to catch problems before they could.

## The second bug in the rollback script

While putting the pieces back, we found another problem in the same script. It hadn't fired yet, because nothing had needed the rollback for real. The step that re-narrows a constraint after undoing a change assumed the underlying log table would only ever contain the values it had at the start. But that log is append-only. Records get added, never removed, and the gate we'd shipped that same day had already logged a new kind of entry.

Run the rollback as written, and that step would fail outright the moment it hit a single one of those new rows. We fixed it to let existing rows through without re-validating them, which is the correct behavior for an append-only log. Then we confirmed the corrected version handles both the untouched case and the case where new rows already exist.

## What the check actually covered

We did not skip a safety step. We added one, on purpose, specifically to avoid running something risky against a live system before we were sure it was correct. The mistake was trusting what the feature's name implied instead of checking what it covers. "Parse without executing" sounds like a blanket guarantee. It is a guarantee about one specific case, stored procedure bodies, and says nothing about everything else, including the exact kind of statement a rollback script is mostly made of.

A mode's name is a claim, and a claim about safety is exactly the kind of thing worth testing on something that isn't live before trusting it on something that is. We wrote the rollback script and tried to validate it before use, but we never checked that our validation method validated anything. The verification queries running alongside it are what turned a damaging mistake into a quick repair.
