Uh oh!
There was an error while loading. Please reload this page.
fix(types): require a violation phrasing in isUniqueViolationError's message limb (#8590) - #8730
Merged
Merged
Conversation
…message limb (#8590) The `unique constraint` limb matched a word pair, not a condition, so every sentence saying a unique constraint is ABSENT was claimed as a violation of one. Measured on live servers across all three supported dialect families (SQLite via better-sqlite3, PostgreSQL 16.13 via pg 8.22.0, MariaDB 10.11.14 via mysql2, all through knex 3.3.0), in both directions plus the NOT NULL / FOREIGN KEY near misses. The dialect sweep found a SECOND instance the card did not know about: PG 42830 (`there is no unique constraint matching given keys for referenced table`), raised by a FOREIGN KEY referencing a non-unique column, puts the pair adjacent in Postgres' own absence sentence. That rules out the negative-lookahead candidate, which is a blocklist keyed on SQLite's wording and still answers true there. The limb is now an allowlist of violation phrasings, restoring the module's stated default (unrecognised is false) to the message channel. Both spellings the retired limb covered are preserved: SQLite's `UNIQUE constraint failed: t.c` and Postgres' `violates unique constraint "..."`. The code/errno channels and the duplicate key/entry limbs are untouched. #8567's pin is inverted rather than deleted, and the absence sentences are pinned per dialect in a new suite covering the code channel too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NaS1PAHJcPfAA2acnV53Tn
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
This was referenced Aug 14, 2026
…que-violation-superstring
… (#8590) The paragraph still described the superstring collision as live and reasoned from it, and conditioned the disjointness prohibition on "while #8590 is open". All three claims are now false: the predicate no longer claims that error, the issue is closed, and the prohibition was never meant to expire — the two predicates answer inverse questions, so a limb travelling between them produces a confident inverted answer permanently, not until some card lands. Also records what the dialect sweep disproved: Postgres did not escape the collision "by luck of word order". PG 42830 puts `unique constraint` adjacent in its own absence sentence, which is why the fix is an allowlist of violation phrasings rather than a negative lookahead on SQLite's wording. Prose only — no emitted code changes, so no changeset. Folds in #8732. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NaS1PAHJcPfAA2acnV53Tn
qq9340100
marked this pull request as ready for review
August 14, 2026 17:24
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#8590
isUniqueViolationError's message limb was a bareunique constraint, and a word pair is not a condition. Every dialect that can say "this row violated a unique constraint" can also say "there is no unique constraint here", and the same two words sit adjacent in both — so the shared predicate answered true for errors meaning the exact opposite of what it detects.rest-server.tsmaps that verdict to409 UNIQUE_VIOLATION: a client told to change a value when nothing was ever compared, on a status an SDK will not retry.The limb now requires a violation phrasing —
unique constraint failed(SQLite) orviolates unique constraint(Postgres).Measured per dialect, on live servers
Ruling 1 of the dispatch, and the card's own caution. All three supported dialect families (
sql-driver.tsrecognises sqlite / postgres / mysql and no others), each driven through both conditions plus the NOT NULL and FOREIGN KEY near misses that share the wording:Duplicate entry/ER_DUP_ENTRY/ errno 1062 vocabulary was written forPostgres was not clean either, and that chose the fix
The card was filed reading the collision as SQLite-only, with Postgres escaping "by luck of word order". That reading was too kind. Sweeping the dialects raised PostgreSQL 42830 — a
FOREIGN KEYreferencing a non-unique column — where Postgres putsunique constraintadjacent in its own absence sentence, with noPRIMARY KEY orin front of it.That is what decided between the card's two candidates:
A lookahead is a blocklist: it can only ever enumerate the absence sentences somebody already tripped over, and it is keyed to one dialect's current wording. The allowlist restores the module's own stated default — unrecognised is
false— to the message channel, which is the direction the module already argues for everywhere else.Blast radius (the main risk, per Ruling 5)
Both spellings the retired limb covered are preserved exactly, per Ruling 2 — it was inherited verbatim from the REST branch #6250 replaced and covered SQLite's
UNIQUE constraint failed: t.cand Postgres'violates unique constraint "...". Theunique violation,duplicate keyandduplicate entrylimbs are untouched, as are thecodeanderrnochannels, so MySQL'sDuplicate entrypath never went through the narrowed limb at all.A mechanical scan of every string literal in the repo found 17 whose verdict moves. None is a regression: they are prose, a different predicate's vocabulary (
looksLikeInternalErrorLeakkeeps its own separate list inerror-leak.ts), fixtures asserted through the status-passthrough path, or the defect sentences themselves. Verified by reading and by test — the five consumer packages were run, not edited:Reachability: latent, confirmed — not raised
The card assumed latent. Confirmed by reading both gates.
SqlDriver.upsertis the only site compiling a caller-supplied conflict target, and itsisUnbackedConflictTargetErrorcheck runs first and unconditionally in the catch, throwing a refusal that declaresstatus: 400.mapDataErrorthen readsdeclaredHttpStatus(line 721) before the unique-violation branch (line 871), so the 400 wins.One nuance worth recording: that refusal keeps the raw driver error as its
cause, and this predicate walkscause— so the refusal object itself answeredtruebefore this change. Only the status gate stood between that and a wrong code on the wire, which is why the verdict is now pinned rather than left to it.Pins
unbacked-conflict-target.test.ts— drivers(sql): the unbacked-conflict-target refusal is SQLite-only — Postgres and MySQL still answer the raw driver error #8567 wrote its pin to point at itself rather than go quietly green. It is inverted, not deleted, and its prose rewritten; the SQLite row movestruetofalse.unique-violation-absence-sentences.test.ts(new) — the absence sentences per dialect, bare / knex-prefixed / plain string, plus the violation spellings, the near misses, the column-extractor gate, thecause-wrapped refusal, and the code channel (so re-readingcodecannot undo the message-side fix from the other side).Reverse verification: restoring the bare limb was predicted to produce 13 failures — 12 in the new suite and the flipped SQLite pin. It produced exactly those 13, test for test. The fix was committed first, so the restore came out of a real commit.
Docs correction folded in (second commit)
unbacked-conflict-target.ts's module head still described the collision as live, reasoned from it, and conditioned the disjointness prohibition on "while #8590 is open". All three claims are false after this change, and the prohibition was never meant to expire — the two predicates answer inverse questions permanently. Corrected in place, and the paragraph now also records that Postgres did not escape "by luck of word order" (42830). Prose only, no emitted code, so no second changeset. Folds in #8732, which is closed as shipped here.This was originally left out because the card's file surface was drawn to keep a verdict change off consumer packages; the PM extended the surface by this one file rather than leave a knowingly-false instruction about these two predicates in the tree — the same failure mode this card exists to fix, one file over.
Verification
Gates run after the final commit, at HEAD
3ebd8608e.mainmoved twice during this card; it is merged in (c1d030823, mergingff3e3fcc5) and the whole closure rebuilt before measuring, so the ratchets measure the tree that merges rather than a stale one:Re-derived against the actual diff with
scripts/pm/dispatch-gates.mjs; it surfaced the changeset family, which the dispatch prompt did not name because the changeset did not exist yet. Those were run too:check-empty-changeset,check-changeset-no-major,check-adr-0087-registrationall pass.Generated by Claude Code