Repository navigation
fix(driver-turso)!: match the url scheme in any case, and refuse a url the local engine cannot open instead of running it on :memory: - #19996
Conversation
…recognised url in a local or replica mode WIP: source change only; tests follow. Claude-Session: https://claude.ai/code/session_01Bvd69VPa6puiNzzPUroDBx Co-authored-by: Claude <noreply@anthropic.com>
…ed-url refusal; flip CONTROL 1 Claude-Session: https://claude.ai/code/session_01Bvd69VPa6puiNzzPUroDBx Co-authored-by: Claude <noreply@anthropic.com>
…nd the unrecognised-url refusal Claude-Session: https://claude.ai/code/session_01Bvd69VPa6puiNzzPUroDBx Co-authored-by: Claude <noreply@anthropic.com>
…obed Claude-Session: https://claude.ai/code/session_01Bvd69VPa6puiNzzPUroDBx Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 6 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 0791cbffd11731d24faafe45e8f352c2e4ac8ec2 && git checkout 0791cbffd11731d24faafe45e8f352c2e4ac8ec2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e8f163fc3a62cc6c65f91d2197f7b516e3a2c90b 4a7cb4de9440da179117f7c2eef2002c9ed13ccc && git checkout -B drift-repro e8f163fc3a62cc6c65f91d2197f7b516e3a2c90b && git merge --no-ff 4a7cb4de9440da179117f7c2eef2002c9ed13ccc
node scripts/docs-audit/affected-docs.mjs --json e8f163fc3a62cc6c65f91d2197f7b516e3a2c90b
|
Contract reviewServed-tier: ① Derived judgmentsThe reviewer's own probe built 63 configurations against the head and base (
② Semver levelConsistent: ③ Boundary flags
Implemented-by: VERDICT: FAIL Edits that make it PASS:
Isolated reviewer: a separate contract-review-tier subagent, fed the card, the PR, the sibling PR #19971 and AGENTS.md only; the seat verified item ① 12 against both Generated by Claude Code |
…e the FILE: forced-replica widening, pin it The pending remote-url refusal changeset described a case-sensitive classifier and a fall-through that this branch removes; its three affected sentences are rewritten against the shipped code (a deliberate correction of a pending release note). The new changeset names the one configuration this branch accepts that the earlier change refused, an uppercase FILE: url under a forced replica mode, and the preservation table pins it with a durability round trip. Claude-Session: https://claude.ai/code/session_01Bvd69VPa6puiNzzPUroDBx Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: ① Derived judgments
② Semver levelConsistent: ③ Boundary flagsThe corrected note: Line 20
Line 25
Line 31
The unchanged sentences of the note were re-judged against head and are TRUE: title, refused bullets, message, what stays accepted, ways out, and marker. No sentence of the corrected note is false or unmeasured against the shipped code. Per ruling 1A (#19940, 5814546887), this record names the corrected note and judges each rewritten sentence. With the PASS below, it is the confirmation of the DELIBERATE CORRECTION red.
Optional, not a FAIL item: the PR-body sentence "Folding widens nothing." is true within its heading's scope (the remote schemes). Folding Implemented-by: VERDICT: PASS Isolated reviewer: the same contract-review-tier subagent, third read on this head, fed only the card, the PR, the sibling PR #19971 and AGENTS.md; adopted by the seat. Generated by Claude Code |
Pending release-note correction on this PR:
|
Fixes #19976
Clause-②: no (narrowing)
TursoDriverno longer runs a url it cannot open on a private:memory:engine. The classifier now reads the url scheme in any letter case, the way@libsql/clientroutes it, so an uppercaseLIBSQL://is remote. Whatever is still unrecognised (a bare path, an unsupported scheme) is refused at construction in a local or replica mode, asVALIDATION_ERROR/ 400, naming thefile:spelling. Both options in triage's execution note were weighed. This PR case-folds the scheme like the client and refuses what is left. Neither half routes any configuration to a:memory:engine it did not name.Why fold the case instead of refusing uppercase too
@libsql/core@0.17.4lib-esm/config.jsline 26:const originalUriScheme = uri.scheme.toLowerCase();. Executed:expandConfig({ url: 'LIBSQL://r.turso.io' }, true).scheme === 'https', andcreateClientopens it (protocol=http).FILE:./x.dbexpands tofileand opens./^libsql:\/\//iininferDriverTypeFromUrl(packages/cli/src/utils/storage-driver.ts) and indetectDriverFromUrl(packages/runtime/src/standalone-stack.ts). So an uppercaseOS_DATABASE_URLselected this driver and then ran on:memory:.ridesWebSocketTransportalready folded case, with an in-code note that foldingdetectMode"must be argued on its own". That argument is above: after this PR, every reader of one url (host sniffer, client, both driver predicates) reads the scheme one way. The note is replaced with a single helper,startsWithScheme, that every predicate in the file compares through.Why refuse a bare path instead of treating it as
file:H3, measured against the installed
@libsql/client@0.17.4:createClientrefuses./data/app.db,data/app.dband/abs/app.dbasURL_INVALID("The URL '…' is not in a valid format"), andC:\data\app.db,sqlite:./x.dbandmemory://xasURL_SCHEME_NOT_SUPPORTED. AlsoURL_INVALID::MEMORY:,'',' file:./y.db'(leading space) andlibsql:host(no//). Control:file:./x.dbopens withprotocol=file. Reading a bare path asfile:would invent a spelling the client refuses, so the same string would open a local file and fail as a replica.The refused set, exactly (for #19977 to mirror at authoring time)
Mode is the forced
mode, or else auto-detected fromurl(andsyncUrl). Schemes compare case-insensitively. "A remote url" means one starting withlibsql://,https://,http://,wss://orws://.'local'or'replica'(forced, or auto-detected with or withoutsyncUrl), andurlis none of: exactly:memory:; a url startingfile:; a remote url.'local'or'replica'andurlis a remote url. That means a forced local or replica mode, or nomodewithsyncUrlset.replica-without-file, now covering only in-memory urls; see "The one widened cell" below): mode is'replica'andurlis exactly:memory:, or afile:url whose remainder is:memory:or starts with:memory:?.Checked in the order 2, 1, 3. The two remote-mode
timeoutrefusals are unchanged:timeoutover 0 with awss:///ws://url, andtimeoutover 0 with a suppliedclient. Case-folding now brings an uppercase WebSocket url with nomodeinto the first. Not refused: a forcedmode: 'remote'with any url. It runs no local engine, and@libsql/clientrefuses a bare path there itself atconnect()(URL_INVALID, pinned).TursoDriver.detectModenow also answers'replica'for an unrecognised url besidesyncUrl(it answered'local'), like its other two arms. The refusal stays in the constructor.toKnexConfig's last arm, which handed such a url:memory:, now calls the same refusal. It cannot be reached from the constructor, and it can no longer produce an in-memory engine nobody named.CONTROL 1 of
turso-driver-uppercase-ws-scheme-timeout-refusal.test.ts: flipped, deliberatelyIt pinned that
WSS:///Ws:///HTTPS:///LIBSQL://with nomodeconstruct as'local', and thatWSS://+timeoutwith no mode stays local and unrefused. That was the fall-through this card removes: a local engine on a private:memory:database. The control now pins the opposite: those urls are'remote', andWSS://+timeoutwith no mode meets the WebSocket refusal with the same message aswss://, except for the echoed scheme. The file's docblock records the flip and its reason. Its reverse-verification paragraph was re-measured (below) and rewritten to match.Measurements
Before/after probe:
initObjects,create,find, then a fresh driver on the same config. BASE =a7581b326'sturso-driver.ts, loaded next to HEAD's in one vitest run over the same dependency closure. TMP and DIR stand for temp and relative directories.a7581b326LIBSQL://…, no mode./DIR/app.db, no modeVALIDATION_ERROR/ 400TMP/bare.db(absolute), no modeVALIDATION_ERROR/ 400./DIR/app.db+mode: 'local'VALIDATION_ERROR/ 400FILE:TMP/upper.db, no modefile:TMP/ctl.dbH1 confirmed by reading
a7581b326(the// Fallback: treat as localindetectMode, the:memory:last arm oftoKnexConfig) and by the rows above. H2 and H3 are covered above. H4: the guard extended islocalEngineDefect/refuseNonDurableLocalEngine. The "or dropmode: 'replica'… for a plain local database" sentence is now emitted only for an in-memory url: a bare path takes the new refusal. It reads "for a plain in-memory local database, which is what the url names: ephemeral by declaration", which is true because dropping the replica on:memory:/file::memory:gives exactly that. The new refusal's own ways out were checked per arm: afile:url for a local file or replica, and "drop" every key (mode,syncUrl) that would still keep a remote url local.H5 reach, on this tree (
a7581b326+ this diff),git grep:libsql://spellings, excluding this package's tests: 3 hits, all prose (the driver-turso: a remote url plus syncUrl is classified replica and handed a :memory: Knex connection, so every write lands in process memory and never reaches the remote #19893 changeset,CHANGELOG.md, a driver comment). Positive control, lowercaselibsql://: 487 hits.HTTP(S)/WS(S)/FILEscheme literals, excluding this package's tests and changelogs: 5 hits, none a turso config (a CLIFile:label, driver comments, a spec redirect-url test). Positive control, the same pattern lowercase: 3,652 hits.urlliterals in every file that mentions turso or libsql, excluding this package's tests: 269 listed. None is a turso config with a bare path or unknown scheme. The leftovers are other drivers, route paths and a' 'empty-url refusal fixture. Positive control: 46file:values in the same listing.OS_DATABASE_DRIVER=turso/driver: 'turso': no bare path anywhere. Neither host sniffer selects this driver for a bare path. The runtime maps it to sqlite, and both select turso only forlibsql://or anhttp(s)://url with a.turso.host.Out-of-repo deployments: NOT MEASURED, not claimed zero.
Tests
HEAD
e5e29cb11:pnpm --filter @objectstack/driver-turso test:Test Files 62 passed (62),Tests 1404 passed (1404).pnpm --filter @objectstack/driver-turso typecheck(tsc --noEmit): clean.--listFilesincludes both touched test files.turso-driver-unrecognised-url-refusal.test.ts(40 cases) pins:code+status+ the identifying first sentence), before and after, for the uppercase url and the bare path, with and withoutsyncUrl, under a forcedmode: 'local'andmode: 'replica', and throughcreateTursoDriver;FILE:durability across a restart, local and replica;URL_INVALIDat connect;file:local and replica,:memory:local, lowercase remote,mode: 'remote'.Reverse verification. The direction was predicted in the new file's header before the run. It ran at
457d65f23, wheresrc/is byte-identical toe5e29cb11except comments;git diff 457d65f23 e5e29cb11 -- packages/drivers/driver-turso/srcis comments only.turso-driver.tswas restored to thea7581b326blob (07363cdb…, on-disk hash verified,startsWithSchemecount 8 then 0) under an absolute-path trap, over the three suites:Tests 35 failed | 52 passed (87).turso-driver-remote-url-replica-refusal.test.ts: all GREEN.02da2f8fequals HEAD's, andgit diff HEADis empty.Ablation. Using
node scripts/ablation-replace.mjs,ridesWebSocketTransportwas reverted tourl.startsWith('wss://') || url.startsWith('ws://'). The anchor fell from 1 hit to 0 and the blob went02da2f8fto5d086ff9. Result:Tests 7 failed | 13 passed (20). RED: the 6 refusal cases and CONTROL 1's timeout case. GREEN: CONTROL 1's classification cases and controls 2 and 3, which is what the rewritten docblock says. Restored, with the blob equal to HEAD's andgit diff HEADempty.Gates (HEAD
e5e29cb11)node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackreturns the same 61 commands atbc2e4f5f8ande5e29cb11. All 61 were run ate5e29cb11.--ran:✓ dispatch-gates --ran: 61 derived famil(ies) accounted for — 59 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3).check:dual-build-cjs-loadsandcheck:type-check-debt. Reason:PREREQUISITE NOT MET, exit 3. Each needs a whole-workspacedist/build (turbo run build --filter='./packages/*' …, as inlint.yml), which was not run locally. Targeted substitute: the builtdist/index.js(CJS) anddist/index.mjs(ESM) of this package load, and each refuses a bare path asVALIDATION_ERROR 400.driver-tursohas noDEBT/TEST_DEBTentry, and itstsc --noEmitis clean.check:lean-entry-closurefirst answered exit 3 (objectql not built). It is green afterturbo run build --filter=@objectstack/objectql.node scripts/check-changeset-no-major.mjs --base origin/main:✓ This diff introduces no \major` bump.The clause-② axis reads this PR's body in CI:LEVEL AXIS: NOT APPLICABLElocally, because a local run has nopull_request` payload.node scripts/check-adr-0087-registration.mjs --base origin/main:✓ check-adr-0087-registration: 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition.Classified as[BREAKING+bang+clause-②-narrowing] not-required (no-migration-prescription), the same shape as the precedent changeset from PR fix(driver-turso)!: refuse a remote url beside syncUrl, and any replica not on a local file, instead of running on :memory: #19971.pnpm check:driver-conformance:OK — 50 covered cell(s), 0 in the DEBT ledger, 0 exempt.Same reading on basea7581b326before the change and one5e29cb11after.node scripts/check-issue-citations.mjs --base origin/main:✅ check-issue-citations: every citation this change adds resolves (or is a declared cross-repo reference)..tsfiles:eslint --no-inline-config --format jsoncounts 3 files, 0 errors and 0 warnings ate5e29cb11. Each file is in eslint's own config (--print-configexit 0; none reported as ignored). Invariance:eslint.config.mjsenables no type-aware linting (noparserOptions.project, no typed rules; its own note at line 327), so this diff cannot move the verdict on an untouched file.pnpm lintover the repo is CI's.origin/main(67ebc84a7). The 17 family-definition files changed there are sdui/objectui manifest scripts,lint.yml,cut-rc.yml,package.jsonand similar. None of them touchespackages/drivers/driver-turso, and no merge was taken. CI runs the current definitions on the merge ref.Acceptance notes
.changeset/19893-turso-remote-url-replica-refusal.md, unreleased) is rewritten by this PR at lines 20, 25 and 31 as a DELIBERATE CORRECTION, because this change made those three sentences false for the shipped code; see "Deliberate correction of a pending changeset" below. No other sentence of that note moves.toKnexConfig's last arm now refuses too, so an ablation of the constructor'sunrecognised-urlline alone leaves the tests green by design: the same envelope comes from the second site. The reverse verification above removes both.file:url with a query string (for examplefile:./x.db?mode=ro) is still handed to better-sqlite3 as a literal filename. That behaviour predates this PR, is durable, is NOT MEASURED here, and no finding is claimed.planMediaColumnMoveor the remote media-column paths.The one widened cell (seat correction of this body, after contract review 5818338847)
The earlier text of this body called arm 3 "same coverage". That was wrong. Under a forced
mode: 'replica', a url that is none offile:,:memory:or a remote url now meets arm 1 (unrecognised-url) instead. An uppercase or mixed-caseFILE:url naming a file, under a forcedmode: 'replica', with or withoutsyncUrl, is no longer refused: it is afile:url, the replica runs on that file, and its rows survive a restart. The #19893 change refused it, because it read the scheme case-sensitively. It is the only configuration refused at basea7581b326and accepted at this head. It is stated in.changeset/19976-turso-unrecognised-url-refusal.md("Newly accepted") and pinned in the PRESERVATION table, 4 cases that go red on the baseturso-driver.ts. Measured at4a7cb4de: withturso-driver.tsrestored to the base blob07363cdb,-t PRESERVATIONgivesTests 4 failed | 9 passed | 31 skipped (44). The 4 red cases are exactly the widened ones, each refusedVALIDATION_ERROR/ 400 by the base replica-without-file message. At head they are 4 of 4 green.Deliberate correction of a pending changeset
This PR changes
.changeset/19893-turso-remote-url-replica-refusal.md. That note is pending: it landed with PR #19971 and no release has consumed it, and it compiles into the same version's CHANGELOG as this PR's note (lockstepfixedgroup). This change made three of its sentences false for the shipped code, so they are rewritten here, from the code at this head. No other sentence of that note moves (git diff -U0: lines 20, 25, 31 only).Check Changesetis therefore red by design (the DELIBERATE CORRECTION class ofscripts/check-empty-changeset.mjs).skip-changesetis not applied, and the note is not restored. The confirmation is the same-head at-tier contract review record that names this note and judges each rewritten sentence (ruling 1A, #19940, 5814546887).TursoDriver.detectModeclassifies as remote: …". New: "A remote url here means one of the schemesTursoDriver.detectModeclassifies as remote:libsql://,https://,http://,wss://,ws://. The driver-turso: a url whose scheme the classifier does not recognise (an uppercaseLIBSQL://, a bare path) and nomodefalls through tolocalon a:memory:Knex engine, so every write is lost on restart #19976 entry in this same version matches them in any letter case, so an uppercaseLIBSQL://is a remote url too."mode: 'replica'only, anyurlthat is not a localfile:path, such as a bare path or an uppercase scheme." New: "under a forcedmode: 'replica', aurlthat is none of:memory:, afile:url or a remote url, such as a bare path or an unsupported scheme. The driver-turso: a url whose scheme the classifier does not recognise (an uppercaseLIBSQL://, a bare path) and nomodefalls through tolocalon a:memory:Knex engine, so every write is lost on restart #19976 entry in this same version refuses such a url in every local or replica mode, and matches thefile:scheme in any letter case: an uppercaseFILE:url naming a file is afile:url and is not refused, and the replica runs on that file (FILE::memory:is refused as in-memory, likefile::memory:)."'local'and still runs on:memory:, with or withoutsyncUrl. So does the same url under a forcedmode: 'local'. That fall-through is tracked as driver-turso: a url whose scheme the classifier does not recognise (an uppercaseLIBSQL://, a bare path) and nomodefalls through tolocalon a:memory:Knex engine, so every write is lost on restart #19976." New: "Not refused by this change: … auto-detected'local'with or withoutsyncUrl, and the local engine was handed:memory:. Under a forcedmode: 'local'the same url got the same:memory:engine. The driver-turso: a url whose scheme the classifier does not recognise (an uppercaseLIBSQL://, a bare path) and nomodefalls through tolocalon a:memory:Knex engine, so every write is lost on restart #19976 entry in this same version removes that fall-through … No configuration runs on an in-memory database it did not name."Generated by Claude Code