You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Found while implementing #8413 (PR pending), out of that card's scope and not fixed there. Measured on origin/main @ 2bacd1aa.
What was measured
#8413 fixed the REMOTE Turso face so that a conflictKeys upsert whose target has no backing unique index refuses with an ADR-0112 envelope (VALIDATION_ERROR / 400) instead of a raw SqliteError. While confirming that the local face was the correct reference for that card, the same probe was run against the LOCAL face — which inherits SqlDriver.upsert:
Wider blast radius. The throw is in SqlDriver.upsert, so it is not a Turso concern at all — every dialect on driver-sql (SQLite, Postgres, MySQL) answers this condition with whatever raw error its own client raises. The wire shape therefore differs per dialect, which is the case an envelope exists to collapse.
mapDataError reads error.code / error.status to build the response envelope; with neither set this falls through to the default branch and ships { "error": "insert into ... values (...)" } — a raw SQL string, with no code, to the caller. That is both unbranchable and an information leak of the statement text, and it is exactly the argument invalidFilterError's docstring already records for the filter refusals (#1116 / #1077's envelope pass).
Adjacent but distinct: #8441 covers a raw driver code reaching client-facing payloads through the batch verbs' code limb. This one is the throw site itself on the upsert path.
Suggested disposition (not proposing the verdict)
The natural shape is the twin of what #8413 landed remotely — recognise the "conflict target is not a key" condition and re-raise it enveloped, first sentence for first sentence with the remote refusal (#5240, one condition one wording). Whether the dialect-spanning recognition is worth a shared predicate in @objectstack/types (beside isUniqueViolationError, which answers the opposite condition) is the part that needs a decision — SQLite answers on the message, and Postgres/MySQL wording for this condition is unmeasured.
Related: #8413 (the remote twin), #8441, #6543 (the shared unique-violation predicate), #1116 / #1077 (the envelope pass this continues).
Found while implementing #8413 (PR pending), out of that card's scope and not fixed there. Measured on
origin/main@2bacd1aa.What was measured
#8413 fixed the REMOTE Turso face so that a
conflictKeysupsert whose target has no backing unique index refuses with an ADR-0112 envelope (VALIDATION_ERROR/ 400) instead of a rawSqliteError. While confirming that the local face was the correct reference for that card, the same probe was run against the LOCAL face — which inheritsSqlDriver.upsert:code: 'SQLITE_ERROR',status: undefined— the identical un-enveloped shape #8413 was filed to remove, on the other face.Why it is a separate card
It is not the same defect and does not belong in #8413:
uniqueentirely — a declared-unique column takes duplicates, and an upsert onconflictKeysdies with a raw SqliteError #8413's remote instance was produced by the missing DDL — the index was never created, so a correctly-declaredunique: truecolumn hit it. Locally,syncDeclaredIndexesdoes create the index, so this reaches only the case where the caller names conflict keys over columns that were genuinely never declared unique. Rarer, and purely caller-side.SqlDriver.upsert, so it is not a Turso concern at all — every dialect ondriver-sql(SQLite, Postgres, MySQL) answers this condition with whatever raw error its own client raises. The wire shape therefore differs per dialect, which is the case an envelope exists to collapse.packages/drivers/driver-sql/src/sql-driver.ts, outside drivers(turso): the remote face dropsuniqueentirely — a declared-unique column takes duplicates, and an upsert onconflictKeysdies with a raw SqliteError #8413's declared surface.Why it is worth fixing
mapDataErrorreadserror.code/error.statusto build the response envelope; with neither set this falls through to the default branch and ships{ "error": "insert into ... values (...)" }— a raw SQL string, with nocode, to the caller. That is both unbranchable and an information leak of the statement text, and it is exactly the argumentinvalidFilterError's docstring already records for the filter refusals (#1116 / #1077's envelope pass).Adjacent but distinct: #8441 covers a raw driver
codereaching client-facing payloads through the batch verbs'codelimb. This one is the throw site itself on the upsert path.Suggested disposition (not proposing the verdict)
The natural shape is the twin of what #8413 landed remotely — recognise the "conflict target is not a key" condition and re-raise it enveloped, first sentence for first sentence with the remote refusal (#5240, one condition one wording). Whether the dialect-spanning recognition is worth a shared predicate in
@objectstack/types(besideisUniqueViolationError, which answers the opposite condition) is the part that needs a decision — SQLite answers on the message, and Postgres/MySQL wording for this condition is unmeasured.Related: #8413 (the remote twin), #8441, #6543 (the shared unique-violation predicate), #1116 / #1077 (the envelope pass this continues).
Generated by Claude Code