wrangler d1 migrations apply fails where d1 execute --file succeeds
Status: root cause isolated, not yet filed with Cloudflare.
Impact: staging cannot be brought up to date. Any future migration
containing triggers will not deploy to production either.
Investigated: 2026-08-11, in a disposable D1 database created and deleted in
the same session. Staging and production were never touched.
Summary
npx wrangler d1 migrations apply <db> --remote fails with:
A request to the Cloudflare API (/accounts/…/d1/database/…/query) failed.
incomplete input: SQLITE_ERROR [code: 7500]
The identical migration file applies successfully through
wrangler d1 execute <db> --remote --file <same file> — 98 queries, no error.
The SQL is therefore valid and the remote API accepts it. The fault is in the
migrations apply code path, which appends its own ledger INSERT to the
migration text and re-splits the combined statement list differently than
--file does.
Minimal reproduction
Failing file: migrations/0043_partner_trust.sql (23,753 bytes, 21 triggers).
# Fails — incomplete input: SQLITE_ERROR [code: 7500]
npx wrangler d1 migrations apply <db> --remote
# Succeeds — "Executed 98 queries in 77.67ms"
npx wrangler d1 execute <db> --remote --file migrations/0043_partner_trust.sql
Both run against the same database, at the same migration position (42 prior
migrations applied), with the same wrangler version.
What was ruled out, and how
Every hypothesis below was tested and disproven. Recorded so nobody re-walks
them.
| Hypothesis |
Test |
Result |
| wrangler version regression |
Retried on 4.121.0 |
Same failure |
| …specifically a regression since production migrated |
Retried on 4.113.0, the version pinned in the lockfile at the commit that authored these migrations |
Same failure |
| Invalid SQL |
scripts/check-migrations-replay.mjs replays all 106 into empty SQLite |
106/106 clean |
| Invalid SQL (D1 semantics) |
Fresh local D1, migrations apply --local |
All applied |
| Triggers are unsupported remotely |
Single CREATE TRIGGER via --file to a remote DB |
Succeeded |
| Production never really had them |
SELECT COUNT(*) FROM sqlite_master WHERE type='trigger' on production |
93 triggers, incl. partner_payout_* from 0058 |
| Batch size / payload limit |
60 trivial non-trigger migrations via migrations apply |
All applied; wrangler applies one file per request |
BEGIN … ; END; on a single line breaks the splitter |
Two triggers, identical semantics, one multi-line and one single-line |
Both succeeded |
| The file itself is bad |
d1 execute --file on 0043 |
Succeeded, 98 queries |
The last two rows together are the finding: same file, same database, same
version — fails through one command, succeeds through the other.
Why staging reports a different first-failure than the replay
Staging has 0001–0057 applied and reports 49 pending from 0058. The
disposable replay started empty and stopped after 0042, i.e. on 0043. Both
are the same defect surfacing at whichever trigger-bearing migration each
database reaches first.
Proposed workaround — NOT YET IMPLEMENTED
Design only. Requires review before it is written or run.
For each pending migration, in ledger order:
wrangler d1 execute <db> --remote --file <migration> — the path proven to
work.
- Insert the ledger row that
migrations apply would have written, so the
migration is not re-applied later.
- Re-read the ledger and assert it advanced by exactly one, and that the
expected name is present, before moving to the next file.
Requirements this must meet:
- Ordered. Never skip or reorder; a data migration may depend on a schema
migration before it.
- Resumable. Re-running after an interruption must continue, not restart.
- Ledger-verified before and after every step, not just at the end.
- Aborts on the first failure rather than continuing.
- Must not run against production without a separate, explicit decision.
The ledger table name and row shape must be read from a database wrangler
itself has written, not assumed.
Filing
Not yet filed. When filed, against cloudflare/workers-sdk, include: the
summary, the two commands, the ruled-out table, wrangler versions 4.113.0 /
4.120.0 / 4.121.0 all reproducing, and the observation that --file succeeds
on identical input.
wrangler d1 migrations applyfails whered1 execute --filesucceedsStatus: root cause isolated, not yet filed with Cloudflare.
Impact: staging cannot be brought up to date. Any future migration
containing triggers will not deploy to production either.
Investigated: 2026-08-11, in a disposable D1 database created and deleted in
the same session. Staging and production were never touched.
Summary
npx wrangler d1 migrations apply <db> --remotefails with:The identical migration file applies successfully through
wrangler d1 execute <db> --remote --file <same file>— 98 queries, no error.The SQL is therefore valid and the remote API accepts it. The fault is in the
migrations applycode path, which appends its own ledgerINSERTto themigration text and re-splits the combined statement list differently than
--filedoes.Minimal reproduction
Failing file:
migrations/0043_partner_trust.sql(23,753 bytes, 21 triggers).Both run against the same database, at the same migration position (42 prior
migrations applied), with the same wrangler version.
What was ruled out, and how
Every hypothesis below was tested and disproven. Recorded so nobody re-walks
them.
scripts/check-migrations-replay.mjsreplays all 106 into empty SQLitemigrations apply --localCREATE TRIGGERvia--fileto a remote DBSELECT COUNT(*) FROM sqlite_master WHERE type='trigger'on productionpartner_payout_*from 0058migrations applyBEGIN … ; END;on a single line breaks the splitterd1 execute --fileon 0043The last two rows together are the finding: same file, same database, same
version — fails through one command, succeeds through the other.
Why staging reports a different first-failure than the replay
Staging has
0001–0057applied and reports 49 pending from0058. Thedisposable replay started empty and stopped after
0042, i.e. on0043. Bothare the same defect surfacing at whichever trigger-bearing migration each
database reaches first.
Proposed workaround — NOT YET IMPLEMENTED
Design only. Requires review before it is written or run.
For each pending migration, in ledger order:
wrangler d1 execute <db> --remote --file <migration>— the path proven towork.
migrations applywould have written, so themigration is not re-applied later.
expected name is present, before moving to the next file.
Requirements this must meet:
migration before it.
The ledger table name and row shape must be read from a database wrangler
itself has written, not assumed.
Filing
Not yet filed. When filed, against
cloudflare/workers-sdk, include: thesummary, the two commands, the ruled-out table, wrangler versions 4.113.0 /
4.120.0 / 4.121.0 all reproducing, and the observation that
--filesucceedson identical input.