SQL Backfill Pipeline Scaffold
Use when recovering a corrupted value requires walking a chain across two or
more systems (e.g. a local DB → a join key into another system's log table →
the original value extracted from a logged JSON response), and the mutation
will eventually be handed to someone else (a DBA, another team) to run against
production.
Inputs
Ask for, or infer from the conversation, before scaffolding:
- What was corrupted, and the chain of systems/DBs needed to recover the
correct value (name each system and the join key between consecutive
hops).
- Roughly how many rows are affected. This determines which mutate shape to
generate — see "Verify/mutate split" below. If unknown, ask; do not
default to the small-batch shape silently.
- Who runs the final mutation — you, in this session, or a hand-off to
someone else. A hand-off raises the bar on self-guarding.
Scaffold shape
docs/
plan.md # what's corrupted, the recovery chain, current status
resume-prompt.md # "paste into a fresh session" prompt with a living status section
phases/
phase1-<name>.sql # RUN AGAINST: <DB> banner comment at the top of every phase file
phase2-<name>.sql
...
scripts/
batch-ids.py # --table/--column flags; one generalized helper, not one per phase
command-steps.md # file -> DB target -> input -> output, plus exact shell commands
- Every
phaseN SQL file starts with a -- RUN AGAINST: <DB> banner comment
so the person executing it can't misapply it to the wrong database.
batch-ids.py reads a plain list of IDs from a file, de-duplicates,
validates the ID shape, and emits a batched INSERT ... VALUES block sized
under SQL Server's ~1000-row VALUES limit — parameterized by --table and
--column so it's reused across phases, not rewritten per phase.
command-steps.md is the single runbook: one row per step, naming the file,
the DB it targets, its input, its output, and the exact runnable command.
Verify/mutate split — branch on row count
Do not assume one shape always applies. Ask "how many rows, roughly?" and
pick:
Small batch (well under SQL Server's ~5,000 row/page lock-escalation
threshold):
- One script that is read-only by construction: build, validate, and preview
the target set — no
UPDATE, DELETE, or open transaction anywhere in it.
- A separate, minimal hand-off script containing the only mutating
statement: a single guarded transaction (
UPDATE ... JOIN) that compares
actual vs. expected row count and rolls back automatically on mismatch.
Large batch (approaching or exceeding the lock-escalation threshold):
- A single guarded transaction is not safe here — one statement holding locks
on thousands of rows risks escalating to a table lock for the run's
duration, and an unindexed table-variable join becomes an unpredictable
query-plan bet at that row count.
- Generate one
UPDATE ... WHERE <primary key> = X AND <same guard predicate>
statement per row instead, each auto-committed (no single transaction spans
more than one row's locks). Every statement carries its own idempotency
guard, so re-running the whole file after a partial or interrupted run is a
safe no-op on rows already fixed.
- Follow with a trailing count-based verification query (count of target rows
still unfixed — expect 0) rather than an in-transaction row-count check.
- Keep any earlier small-batch script as a documented "legacy — small-batch
fallback only" reference rather than deleting it, so the reason it doesn't
scale stays attached to the code.
Apply sql-server-safety
to every script this skill generates — in particular: the read-only verify
script must contain zero mutating statements, the verify and mutate scripts
must share one written predicate rather than two independently maintained
copies, and no verification/completion query may carry NOLOCK.
Non-Goals
- This does not replace DBA review for genuinely high-risk production
mutations.
- This does not generate the recovery-chain logic itself (the join keys and
extraction logic are specific to the systems involved) — it scaffolds the
file structure, batching helper, and verify/mutate split around that logic.
1---2name: sql-backfill-pipeline-scaffold3description: Scaffold a multi-phase SQL Server backfill/data-recovery pipeline that walks a chain across multiple systems to recover a corrupted value — numbered phase scripts, a batched ID-input helper, a hard read-only-verify vs. mutate split that branches on row count, and a command-steps runbook.4---56# SQL Backfill Pipeline Scaffold78Use when recovering a corrupted value requires walking a chain across two or9more systems (e.g. a local DB → a join key into another system's log table →10the original value extracted from a logged JSON response), and the mutation11will eventually be handed to someone else (a DBA, another team) to run against12production.1314## Inputs1516Ask for, or infer from the conversation, before scaffolding:17181. What was corrupted, and the chain of systems/DBs needed to recover the19 correct value (name each system and the join key between consecutive20 hops).212. Roughly how many rows are affected. This determines which mutate shape to22 generate — see "Verify/mutate split" below. If unknown, ask; do not23 default to the small-batch shape silently.243. Who runs the final mutation — you, in this session, or a hand-off to25 someone else. A hand-off raises the bar on self-guarding.2627## Scaffold shape2829```30docs/31 plan.md # what's corrupted, the recovery chain, current status32 resume-prompt.md # "paste into a fresh session" prompt with a living status section33phases/34 phase1-<name>.sql # RUN AGAINST: <DB> banner comment at the top of every phase file35 phase2-<name>.sql36 ...37scripts/38 batch-ids.py # --table/--column flags; one generalized helper, not one per phase39command-steps.md # file -> DB target -> input -> output, plus exact shell commands40```4142- Every `phaseN` SQL file starts with a `-- RUN AGAINST: <DB>` banner comment43 so the person executing it can't misapply it to the wrong database.44- `batch-ids.py` reads a plain list of IDs from a file, de-duplicates,45 validates the ID shape, and emits a batched `INSERT ... VALUES` block sized46 under SQL Server's ~1000-row `VALUES` limit — parameterized by `--table` and47 `--column` so it's reused across phases, not rewritten per phase.48- `command-steps.md` is the single runbook: one row per step, naming the file,49 the DB it targets, its input, its output, and the exact runnable command.5051## Verify/mutate split — branch on row count5253Do not assume one shape always applies. Ask "how many rows, roughly?" and54pick:5556**Small batch (well under SQL Server's ~5,000 row/page lock-escalation57threshold):**58- One script that is read-only by construction: build, validate, and preview59 the target set — no `UPDATE`, `DELETE`, or open transaction anywhere in it.60- A separate, minimal hand-off script containing the *only* mutating61 statement: a single guarded transaction (`UPDATE ... JOIN`) that compares62 actual vs. expected row count and rolls back automatically on mismatch.6364**Large batch (approaching or exceeding the lock-escalation threshold):**65- A single guarded transaction is not safe here — one statement holding locks66 on thousands of rows risks escalating to a table lock for the run's67 duration, and an unindexed table-variable join becomes an unpredictable68 query-plan bet at that row count.69- Generate one `UPDATE ... WHERE <primary key> = X AND <same guard predicate>`70 statement per row instead, each auto-committed (no single transaction spans71 more than one row's locks). Every statement carries its own idempotency72 guard, so re-running the whole file after a partial or interrupted run is a73 safe no-op on rows already fixed.74- Follow with a trailing count-based verification query (count of target rows75 still unfixed — expect 0) rather than an in-transaction row-count check.76- Keep any earlier small-batch script as a documented "legacy — small-batch77 fallback only" reference rather than deleting it, so the reason it doesn't78 scale stays attached to the code.7980Apply [`sql-server-safety`](../../../baselines/sql-server-safety/baseline.md)81to every script this skill generates — in particular: the read-only verify82script must contain zero mutating statements, the verify and mutate scripts83must share one written predicate rather than two independently maintained84copies, and no verification/completion query may carry `NOLOCK`.8586## Non-Goals8788- This does not replace DBA review for genuinely high-risk production89 mutations.90- This does not generate the recovery-chain logic itself (the join keys and91 extraction logic are specific to the systems involved) — it scaffolds the92 file structure, batching helper, and verify/mutate split around that logic.