Filed out of #8445, which fixed the same condition on the SQLite face only. Unassigned; recording what was and was not measured.
What #8445 landed
SqlDriver.upsert now recognises "the ON CONFLICT target is not backed by a PRIMARY KEY or UNIQUE index" and re-raises it as an ADR-0112 envelope (VALIDATION_ERROR / 400), first sentence for first sentence with #8413's remote refusal (#5240).
Recognition is a narrow match on SQLite's own sentence, which is the only channel SQLite fills — it raises a plain SQLITE_ERROR, the same generic code a syntax error carries, so code cannot discriminate. Measured through knex + better-sqlite3 (the message knex builds is the statement, then -, then SQLite's text):
upsert('plain', { email: 'a@b.com', title: 'x' }, ['email'])
-> name=SqliteError code=SQLITE_ERROR status=undefined
msg=insert into `plain` (...) values (...) on conflict (`email`) do update set ...
- ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint
What is left
driver-sql serves three dialects. Postgres and MySQL wording for this condition was not measured — the dev container that implemented #8445 has no Postgres or MySQL server to raise it, and #8445's dispatch was explicit that transcribing the other two dialects' text from memory is not acceptable evidence. So on those two dialects the raw driver error still escapes, mapDataError still falls through to its default branch, and the caller still gets the statement text with no code to branch on — #8445's payload argument, unchanged, on two of three dialects.
Two questions, in order:
- Measure. What do Postgres and MySQL actually raise here? Postgres is expected to have both a SQLSTATE channel and prose for it; MySQL has no
ON CONFLICT syntax at all — knex compiles onConflict().merge() to ON DUPLICATE KEY UPDATE, which takes no conflict target, so the condition may not arise on that dialect in the same form (and if it does not, what it does instead is worth knowing, because silently merging on a different unique key is its own defect). - Then decide the home. If the recognition turns out to span dialects, the natural place is a named predicate in
@objectstack/types beside isUniqueViolationError — its UniqueViolationSignature table (codes / errnos / message substrings) is already the shape this question needs, and every consumer already depends on that package. ⚠️ It must stay a separate predicate: isUniqueViolationError answers the OPPOSITE condition (a unique index exists and the row violated it), and merging them would report a working constraint as a missing one.
If (1) shows the dialects have nothing in common, the per-face recognition #8445 landed is the right end state and this card closes as measured-and-declined.
Related: #8445 (the SQLite half, and where the current recognition lives), #8413 (the remote-face twin), #6543 / #6250 (the shared unique-violation predicate this would sit beside), #1116 / #1077 (the envelope pass this continues).
Generated by Claude Code
Filed out of #8445, which fixed the same condition on the SQLite face only. Unassigned; recording what was and was not measured.
What #8445 landed
SqlDriver.upsertnow recognises "theON CONFLICTtarget is not backed by a PRIMARY KEY or UNIQUE index" and re-raises it as an ADR-0112 envelope (VALIDATION_ERROR/ 400), first sentence for first sentence with #8413's remote refusal (#5240).Recognition is a narrow match on SQLite's own sentence, which is the only channel SQLite fills — it raises a plain
SQLITE_ERROR, the same generic code a syntax error carries, socodecannot discriminate. Measured through knex + better-sqlite3 (the message knex builds is the statement, then-, then SQLite's text):What is left
driver-sqlserves three dialects. Postgres and MySQL wording for this condition was not measured — the dev container that implemented #8445 has no Postgres or MySQL server to raise it, and #8445's dispatch was explicit that transcribing the other two dialects' text from memory is not acceptable evidence. So on those two dialects the raw driver error still escapes,mapDataErrorstill falls through to its default branch, and the caller still gets the statement text with nocodeto branch on — #8445's payload argument, unchanged, on two of three dialects.Two questions, in order:
ON CONFLICTsyntax at all — knex compilesonConflict().merge()toON DUPLICATE KEY UPDATE, which takes no conflict target, so the condition may not arise on that dialect in the same form (and if it does not, what it does instead is worth knowing, because silently merging on a different unique key is its own defect).@objectstack/typesbesideisUniqueViolationError— itsUniqueViolationSignaturetable (codes / errnos / message substrings) is already the shape this question needs, and every consumer already depends on that package.isUniqueViolationErroranswers the OPPOSITE condition (a unique index exists and the row violated it), and merging them would report a working constraint as a missing one.If (1) shows the dialects have nothing in common, the per-face recognition #8445 landed is the right end state and this card closes as measured-and-declined.
Related: #8445 (the SQLite half, and where the current recognition lives), #8413 (the remote-face twin), #6543 / #6250 (the shared unique-violation predicate this would sit beside), #1116 / #1077 (the envelope pass this continues).
Generated by Claude Code