Skip to content

drivers(sql): the unbacked-conflict-target refusal is SQLite-only — Postgres and MySQL still answer the raw driver error #8567

Description

@os-zhuang

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:

  1. 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).
  2. 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

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions