You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
rest/metadata-protocol: one unique conflict, two wire spellings on ONE route — a batch/import ROW reports DUPLICATE_RECORD while the whole-request failure on the same route reports UNIQUE_VIOLATION #14723
Filed under the contract review on #14541 (verdict PASS WITH CONDITIONS, condition 2), which required the residue to be disclosed and given a card rather than implied away. ⛔ This card does not pick a winner; that is the decision it exists to carry.
The fork
Two spellings of one condition — a driver's unique-constraint refusal — reach a client on the same route, chosen by whether the failure was whole-request or per-row.
Row level — DUPLICATE_RECORD.packages/metadata-protocol/src/protocol.ts's toRowApiError puts a thrown REGISTERED code on the row verbatim (ErrorCode.safeParse(err.code).success ? err.code : …), so a DuplicateRecordError raised for one row of POST /api/v1/data/:object/batch or POST /api/v1/batch reports code: 'DUPLICATE_RECORD', httpStatus: 409. Pinned at packages/runtime/src/batch-row-driver-text-real-driver.integration.test.ts:282. packages/rest/src/import-runner.ts:293 (first?.code ?? e?.code ?? 'IMPORT_ROW_FAILED') does the same for import row reports, and #14095's changeset records that as deliberate ("Import row reports improve … now DUPLICATE_RECORD").
Whole-request level — UNIQUE_VIOLATION. The single-record /data door has answered UNIQUE_VIOLATION since #6250/#7821, and #14389 restored it there for the envelope; #14541 restores it on the bulk / import / metadata / UI doors. So after #14541 a whole-request failure on POST /data/:object/batch or POST /data/:object/import answers UNIQUE_VIOLATION while a row-level failure on that same route answers DUPLICATE_RECORD.
Neither half is a regression and neither was introduced by #14541 — the fork has existed between the single door and the rows since #14389. What #14541 does is move the UNIQUE_VIOLATION side onto the routes whose ROWS speak the other spelling, so the two now sit side by side in one response for the first time, and that is why it wants deciding rather than living on.
a code either IS the standard member for its condition, or it is registered here — and if it merely re-spells a standard member, that registration is a recorded waiver
There is no waiver pairing UNIQUE_VIOLATION with DUPLICATE_RECORD, and standardSynonymOf('UNIQUE_VIOLATION') answers undefined because the token prong under-matches by design. ADR-0112's "one name per concept" is the other half.
Both stay, with a recorded waiver. Cheapest, and the ledger header names it as a legitimate shape — but it fixes the ADR-0112 objection by declaring the duplication rather than removing it, on a surface where the two spellings now appear in one response body.
Not claimed
No measurement here says a consumer is broken today. Both codes are registered, both bodies parse, and the fork is stable rather than newly introduced.
I did not census out-of-repo consumers of either spelling.
Filed under the contract review on #14541 (verdict
PASS WITH CONDITIONS, condition 2), which required the residue to be disclosed and given a card rather than implied away. ⛔ This card does not pick a winner; that is the decision it exists to carry.The fork
Two spellings of one condition — a driver's unique-constraint refusal — reach a client on the same route, chosen by whether the failure was whole-request or per-row.
Row level —
DUPLICATE_RECORD.packages/metadata-protocol/src/protocol.ts'stoRowApiErrorputs a thrown REGISTERED code on the row verbatim (ErrorCode.safeParse(err.code).success ? err.code : …), so aDuplicateRecordErrorraised for one row ofPOST /api/v1/data/:object/batchorPOST /api/v1/batchreportscode: 'DUPLICATE_RECORD',httpStatus: 409. Pinned atpackages/runtime/src/batch-row-driver-text-real-driver.integration.test.ts:282.packages/rest/src/import-runner.ts:293(first?.code ?? e?.code ?? 'IMPORT_ROW_FAILED') does the same for import row reports, and #14095's changeset records that as deliberate ("Import row reports improve … nowDUPLICATE_RECORD").Whole-request level —
UNIQUE_VIOLATION. The single-record/datadoor has answeredUNIQUE_VIOLATIONsince #6250/#7821, and #14389 restored it there for the envelope; #14541 restores it on the bulk / import / metadata / UI doors. So after #14541 a whole-request failure onPOST /data/:object/batchorPOST /data/:object/importanswersUNIQUE_VIOLATIONwhile a row-level failure on that same route answersDUPLICATE_RECORD.Neither half is a regression and neither was introduced by #14541 — the fork has existed between the single door and the rows since #14389. What #14541 does is move the
UNIQUE_VIOLATIONside onto the routes whose ROWS speak the other spelling, so the two now sit side by side in one response for the first time, and that is why it wants deciding rather than living on.The text this sits against
packages/spec/src/api/error-code-ledger.zod.ts's header:There is no waiver pairing
UNIQUE_VIOLATIONwithDUPLICATE_RECORD, andstandardSynonymOf('UNIQUE_VIOLATION')answersundefinedbecause the token prong under-matches by design. ADR-0112's "one name per concept" is the other half.What a decision has to weigh (not decided here)
DUPLICATE_RECORDwins. It is what an application is told to branch on for the engine envelope (DuplicateRecordError's own docblock: "branch oncode === 'DUPLICATE_RECORD'(ADR-0112) rather than on a dialect's code or message"), and it is already what the rows say. Cost: it moves the wire code on the single-record door, whichmapDataErrorhas noDUPLICATE_RECORDarm: the engine's insert conflict envelope reaches the wire through the generic passthrough, dropping thefieldkey and the user-facing conflict sentence #14389 explicitly refused to do on the grounds that "every consumer branching on this conflict today readsUNIQUE_VIOLATION, and renaming a wire code under existing consumers is a published-contract change, not a door's call".UNIQUE_VIOLATIONwins. It is the published spelling —content/docs/protocol/kernel/http-protocol.mdxdocuments the 409 "Constraint Violations" body with it, it is the standard-catalog member, and it is what the doors answer. Cost: it moves the row-level code that An application told to rely on a unique index cannot recognise the violation: ObjectQL rethrows the raw driver error andisUniqueViolationErroris not reachable from an app #14095 deliberately introduced, and the row pin above changes.Not claimed