Repository navigation
fix(objectql): the duplicate refusal reads in the writer's locale and names a declared composite unique index's fields - #33348
Conversation
…nd name a declared composite unique index's fields Claude-Session: https://claude.ai/code/session_016WV4cz2MxDkQYCC4U8indM Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cz2MxDkQYCC4U8indM Co-authored-by: Claude <noreply@anthropic.com>
…on a real driver; changeset Claude-Session: https://claude.ai/code/session_016WV4cz2MxDkQYCC4U8indM Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 176 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin f44d55ea5b382c9f88086eafbee18ca5f506745a && git checkout f44d55ea5b382c9f88086eafbee18ca5f506745a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6538ba48fc4590402cd6280cf83a8902f6de83d3 0b701b1551e4761e3e0c2a097f819c6f5e3014c1 && git checkout -B drift-repro 6538ba48fc4590402cd6280cf83a8902f6de83d3 && git merge --no-ff 0b701b1551e4761e3e0c2a097f819c6f5e3014c1
node scripts/docs-audit/affected-docs.mjs --json 6538ba48fc4590402cd6280cf83a8902f6de83d3
|
|
Contract-review-claim: session_016WV4d1PGptBW3QRQNmy5e6 · head af12b05 · 2026-11-14T17:16:40Z review#3. |
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment#33213 asks that a duplicate refusal read in the writer's language and say which combination collided. Composing at the producer (the engine, which holds the schema and the locale), and relaying in the classification (which holds neither), is the same split ruling C on #32440 set for refusal sentences. Matching driver columns against a declared index before naming anything turns driver text into a verdict only when the schema confirms it. Three keys chosen by how much can be named honestly is the smallest catalog that covers the cases. The design holds. Implemented-by: VERDICT: PASS |
|
domain:server#5 · os-elon-musk · session_016WV4cz2MxDkQYCC4U8indM ACCEPT · area A2 · Fixes #33213 · head 0b701b1 · 2026-11-14T18:21Z
|
Requested by Elon · project thread
Fixes #33213
Clause-②: yes (widening)
Metadata-protocol: none
Maintainer quick read
UNIQUE_VIOLATIONsentence is now composed by the engine in the writer's language from the platform operation-message catalog, and a composite unique index names its fields by label when that can be established.Accept-Language: zh-CNgot the English "A record with this value already exists" and could not tell which fields collided.code, 409 and the meaning offieldstay), only theerrortext moves; the English single-field and no-field sentences are byte-identical. Rollback is a revert of this PR.packages/objectql/src/duplicate-record-error.ts(declaredCompositeIndexandbuildEndUserMessage),packages/types/src/unique-violation.ts(newuniqueViolationColumns; the two existing functions answer as before),packages/spec/src/system/operation-message.ts(three new catalog keys).pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 src/engine-duplicate-record-label-sentence.test.ts; expect all green, including the zh-CN composite case asserting已存在「工厂」、「牌号」、「成本周次」均相同的记录.What changes
DuplicateRecordError(@objectstack/objectql) now renders its end-user sentence from the platform operation-message catalog (renderOperationMessage, the catalogDELETE_RESTRICTEDalready renders from) in the write's message context, so a deployment'serrors.duplicate_record*translation overrides it. It is carried in-process asendUserMessage, and theDUPLICATE_RECORDarm ofclassifyDataError(@objectstack/types) ships it as the 409 body'serror. That arm holds neither a locale nor a schema, which is why it composed fixed English before.en,zh-CN,ja-JP,es-ES):duplicate_record(names nothing),duplicate_record_field({{field}}, one label),duplicate_record_fields({{fields}}, a list spelled by the locale's existingfield_label_quoted/field_label_list_separatorrules). The Englishduplicate_recordandduplicate_record_fieldare byte-identical to the fixed text the HTTP doors answered, so an English deployment reads no change for one field or none.uniqueViolationColumns(error)(new,@objectstack/types) reads the composite key's columns from the SQLite and Postgres texts the existing readers already parse, organization part set aside asuniqueViolationFieldsets it aside. The engine names them only when that set equals thefieldsof a unique index ('global'or'organization') the object declares, and the user's sentence names them only when every one has a label (translation, else declaredlabel). Anything short of that names nothing: driver text alone cannot tell atenancy.tenantFieldcolumn from an authored one, and a wrong field name is worse than none.uniqueViolationColumn/uniqueViolationFieldanswer exactly as before (the shared reader was generalised over a reduction, not changed).codestaysUNIQUE_VIOLATION, the status 409,fieldkeeps its single-column contract (absent for a composite), and no key is added. The composite's labels also enter the engine'smessage(relayed asdeveloperMessage) and its API names enter the in-processdeveloperMessage.userMessage: that name on a thrown error is the ApiError contract's producer-marked channel, whichwithDeclaredUserMessagepromotes to its own wire key. Whether this refusal joins that channel is a contract decision this fix does not make, so the property isendUserMessageand the wire is unchanged (pinned:not.toHaveProperty('userMessage')).Tests
Pins added (zh-CN and en, single-field and multi-field, plus the fallbacks):
packages/objectql/src/engine-duplicate-record-label-sentence.test.ts— new top-leveldescribein the existing harness, through the real write doors (insert,updateby id) and the realmapDataError: a declaredunique: 'organization'index overplant + carbon_grade + cost_weeknamed in en (A record with the same “Plant”, “Carbon Grade”, “Cost Week” already exists) and zh-CN (已存在「工厂」、「牌号」、「成本周次」均相同的记录), the Postgres DETAIL shape, an uncatalogued locale (de-DE) falling back to the en sentence, a deploymenterrors.duplicate_record_fieldsoverride; five controls that must name nothing (no matching declared index, a non-unique match, no indexes, one unlabelled field, a MySQL index-only refusal), each asserting the unnamed sentence in zh-CN and en; a single unlabelled field in zh-CN. The two existing zh-CN single-field pins now expect已存在相同客户名称的记录(they pinned the English sentence around a Chinese label — the reported defect).packages/rest/src/rest-duplicate-record-arm.test.ts— a realObjectQLon real better-sqlite3 with a declared'global'composite index: the exact 409 body in en and zh-CN, nofield, no value.packages/spec/src/system/operation-message.test.ts— the three keys in every locale with exactly their own placeholder, en byte-identity with the previous wire text, zh-CN / base-language / unknown-locale rendering, theerrors.override.packages/types/src/unique-violation.test.ts—uniqueViolationColumnson SQLite and Postgres composites, no overlap withuniqueViolationField, andundefinedfor index names, expression parts and a NOT NULL refusal.Runs (heads 330e619 and af12b05)
pnpm --filter @objectstack/types test— Test Files 39 passed (39), Tests 907 passed (907).pnpm --filter @objectstack/spec test— Test Files 839 passed, 1 skipped (840); Tests 26834 passed, 1 skipped, 1 todo.pnpm --filter @objectstack/objectql test— 594 passed, 1 failed:federated-injected-column-readers.test.tsflagged anorganization_idliteral the first cut used in the matcher. Fixed in af12b05 by comparing the declared index as written (no literal); re-run at af12b05: that file plus the three duplicate-record files, Test Files 4 passed (4), Tests 119 passed (119). The full objectql suite was not re-run at af12b05 (lock contention); CI runs it.pnpm --filter @objectstack/rest exec vitest runonrest-duplicate-record-arm,import-runner-unique-violation-row,import-row-report-field-20701,error-response-structured-arm-door-parity,error-response-generic-passthrough-object-parityat af12b05 — Test Files 5 passed (5), Tests 135 passed (135).pnpm --filter @objectstack/types typecheck,@objectstack/objectql typecheck,@objectstack/spec typecheck— exit 0 at 330e619 (the af12b05 objectql source change passed its tsup DTS build in the rest closure build).Ablation (one-shot, not kept)
node scripts/ablation-replace.mjs --file packages/objectql/src/duplicate-record-error.ts --anchor "this.endUserMessage = buildEndUserMessage(...);" --replacement "this.endUserMessage = '';"under the verify lock, runningengine-duplicate-record-label-sentence.test.ts(the subject resolves fromsrc, so no rebuild leg): mutation landed (anchor 1 -> 0, blob 55cc4074 -> be7c0e75), Tests 24 failed / 37 passed (61) — red, the expected direction; restored (blob == HEAD 55cc4074,git diff HEADempty).Acceptance notes
for key 'idx_…'), never columns; mapping a physical index name to a declared index is the wider schema-lookup contractuniqueViolationColumn's doc already defers. The sentence is still in the writer's locale there.organization_iditself (fields: ['organization_id', 'a', 'b']) matches nothing and gets the unnamed (localized) sentence: the declared side is compared as written. The spec already steers new code tounique: 'organization', which matches.unique: trueindex (refused at parse since protocol 18, converted on stored rows) is not treated as unique by the matcher. The existing real-driver pin inrest-duplicate-record-arm.test.ts§2 registers such an object straight into the registry and keeps its unnamed sentence for that reason; the new real-driver pin declares'global'.classifyDataError(a unique error that did not pass through the engine envelope) keeps its fixed English sentence: that module holds no locale. Every engine write door wraps first, so the reported path is the envelope.envelope without endUserMessagefallback in the arm keeps the previous sentence for an envelope built by an older engine; every envelope this engine builds carries the sentence.renderOperationMessagerefusal, this takes the oneExecutionContext.localevalue, so a request locale the catalog lacks readsen, not the workspace default (the limitationApiErrorSchema.userMessage"Language" already records for the operation refusals).Verification
node scripts/pm/dispatch-gates.mjs --commandsderived 87 families for this diff at af12b05; all 87 run, 80 exit 0. Seven exited 3 = PREREQUISITE NOT MET, read as NOT MEASURED (they read built output that predates the source or needs a deeper clone):check-plugin-teardown-shape --self-test(shallow clone), speccheck:api-surface,check:dual-source-exports,check:entry-nameability,check:exported-any(specdistpredates a test-file edit),check:dual-build-cjs-loads,check:type-check-debt(closure not built). CI runs all of them on a clean build.--ranreconciliation: 87 derived, 87 run, 0 unrun.grep -naPover the diff: no match).origin/mainhas since moved 21 commits, none touching a file of this diff (git merge-treeclean).Human Minimum
packages/specholds no runtime logic.pnpm lintpasses.main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_016WV4cz2MxDkQYCC4U8indM