|
4531 | 4531 | "title": "Field-level `unique` on the write path: colliding writes refused 409 UNIQUE_VIOLATION on both driver families, NULL-distinct, organization-scoped by default, whole-batch refusal, autonumber resync converges", |
4532 | 4532 | "since": "v17", |
4533 | 4533 | "status": "active", |
4534 | | - "revision": 11, |
| 4534 | + "revision": 12, |
4535 | 4535 | "priority": "P1", |
4536 | 4536 | "surface": "api", |
4537 | 4537 | "personas": [ |
|
4560 | 4560 | ], |
4561 | 4561 | "acceptance": [ |
4562 | 4562 | { |
4563 | | - "clause": "a colliding create is REFUSED, not landed: status 409, code UNIQUE_VIOLATION (ADR-0112 envelope), whose developerMessage NAMES THE OBJECT by its declared label ('Duplicate record refused on 'LABEL': …'), by its API name ('qa_unique') only where the object declares no label, and states 'No record was written.', while the user-facing `error` stays generic ('A record with this value already exists'); NEITHER carries a driver prefix, a driver statement or bound values — and the row count is unchanged. On the live SQLite leg the field is named only when the driver reports a single offending column (a tenant-scoped unique index is composite, so the sentence reads 'a unique constraint already holds these values'); the `object.field` wording is the memory driver's and stays a memory-pin assertion", |
| 4563 | + "clause": "a colliding create is REFUSED, not landed: status 409, code UNIQUE_VIOLATION (ADR-0112 envelope), whose developerMessage NAMES THE OBJECT by its declared label ('Duplicate record refused on 'LABEL': …'), by its API name ('qa_unique') only where the object declares no label, and states 'No record was written.', while the user-facing `error` names the field by its label ('A record with this LABEL already exists') where a single offending column is reported and the field has a label, and is otherwise generic ('A record with this value already exists') — never by its API key, which stays on `field`; NEITHER carries a driver prefix, a driver statement or bound values — and the row count is unchanged. On the live SQLite leg the field is named only when the driver reports a single offending column (a tenant-scoped unique index is composite, so the sentence reads 'a unique constraint already holds these values'); the `object.field` wording is the memory driver's and stays a memory-pin assertion", |
4564 | 4564 | "oracle": "api", |
4565 | | - "verify": "the refusal body — status, code, and the developerMessage naming the object, read from developerMessage, not from the generic `error` — + a post-refusal count; the SQL-family envelope is single-sourced at packages/objectql/src/duplicate-record-error.ts (buildDuplicateMessage / envelopeUniqueViolation); the memory driver's `object.field` sentence (memory-unique-constraint.ts uniqueViolationError) is asserted by memory-unique-constraint.test.ts, not live", |
| 4565 | + "verify": "the refusal body — status, code, and the developerMessage naming the object, read from developerMessage, not from the user-facing `error` — + a post-refusal count; the SQL-family envelope is single-sourced at packages/objectql/src/duplicate-record-error.ts (buildDuplicateMessage / envelopeUniqueViolation); the memory driver's `object.field` sentence (memory-unique-constraint.ts uniqueViolationError) is asserted by memory-unique-constraint.test.ts, not live", |
4566 | 4566 | "evidence": "refusal response + before/after counts" |
4567 | 4567 | }, |
4568 | 4568 | { |
|
4607 | 4607 | ], |
4608 | 4608 | "automated": { |
4609 | 4609 | "kind": "dogfood", |
4610 | | - "ref": "packages/qa/dogfood/test/field-unique-rest-refusal.dogfood.test.ts, at the REST data doors of a booted stack on the stock SQL family (bootStack's SQLite datasource), on the scratch object the fixtures prescribe (sku unique:true, gsku unique:'global', nick): acceptance[0], negative[0] and negative[1] (a colliding create is 409 UNIQUE_VIOLATION; the developerMessage names the object by its label and says 'No record was written.'; on the tenant-scoped composite index the error is the generic 'A record with this value already exists', on the single-column global index it names the field; no driver prefix, statement or bound value; the colliding row is not stored), acceptance[1] (two rows with no sku both land), acceptance[2] (a PATCH onto a taken value is 409 and the row keeps its value; a PATCH leaving sku alone, or re-sending its own value, is 200) and acceptance[3] with negative[2] at the REST door that stamps one value onto two rows, POST /data/OBJECT/updateMany with options.atomic (its per-row report refuses both, ROLLED_BACK then UNIQUE_VIOLATION, and both rows re-read unchanged; REST has no predicate-update door, so the engine's predicate updateMany stays the memory pin's). packages/drivers/driver-memory/src/memory-unique-constraint.test.ts — acceptance[4]'s memory-family legs (the \"[#13197] scoping matches driver-sql's ADR-0120 D1/D3 rule\" describe), acceptance[5] ('the refusal is recognisable to the ENGINE, so the autonumber resync converges'), negative[2]'s predicate half ('updateMany refuses BEFORE mutating anything — no half-applied batch') and negative[3]'s memory-family half, at the memory driver; + packages/drivers/driver-sql/src/sql-driver-8577-tenant-scoped-declared-unique.test.ts + packages/drivers/driver-sql/src/sql-driver-declared-index-organization-respelling.test.ts — acceptance[4]'s SQL-family legs and negative[3]'s SQL-family half, at SqlDriver over better-sqlite3, through a test-local createAsApi mapping; driver-level evidence beside acceptance[0]–[3], never their pass" |
| 4610 | + "ref": "packages/qa/dogfood/test/field-unique-rest-refusal.dogfood.test.ts, at the REST data doors of a booted stack on the stock SQL family (bootStack's SQLite datasource), on the scratch object the fixtures prescribe (sku unique:true, gsku unique:'global', nick): acceptance[0], negative[0] and negative[1] (a colliding create is 409 UNIQUE_VIOLATION; the developerMessage names the object by its label and says 'No record was written.'; on the tenant-scoped composite index the error is the generic 'A record with this value already exists', on the single-column global index it names the field by its label ('Global SKU'), the API key 'gsku' only on `field`; no driver prefix, statement or bound value; the colliding row is not stored), acceptance[1] (two rows with no sku both land), acceptance[2] (a PATCH onto a taken value is 409 and the row keeps its value; a PATCH leaving sku alone, or re-sending its own value, is 200) and acceptance[3] with negative[2] at the REST door that stamps one value onto two rows, POST /data/OBJECT/updateMany with options.atomic (its per-row report refuses both, ROLLED_BACK then UNIQUE_VIOLATION, and both rows re-read unchanged; REST has no predicate-update door, so the engine's predicate updateMany stays the memory pin's). packages/drivers/driver-memory/src/memory-unique-constraint.test.ts — acceptance[4]'s memory-family legs (the \"[#13197] scoping matches driver-sql's ADR-0120 D1/D3 rule\" describe), acceptance[5] ('the refusal is recognisable to the ENGINE, so the autonumber resync converges'), negative[2]'s predicate half ('updateMany refuses BEFORE mutating anything — no half-applied batch') and negative[3]'s memory-family half, at the memory driver; + packages/drivers/driver-sql/src/sql-driver-8577-tenant-scoped-declared-unique.test.ts + packages/drivers/driver-sql/src/sql-driver-declared-index-organization-respelling.test.ts — acceptance[4]'s SQL-family legs and negative[3]'s SQL-family half, at SqlDriver over better-sqlite3, through a test-local createAsApi mapping; driver-level evidence beside acceptance[0]–[3], never their pass" |
4611 | 4611 | }, |
4612 | 4612 | "source": [ |
4613 | 4613 | "packages/drivers/driver-memory/src/memory-unique-constraint.ts#scope (#13197/#13249, commit 56c093c — single judgment point; scope table read off driver-sql, arm for arm)", |
|
4683 | 4683 | "date": "2026-10-27", |
4684 | 4684 | "change": "automated.ref names the dogfood pin that scores acceptance[0]–[3] and negative[0]–[2] over REST on the SQL family (#26573 slice 3); 'NO pin drives a REST door' no longer holds. Measured: the global index names the field in the user-facing error by its API key (gsku) and in developerMessage by its label; the composite tenant index names none. acceptance[3]'s REST door is the atomic updateMany records batch, since REST has no predicate update. kind unit → dogfood. No clause changed", |
4685 | 4685 | "ref": "#26573" |
| 4686 | + }, |
| 4687 | + { |
| 4688 | + "revision": 12, |
| 4689 | + "date": "2026-10-27", |
| 4690 | + "change": "acceptance[0].clause: the user-facing `error` NAMES THE FIELD BY ITS LABEL ('A record with this LABEL already exists') where the driver reports a single offending column and the field has a label, and is generic otherwise, where it named the field by its API key ('A record with this gsku already exists') while developerMessage named it by label. The engine's DuplicateRecordError carries the label it resolves for its sentence as fieldLabel (packages/objectql/src/duplicate-record-error.ts, in the writer's locale), and the REST classification names the field by it (packages/types/src/data-error-classification.ts); `field` keeps the API key. automated.ref's sentence follows it, and the dogfood pin's exact body reads 'A record with this Global SKU already exists'. Status 409, code UNIQUE_VIOLATION, developerMessage and the composite-index generic sentence are unchanged", |
| 4691 | + "ref": "#26867" |
4686 | 4692 | } |
4687 | 4693 | ] |
4688 | 4694 | }, |
|
0 commit comments