Running an AI agent to execute your D1 migrations will silently wreck your database — unless you explicitly forbid it from wrapping DDL in a transaction.
Claude Code, when handed a migration task, defaults to wrapping everything in BEGIN TRANSACTION / COMMIT because it looks safer. On D1 (SQLite under the hood), mixing DDL and DML in one transaction produces unpredictable behavior. The error you get — D1_ERROR: cannot start a transaction within a transaction — doesn't even hint at the real cause until you attach wrangler tail and watch the raw logs. I spent the better part of a day diagnosing this before I understood it was the agent being "helpful."
My ad analytics SaaS runs 12 Workers sharing 3 D1 databases. One bad migration cascades fast. The pattern I've settled on after 8 months is four discrete stages: add a nullable column (online, zero downtime), dual-write to both old and new columns for at least 24 hours, chunked backfill via Durable Object scheduler, then manual table recreation for the column drop. The backfill chunk size matters more than you'd think — I pushed 120k rows in one shot and hit the 30-second Worker timeout. Splitting into 1,000-row batches fixed it:
const batch = await db







