fix(driver-turso): the remote face refuses a missing table or column with the local face's code instead of answering [] - #20461
Conversation
…with the local face's code instead of answering [] RemoteTransport.aggregate's catch answered `no such table` / `no such column` with [], and the terminal of find's projection backstop answered `no such column` with []. The transport now lets the backend's error out (find keeps the local ladder's projection and ORDER BY rungs first), and TursoDriver's remote read exits classify it with the local face's inherited seam, SqlDriver.aggregateBackendFault: INVALID_FIELD / 400, INVALID_FILTER / 400 or DATABASE_ERROR / 500, as the local face answers over the same file. Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN Co-authored-by: Claude <noreply@anthropic.com>
…umn refusal Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 15 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 5 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 7 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 f3a610b4b9cea36321f24d160394f9c8aafa2643 && git checkout f3a610b4b9cea36321f24d160394f9c8aafa2643
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin acd009521e6e8e4d3cb9d6df43d84aa53c44b4b3 0c47b7bbe7cfd4ef10c0d436ab489ce4c70b9935 && git checkout -B drift-repro acd009521e6e8e4d3cb9d6df43d84aa53c44b4b3 && git merge --no-ff 0c47b7bbe7cfd4ef10c0d436ab489ce4c70b9935
node scripts/docs-audit/affected-docs.mjs --json acd009521e6e8e4d3cb9d6df43d84aa53c44b4b3
|
…NG banner and ADR-0087 disposition The seat's amended claim rules the remote-face refusal an accept-set narrowing: minor under the launch-window convention, the BREAKING banner, a FROM -> TO line, and the ADR-0087 disposition not-required (already-registered driver-sql-unresolvable-where-column-refused). Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs read: card #20424 (body and all 5 comments, including the amended claim 5872640432 and both os-dev reports), PR #20461 (body, file list, the one docs-drift comment, no reviews), the net diff against the merge base ① Derived judgmentsEvery accept-set and public-surface change the diff implies, each named:
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
…holds remote-transport.ts: find()'s no-such-column backstop refuses under SQLITE_DQS=0 rather than answering []. The #20107 parity suite's header prediction for its first ablation leg: aggregate now refuses the missing table as DATABASE_ERROR / 500. Text only. Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Delta review over the PASS 5873239611 at ① Derived judgments
② Semver levelUnchanged and still right: ③ Boundary flags
Implemented-by: VERDICT: PASS |
…ot the swallowed empty list The filter-position passage said the driver's "no such column" is swallowed and the list comes back empty. driver-sql, and the local faces of TursoDriver/SqliteWasmDriver (both extend SqlDriver), have refused an unresolvable WHERE column with INVALID_FILTER / 400, naming the column, since #8790. The Turso remote face refuses the same way as of 3e8b492 (#20461), through the inherited aggregateBackendFault seam. Rewrote the passage to the refusal and its remedy (schema sync, or naming a column the table has), and reworded the "why an error not a warning" rationale to match. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VDtqoecgES7ScQYGbFVDRv
Fixes #20424
Clause-②: no (narrowing)
Seat ruling: the amended claim 5872640432 on #20424 rules this change an accept-set narrowing:
Clause-②: no (narrowing),@objectstack/driver-tursominorwith the BREAKING banner, and the ADR-0087 dispositionnot-required (already-registered driver-sql-unresolvable-where-column-refused).Summary
On the Turso remote face,
aggregate,find,findOneandcountnow answer a missing table or a missing column the way the local face of the same driver answers it over the same file:DATABASE_ERROR/ 500,INVALID_FIELD/ 400 orINVALID_FILTER/ 400, and never "no rows". Two catches inRemoteTransportread the backend'sno such table/no such columnas[]. They now let the error out, andTursoDriver's remote read exits classify it with the local face's inherited seam,SqlDriver.aggregateBackendFault. No code is minted, nothing indriver-sqlorspecis edited, and no classification is copied.Measured head:
daad09aa1(this branch after mergingorigin/mainat3cf644938). Patch round 1 changed only the changeset and this body, then mergedorigin/mainat2304b1608: head3b56eb769, withpackages/driversbyte-identical todaad09aa1(see "Patch round 1" below).H1: reproduced on
mainbefore any changeBase
6e3e5462c. The remote face is aTursoDriverover a real@libsql/clienton afile:database, and the local control is aTursoDriverover the same file (a throwaway probe, not committed). "Declared field, column absent" is a field the object declares and the table lacks.aggregate, mapped table really absentDATABASE_ERROR/ 500[]aggregategrouped by a declared field, column absentINVALID_FIELD/ 400[]findwhosewherenames that fieldINVALID_FILTER/ 400[]findOne, the samewhereINVALID_FILTER/ 400nullfindwith a projection and thatwhereINVALID_FILTER/ 400[]count, the samewhereINVALID_FILTER/ 400DATABASE_ERROR/ 500aggregatesumming that fieldINVALID_FIELD/ 400[]aggregatewhosewherenames that fieldINVALID_FILTER/ 400[]findordered by that field[]findprojecting that fieldfind, mapped table absentDATABASE_ERROR/ 500DATABASE_ERROR/ 500H1 holds for all three card rows. H4 holds too: rows 1 to 3 answered the same way for a managed object whose synced table or column was dropped under it (
aggregate[],aggregategrouped[],find[], where the local face refused with 500 / 400 / 400).H2: where the
[]came from, and why each catch existedRemoteTransport.aggregate's catch.git log -Sfinds it in the method's first version,101d5c345("Add i18n, analytics, turso aggregate, auth date normalization", 2026-05-16). It had no comment, no test and no card, and nothing in that commit names a first-boot or not-yet-created table. The local face refuses both conditions (a missing table since driver-sql (PG): sum/avg/min/max over a boolean column throw the raw PostgreSQL 42883 with no ADR-0112 envelope (status undefined) #11455, a missing column since driver-sql: an unresolvable WHERE column onaggregate()answers DATABASE_ERROR/500 wherefind()andcount()answer INVALID_FILTER/400 — the #8790 refusal never reached the third read door #11541), so under the order's rule there is no case to keep. The catch is removed.RemoteTransport.find's$selectbackstop. It arrived with the migration from the cloud repository (06ba03627). Its comment names one case, a list view that projects fields the object lacks, and that case is kept: the projection is still dropped and the rows answer. Its terminalreturn [](no projection to drop, or the retry failed too) was the pre-driver-sql: one unresolvable WHERE column, two answers —find()silently returns [] whilecount()throws a raw dialect error with no ADR-0112 envelope #8790 behaviour it mirrored. The local face's terminal became a refusal in716ac9bf8, and this copy never followed. The unit pinstill returns empty when even SELECT * fails (e.g. unknown table)asserted that[], and its stub threwno such column, not a table error: a realno such tablewas already rethrown by this catch. It is replaced, with the same input and the opposite assertion.H3: the seam. The mechanism is falsified; the seam is reachable
The hypothesis was a field check that decides before the statement runs. The local face has none for this condition: the ingress field checks pass because the field is declared. The local face decides after the statement runs, from the backend's error.
countandfindRowssend an unresolvable column tounresolvableFilterColumnRefusaland everything else tobackendStatementFault.aggregategoes throughaggregateBackendFault, which attributes the column to the groupBy, the aggregation or thewherefrom the caller's own query.All of those compositions are
protectedonSqlDriver, andTursoDriverinherits them. The class predicate they share,isUnresolvableColumnError, is a module function that@objectstack/driver-sqldoes not export. So:aggregateexit callsaggregateBackendFault(object, query, error)with the caller's own query. That is the local exit verbatim.find/findOne/countexits call the same method with thewherealone. With no groupBy and no aggregation, arm 1 cannot fire. Arm 2 isunresolvableFilterColumnRefusal(object, error, where), and the terminal isbackendStatementFault, which is the local exit. The two can differ only on a recognised wording whose column name does not parse, and that cannot arise on libSQL, whose only wording isno such column: NAME.remoteReadFaultfirst returns anything that already declares a numericstatusunchanged. That is the same "is it already ours" gatebackendStatementFaultapplies, so the classifier sees what the local one sees. The transport's own compile refusals and the timeout envelope keep their answers.The
wherehanded over is the caller's own, from beforetoRemoteFilterrewrote it, so the read-scope provenance marks decide whether the column is named, exactly as they do locally.What changes
packages/drivers/driver-turso/src/remote-transport.ts:aggregate: the catch is removed, and the backend's error leaves the method.find: the ladder has the local face's two rungs, the projection and then the ORDER BY (the new one), each rebuilt with the caller'swhere, which neither drops. The terminal throws the last rung's error instead of answering[].packages/drivers/driver-turso/src/turso-driver.ts:remoteReadExit(object, query, read)ends in a new privateremoteReadFault: the status gate, thenaggregateBackendFault.aggregatearm now goes through that exit.find,findOneandcountpass the caller'swhere.refuseRemoteColumnMap's docblock moves to the past tense: the[]it described is now a refusal, and the 501 refusal stays. A parenthetical sentence is also added after it, saying so: that read is now refusedINVALID_FILTER/ 400, and the 501 refusal stays the answer.RemoteTransport's orTursoDriver's published signatures. The constructor (PR fix(driver-turso)!: new TursoDriver refuses syncUrl under a forced remote mode, and sync with no syncUrl (#20200) #20447) and the write doors are untouched.Bounded in-place fixes, declared
count's remote exit. For the samewhere, local answersINVALID_FILTER/ 400 and remote answersDATABASE_ERROR/ 500. This is not one of the card's three rows. It sharesremoteReadExitwithfind, and leaving it would reopen the split driver-sql: one unresolvable WHERE column, two answers —find()silently returns [] whilecount()throws a raw dialect error with no ADR-0112 envelope #8790 ruled out: a list view calls both halves. ① Same class: a remote read exit answers an unresolvablewherecolumn differently from the local face. ② Mechanical, with a pinned shape: the local seam, called. ③ Inside the claimed surface (the remote read arms ofturso-driver.ts), and no other claim holds those lines. ④ Same package tests.[]turns "ordered by a column the table lacks" from[]into anINVALID_FILTERrefusal that tells the caller their filter was wrong (measured, ablation leg D below). The local face answers the rows, unordered (fix(sharing): 共享规则新建页 — 自定义 widget 未国际化,且「接收方」永远无可选项 #3821). The registered migration entrydriver-sql-unresolvable-where-column-refusedalready says the ladder drops an ORDER BY and keeps the rows. ① The samereturn []line. ② Mechanical: the local ladder's second rung. ③ The claimed catch, in the claimed file. ④ Same package tests.Tests
turso-local-remote-missing-table-column-parity.test.ts: 22 cases over a real@libsql/clientfile:database, with a local driver over the same file.code+statuson both faces.findOne, find with projection andwhere,count(federated and managed),sumover the field,aggregatewith thatwhere, and ORDER BY and projection recoveries answering the literal rows on both faces (8 cases).remote-transport-unknown-select.test.ts: the[]pin is replaced as described under H2.daad09aa1:pnpm --filter @objectstack/driver-turso exec vitest run --maxWorkers=2gaveTest Files 75 passed (75)andTests 2003 passed | 16 skipped (2019).pnpm --filter @objectstack/driver-turso typecheckexited 0, andtsc --listFilesincludes both test files. Run after rebuilding the dependency closure on the merged tree.Reverse verification
The fix was committed first (
a10647239). Every leg went throughnode scripts/ablation-replace.mjs: the anchor hit 1 → 0, the blob changed, and the restore was proven by blob == HEAD and an emptygit diff HEAD. An outer trap re-verified the restore against the HEAD blob. The subject resolves fromsrc/through a relative import (vitest, no alias), so nodist/is in the path. Suites: the new parity file, the unit file and the #20107 external-object parity file (79 cases). Directions were predicted in the test header before running:aggregate's[]catch restored: 6 failed / 73 passed, exactly the six aggregate pairs.return []: 5 failed / 74 passed. That is the find, findOne and find-with-projection pairs, plus the replaced unit pin.count,aggregateand ORDER BY stayed green.remoteReadFaultreduced tobackendStatementFault: 10 failed / 69 passed, every 400 pair. Both missing-table pairs stayed green, since they answer 500 on both faces anyway.INVALID_FILTERwording ("A filter on object 'ext_t' names a column the database could not resolve …") where the local face answered the rows.INVALID_FIELD/ 400. The real compile-refusal control stayed green, as predicted: no real refusal text parses as a backend column fault today.A first harness dry run was refused by the tool itself (its replacement contained the anchor, so the anchor count did not drop), and it restored. Nothing was measured there.
Gates at
daad09aa1node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 63 commands (5 paths vs merge base3cf644938). All 63 were run, and each exit code was written to a file before any pipe.Three gates first answered PREREQUISITE NOT MET (exit 3):
check:dual-build-cjs-loads,check:lean-entry-closureandcheck:type-check-debt. I built every package withpnpm turbo run build --filter='./packages/*' --filter='./packages/*/*' --concurrency=2: 71 of 71 tasks succeeded. All three then exited 0. The dist-sweepingcheck:dts-closure(71 packages),check:sourcemap-no-sources-content(68) andcheck:published-fileswere re-run on the full build and exited 0.--rananswered:63 derived famil(ies) accounted for — 63 run, 0 NOT-MEASURED (a DERIVED zero — all 63 recorded an exit code and none of them is 3).Driver conformance ledger:
6e3e5462c:OK — 50 covered cell(s), 0 in the DEBT ledger, 0 exempt.daad09aa1:OK — 50 covered cell(s), 0 in the DEBT ledger, 0 exempt.driver-tursorow isokin every column at both readings.Lint, narrowed:
eslint --no-inline-config --format jsonover the 4 changed.tsfiles gave 4 files, 0 errors and 0 warnings.eslint.config.mjs:**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}minus its ignores.parserOptionsare{ecmaVersion: latest, sourceType: module}, with noproject. All 6 active rules are per-file AST rules, so type-aware linting is off and this diff cannot move a verdict on any untouched file. The fullpnpm lintis CI's.Clause-② and the changeset
.changeset/20424-turso-remote-missing-table-column-refused.mdships@objectstack/driver-tursominor, withClause-②: no (narrowing), the BREAKING banner in the launch-window form (check-changeset-no-majorrefusesmajor), a FROM → TO line with the fix, and the ADR-0087 dispositionnot-required (already-registered driver-sql-unresolvable-where-column-refused). This follows the seat's ruling in the amended claim 5872640432. The precedent the ruling reads:find()silently returns [] whilecount()throws a raw dialect error with no ADR-0112 envelope #8790 (716ac9bf8,find[]→INVALID_FILTER/ 400, the same shape as row 3) declared BREAKING accept-set narrowing:minor, with an ADR-0087registereddisposition and the migration entrydriver-sql-unresolvable-where-column-refused.surfacealready names "driver-sql(and itsTursoDriver/SqliteWasmDriversubclasses)" and prescribes this change's remedy (name a real column, or run schema sync). So no new entry is owed; theaggregatelegs are the same drifted-schema family with the same remedy. Remote-face users meet the refusal for the first time here, so the banner is owed.aggregate()answers DATABASE_ERROR/500 wherefind()andcount()answer INVALID_FILTER/400 — the #8790 refusal never reached the third read door #11541 (ef52884a8,aggregate500 →INVALID_FIELD/ 400) shipped aspatchwith no narrowing: a refusal that changed its code, not an accept set.RemoteTransport.findandRemoteTransport.aggregate, exported from the package root, now raise the backend's error where they answered[]. The changeset says so.Files
.changeset/20424-turso-remote-missing-table-column-refused.md(+33 / -0)packages/drivers/driver-turso/src/remote-transport.ts(+45 / -22)packages/drivers/driver-turso/src/turso-driver.ts(+90 / -15)packages/drivers/driver-turso/src/remote-transport-unknown-select.test.ts(+15 / -4)packages/drivers/driver-turso/src/turso-local-remote-missing-table-column-parity.test.ts(+345 / -0)packages/drivers/driver-turso/src/turso-local-remote-external-object-parity.test.ts(+3 / -2, a header prediction line only)In total, +531 / -43 over 6 files, under the 5,000-line threshold. No governed surface.
Patch round 1
The seat's amended claim 5872640432 answered the round-0 open question with B, and accepted the two bounded in-place fixes and the H3 route. This round changes only the changeset and this body, with no code or test change:
patch→minor;Clause-②: no→Clause-②: no (narrowing); the BREAKING banner; the ADR-0087 markernot-required (already-registered driver-sql-unresolvable-where-column-refused)with its reason; a FROM → TO line naming the refusals, the fix, andRemoteTransport.find/aggregatenow raising where they answered[].Clause-②line, the seat-ruling line under it, the "Clause-② and the changeset" section, the Files line and one Acceptance note.origin/mainmerged at2304b1608(a true merge commit, five incoming commits, none on adriver-*path). Head3b56eb769.git diff daad09aa1 3b56eb769 -- packages/driversis empty. After rebuilding the dependency closure on the merged tree:pnpm --filter @objectstack/driver-turso exec vitest run --maxWorkers=2gaveTest Files 75 passed (75)andTests 2003 passed | 16 skipped (2019), andtypecheckexited 0.3b56eb769, each exit code recorded before any pipe:pnpm check:adr-0087-registrationexit 0;node scripts/check-changeset-no-major.mjs --base origin/main --event(this body) exit 0;node scripts/check-empty-changeset.mjs --base origin/mainexit 0.Patch round 2
After the at-tier review 5873239611 (PASS at
3b56eb769) and the seat's amended claim 5873267997, this round is text only plus a merge, with no logic or test-assertion change:remote-transport.ts, the$-prefixed-key comment: its sentence sayingfind()'sno such columnbackstop "swallows the error into[]anyway" underSQLITE_DQS=0was made false by this PR. It now says the backstop refuses, as the local face does (INVALID_FILTER/ 400), and that it used to answer[].turso-local-remote-external-object-parity.test.ts, the driver-turso remote: a federated object'sexternal.remoteNameis ignored — remotefindqueries a table named after the object and throws a bareLibsqlError(no code, no status), while the local face reads the mapped table #20107 suite's header prediction for its first ablation leg:aggregatenow refuses the missing table asDATABASE_ERROR/ 500, instead of answering[]in place of the sum.origin/mainmerged atacd009521(a true merge commit), carrying PR fix(driver-turso)!: new TursoDriver refuses syncUrl under a forced remote mode, and sync with no syncUrl (#20200) #20447 (bea6d2ea3, the constructor region ofturso-driver.ts). The merge was clean. Head0c47b7bbe.pnpm --filter @objectstack/driver-turso testgaveTest Files 76 passed (76)andTests 2036 passed | 18 skipped (2054)(the added file and cases are fix(driver-turso)!: new TursoDriver refuses syncUrl under a forced remote mode, and sync with no syncUrl (#20200) #20447's), andtypecheckexited 0.dispatch-gates --commandsat0c47b7bbederived the same 63 commands. After a full package build (71 of 71), all 63 exited 0 on the first pass.--ran:63 derived famil(ies) accounted for — 63 run, 0 NOT-MEASURED (a DERIVED zero — all 63 recorded an exit code and none of them is 3). Driver conformance:OK — 50 covered cell(s), 0 in the DEBT ledger, 0 exempt.Narrowed eslint over the 5 changed.tsfiles: 0 errors, 0 warnings.external.remoteNameis ignored — remotefindqueries a table named after the object and throws a bareLibsqlError(no code, no status), while the local face reads the mapped table #20107 header's third prediction ("A filter on the renamed field answers[]") is also out of date after this PR. With the column-map refusal deleted, that filter would now be refusedINVALID_FILTER/ 400. It is prediction prose, and no assertion depends on it.Acceptance notes
isUnresolvableColumnErroris not exported from@objectstack/driver-sql, so the remote find/count exit reaches it throughaggregateBackendFaultwith awhere-only query. An exported predicate, or a protectedwhere-exit onSqlDrivershared bycountandfindRows, would be the plainer seam. This card may not editdriver-sql. Carrier: none.RemoteTransport.findstill recognises the unresolvable-column class inline (no such column, orcolumn+does not exist) to gate its ladder. That pre-existing copy of the predicate lacks the MySQL arm, which libSQL never speaks.INVALID_FILTER(unnamed wording) and this face answersDATABASE_ERROR/ 500. No producer is known.RemoteTransport.findandRemoteTransport.aggregate, used on their own withoutTursoDriver, now raise the backend's error where they answered[]. The changeset says so.mainmoved after the measured merge. Round 0 did not re-mergeb28550818(additive inpackages/speconly). Patch round 1 mergedorigin/mainat2304b1608(five commits, none on adriver-*path). It then moved once more, tofbeb56e4b(onepackages/spectest pin,turbo.jsonandscripts/cross-package-test-inputs.mjs, nodriver-*path). Patch round 2 mergedorigin/mainatacd009521, which carries both. PR fix(driver-turso)!: new TursoDriver refuses syncUrl under a forced remote mode, and sync with no syncUrl (#20200) #20447 (driver-turso:new TursoDriveracceptssyncUrl/syncundermode: 'remote'and ignores them —isSyncEnabled()answers true, no sync runs, andsync()rejectsSYNC_NOT_SUPPORTED#20200) landed asbea6d2ea3, is merged here, and driver-turso was re-tested on the combined tree (see "Patch round 2").Generated by Claude Code