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
mapDataError has no DUPLICATE_RECORD arm: the engine's insert conflict envelope reaches the wire through the generic passthrough, dropping the field key and the user-facing conflict sentence #14389
Found while implementing #14095 (the ObjectQL half of the 2026-09-01 unique-violation ruling). The engine change is correct and lands on its own; this is the REST-side half of the same condition, in domain:cli territory, filed rather than ridden on that PR.
What changes, and where
Since #14095engine.insert answers a driver unique violation with an ADR-0112 envelope: code: 'DUPLICATE_RECORD', status: 409, the driver error whole on cause, and field when uniqueViolationColumn determinably named the conflicting column.
classifyDataError (packages/rest/src/error-response.ts) reaches the declared-status passthrough arm first, because the envelope declares a status. That arm returns { status, body: { error, ...thrownCodeFields(error, status), object } } — it ships no structured fields of its own. The dedicated isUniqueViolationError arm further down, which builds 409 UNIQUE_VIOLATIONwith a field key, is never reached for an insert conflict any more.
Measured, end to end
Real engine, real drivers, the real mapDataError. AFTER is the envelope; BEFORE is the same conflict's raw driver error handed to the same boundary:
driver
index
status
code
field on the body
driver-sqlite-wasm
single column
409 → 409
UNIQUE_VIOLATION → DUPLICATE_RECORD
email → absent
driver-sqlite-wasm
composite
409 → 409
UNIQUE_VIOLATION → DUPLICATE_RECORD
absent → absent (composite names none, by contract)
driver-memory
single column
409 → 409
UNIQUE_VIOLATION → DUPLICATE_RECORD
absent → absent
driver-memory
composite
409 → 409
UNIQUE_VIOLATION → DUPLICATE_RECORD
absent → absent
So: the status is unaffected — the passthrough honours the declared 409 on every driver, which is the good half. Two things do move:
The sentence changes from A record with this email already exists to the engine's own Duplicate record refused on 'duly_note': a unique constraint on 'email' already holds this value. No record was written. Accurate, but it is the platform's sentence, not the one the 409 arm curated for an end user.
For driver-memory the change is an improvement in one respect: that driver's own refusal already declared status: 409, so it ALREADY took the passthrough, and its raw message — which echoes the offending values as JSON (a record with the values {"email":"a@b.example"} already exists) — was reaching the client. The envelope replaces it with a sentence carrying no values.
The shape of the fix
A dedicated DUPLICATE_RECORD arm in classifyDataError, placed with the other structured 409s (DELETE_RESTRICTED, CONCURRENT_UPDATE) ahead of the declared-status passthrough — that placement already exists in the file for exactly this reason ("Surfaced FIRST so the structured fields survive the generic catch-alls"). The envelope carries everything such an arm needs: object, field, developerMessage, and cause.
Open questions for whoever takes it, both wire-contract decisions rather than mechanics:
Which code should the wire speak — DUPLICATE_RECORD (what the producer now declares) or UNIQUE_VIOLATION (what clients see today)? Both are registered. The ADR-0112 rule is that the producer names the condition, which argues for the former; back-compatibility argues for the latter, and there is a third option where the arm keeps UNIQUE_VIOLATION on the wire and puts the producer's spelling in declaredCode.
Which sentence — the engine's, or the arm's curated end-user wording plus developerMessage beside it (the DELETE_RESTRICTED split).
Not a regression in the importer
Measured separately: the import row report READS err.message and err.code (toFailedResult → sanitizeRowError). Its code improves from a dialect token (SQLITE_CONSTRAINT_UNIQUE, 11000) to DUPLICATE_RECORD, and the message is the engine's sentence, which the SQL backstop passes through unchanged (it names no statement). No fix owed there.
Triage housekeeping (R+98): the Blocked-by: #14095 line has been removed from this body. #14095 closed as completed on 2026-09-02T06:52Z via PR #14405 — the envelope this card reacts to is on main now, so the line was inert and misrepresented the card as gated. This card is pm:dispatched with PR #14544 open against it, which is the live state; nothing else changes.
Found while implementing #14095 (the ObjectQL half of the 2026-09-01 unique-violation ruling). The engine change is correct and lands on its own; this is the REST-side half of the same condition, in
domain:cliterritory, filed rather than ridden on that PR.What changes, and where
Since #14095
engine.insertanswers a driver unique violation with an ADR-0112 envelope:code: 'DUPLICATE_RECORD',status: 409, the driver error whole oncause, andfieldwhenuniqueViolationColumndeterminably named the conflicting column.classifyDataError(packages/rest/src/error-response.ts) reaches the declared-status passthrough arm first, because the envelope declares astatus. That arm returns{ status, body: { error, ...thrownCodeFields(error, status), object } }— it ships no structured fields of its own. The dedicatedisUniqueViolationErrorarm further down, which builds409 UNIQUE_VIOLATIONwith afieldkey, is never reached for an insert conflict any more.Measured, end to end
Real engine, real drivers, the real
mapDataError.AFTERis the envelope;BEFOREis the same conflict's raw driver error handed to the same boundary:fieldon the bodydriver-sqlite-wasmUNIQUE_VIOLATION→DUPLICATE_RECORDemail→ absentdriver-sqlite-wasmUNIQUE_VIOLATION→DUPLICATE_RECORDdriver-memoryUNIQUE_VIOLATION→DUPLICATE_RECORDdriver-memoryUNIQUE_VIOLATION→DUPLICATE_RECORDSo: the status is unaffected — the passthrough honours the declared 409 on every driver, which is the good half. Two things do move:
fieldkey is gone on the dialects that name a column. That key is what an import UI or a Console form highlights ("A record with thisemailalready exists"), and it is the whole point of import-runner's sanitizeRowError keeps its own three-dialect unique regexes — deferred from #6250 to avoid a merge race with #4633 #6544 / REST: the UNIQUE_VIOLATION 409 message is hard-coded English and carries no field — and it is less informative than the bulk path's own message for the same constraint #7821.A record with this email already existsto the engine's ownDuplicate record refused on 'duly_note': a unique constraint on 'email' already holds this value. No record was written.Accurate, but it is the platform's sentence, not the one the 409 arm curated for an end user.For
driver-memorythe change is an improvement in one respect: that driver's own refusal already declaredstatus: 409, so it ALREADY took the passthrough, and its raw message — which echoes the offending values as JSON (a record with the values {"email":"a@b.example"} already exists) — was reaching the client. The envelope replaces it with a sentence carrying no values.The shape of the fix
A dedicated
DUPLICATE_RECORDarm inclassifyDataError, placed with the other structured 409s (DELETE_RESTRICTED,CONCURRENT_UPDATE) ahead of the declared-status passthrough — that placement already exists in the file for exactly this reason ("Surfaced FIRST so the structured fields survive the generic catch-alls"). The envelope carries everything such an arm needs:object,field,developerMessage, andcause.Open questions for whoever takes it, both wire-contract decisions rather than mechanics:
codeshould the wire speak —DUPLICATE_RECORD(what the producer now declares) orUNIQUE_VIOLATION(what clients see today)? Both are registered. The ADR-0112 rule is that the producer names the condition, which argues for the former; back-compatibility argues for the latter, and there is a third option where the arm keepsUNIQUE_VIOLATIONon the wire and puts the producer's spelling indeclaredCode.developerMessagebeside it (theDELETE_RESTRICTEDsplit).Not a regression in the importer
Measured separately: the import row report READS
err.messageanderr.code(toFailedResult→sanitizeRowError). Itscodeimproves from a dialect token (SQLITE_CONSTRAINT_UNIQUE,11000) toDUPLICATE_RECORD, and the message is the engine's sentence, which the SQL backstop passes through unchanged (it names no statement). No fix owed there.Triage housekeeping (R+98): the
Blocked-by: #14095line has been removed from this body. #14095 closed as completed on 2026-09-02T06:52Z via PR #14405 — the envelope this card reacts to is onmainnow, so the line was inert and misrepresented the card as gated. This card ispm:dispatchedwith PR #14544 open against it, which is the live state; nothing else changes.Generated by Claude Code