Severity: P0. Reviewed at 82b715c. Three clauses, filed together because clause 1 alone leaves a client strictly worse off than the status quo.
The hole
A non-atomic batch that fails partway through has no normative behavior anywhere in SPEC.md.
- S6.2 (
SPEC.md:139) covers only the atomic case: "If an atomic batch fails partway through, the response is the error envelope in section 7, not a partial results array."
- The only statement of the non-atomic rule lives at
conformance/README.md:51 (B-4), which grants servers a permission SPEC.md never grants.
SPEC.md:13 says the spec prose is the normative content. The conformance doc is inventing normative content in a document with no authority to make it.
A client written against SPEC.md alone cannot know what a failed non-atomic batch returns.
Clause 1 -- forbid partial-success responses for non-atomic batches
Both reference servers already fail closed (worker runBatch and DO runBatch both throw out of the non-atomic loop rather than returning the partial out array), so this codifies shipped behavior. Add to S6.2 alongside the existing atomic sentence.
The alternative considered and rejected: a result | error union per statement. Forbid is cheaper and matches both reference servers, but only if clauses 2 and 3 ride with it -- see below.
Clause 2 -- error.statementIndex REQUIRED for non-atomic batch errors
Today error.statementIndex is OPTIONAL (SPEC.md:157). Fail-closed plus an optional index means the client learns that it failed and cannot learn how far it got. That combination is worse than the union.
Scope this to the non-atomic path only. Verified against both reference servers at 82b715c:
examples/cloudflare-worker-to-d1/src/index.ts -- the atomic branch is db.batch(prepared) and throws bare. Only the non-atomic for loop attaches (wrapped as { statementIndex?: number }).statementIndex = i.
examples/cloudflare-durable-object/src/index.ts -- same shape. ctx.storage.transactionSync(...) throws bare; only the non-atomic loop stamps the index.
- Both outer catches are conditional (
if (statementIndex !== undefined) error.statementIndex = statementIndex), so on an atomic failure the key is simply absent.
A blanket "REQUIRED for batch errors" would make both reference servers non-conforming the day it lands, and D1 batch failure does not surface a per-statement index to attach. Non-atomic is also the only case where the client needs it: on an atomic failure nothing persisted, so there is nothing to locate.
Scoping credit: smugglr, from the applier seat.
Considered and not filed: a SHOULD-emit on the atomic path for diagnostics. Zero consumers need it, D1 cannot satisfy it, and an unsatisfiable SHOULD is surface for its own sake.
Clause 3 -- state that preceding statements remain committed
This is the sentence nobody has, and it is the one a migration applier requires. Proposed text, verbatim from the consumer that needs it:
In a non-atomic batch, statements preceding the failing statement have been executed and their effects persist. Clients MUST NOT treat a non-atomic batch error as no-statements-applied.
Why it is load-bearing: in conformance/README.md, B-4's "fail closed" sits directly under B-3's "no rows persisted." Read together they imply nothing-applied. That is false, and it is exactly the false belief that corrupts a migration ledger on replay -- a client that re-drives a partially-applied batch believing nothing landed will double-apply every statement before the failure point.
Acceptance
Origin: codex spec review, verified against source by platform and smugglr. Related: the "when not rejected" qualifier at SPEC.md:206 points at a rejection mechanism that does not exist -- filed separately.
Severity: P0. Reviewed at 82b715c. Three clauses, filed together because clause 1 alone leaves a client strictly worse off than the status quo.
The hole
A non-atomic batch that fails partway through has no normative behavior anywhere in SPEC.md.
SPEC.md:139) covers only the atomic case: "If an atomic batch fails partway through, the response is the error envelope in section 7, not a partialresultsarray."conformance/README.md:51(B-4), which grants servers a permission SPEC.md never grants.SPEC.md:13says the spec prose is the normative content. The conformance doc is inventing normative content in a document with no authority to make it.A client written against SPEC.md alone cannot know what a failed non-atomic batch returns.
Clause 1 -- forbid partial-success responses for non-atomic batches
Both reference servers already fail closed (worker
runBatchand DOrunBatchboththrowout of the non-atomic loop rather than returning the partialoutarray), so this codifies shipped behavior. Add to S6.2 alongside the existing atomic sentence.The alternative considered and rejected: a
result | errorunion per statement. Forbid is cheaper and matches both reference servers, but only if clauses 2 and 3 ride with it -- see below.Clause 2 --
error.statementIndexREQUIRED for non-atomic batch errorsToday
error.statementIndexis OPTIONAL (SPEC.md:157). Fail-closed plus an optional index means the client learns that it failed and cannot learn how far it got. That combination is worse than the union.Scope this to the non-atomic path only. Verified against both reference servers at 82b715c:
examples/cloudflare-worker-to-d1/src/index.ts-- the atomic branch isdb.batch(prepared)and throws bare. Only the non-atomicforloop attaches(wrapped as { statementIndex?: number }).statementIndex = i.examples/cloudflare-durable-object/src/index.ts-- same shape.ctx.storage.transactionSync(...)throws bare; only the non-atomic loop stamps the index.if (statementIndex !== undefined) error.statementIndex = statementIndex), so on an atomic failure the key is simply absent.A blanket "REQUIRED for batch errors" would make both reference servers non-conforming the day it lands, and D1 batch failure does not surface a per-statement index to attach. Non-atomic is also the only case where the client needs it: on an atomic failure nothing persisted, so there is nothing to locate.
Scoping credit: smugglr, from the applier seat.
Considered and not filed: a SHOULD-emit on the atomic path for diagnostics. Zero consumers need it, D1 cannot satisfy it, and an unsatisfiable SHOULD is surface for its own sake.
Clause 3 -- state that preceding statements remain committed
This is the sentence nobody has, and it is the one a migration applier requires. Proposed text, verbatim from the consumer that needs it:
Why it is load-bearing: in
conformance/README.md, B-4's "fail closed" sits directly under B-3's "no rows persisted." Read together they imply nothing-applied. That is false, and it is exactly the false belief that corrupts a migration ledger on replay -- a client that re-drives a partially-applied batch believing nothing landed will double-apply every statement before the failure point.Acceptance
error.statementIndexREQUIRED for non-atomic batch errors, OPTIONAL otherwise.conformance/README.mdB-4 is rewritten to test the spec rule rather than grant a permission; the partial-success permission is removed.Origin: codex spec review, verified against source by platform and smugglr. Related: the "when not rejected" qualifier at
SPEC.md:206points at a rejection mechanism that does not exist -- filed separately.