Repository navigation
fix(driver-sqlite-wasm)!: a file wasm SQLite cannot read is refused at connect, naming the file, instead of being renamed and replaced with an empty database - #25200
Conversation
…instead of renaming it WasmSqliteConnection.open() throws SQLITE_NOTADB / SQLITE_CORRUPT naming the file, with a prescription to move it aside; quarantineCorruptFile retires. resolveSqliteDriver re-throws a wasm file refusal instead of stepping down to the in-memory rung. Claude-Session: https://claude.ai/code/session_015pbbff7SHh7EEEWiasBdDe Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pbbff7SHh7EEEWiasBdDe Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pbbff7SHh7EEEWiasBdDe Co-authored-by: Claude <noreply@anthropic.com>
…iod the connect line adds Claude-Session: https://claude.ai/code/session_015pbbff7SHh7EEEWiasBdDe Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pbbff7SHh7EEEWiasBdDe Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check9 anchor(s) derived from 2 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 8 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 76f84229cf98a12f14114524e2f15aaa1c08eeeb && git checkout 76f84229cf98a12f14114524e2f15aaa1c08eeeb
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2919ddb875cd0daa38ef452d741bcba04cf366af cda55e65473673ae0db2ebae9b3aac7143384404 && git checkout -B drift-repro 2919ddb875cd0daa38ef452d741bcba04cf366af && git merge --no-ff cda55e65473673ae0db2ebae9b3aac7143384404
node scripts/docs-audit/affected-docs.mjs --json 2919ddb875cd0daa38ef452d741bcba04cf366af |
Contract reviewServed-tier: Face: ① Derived judgmentsRuling 6311031589, item by item:
The one-vocabulary claim ("the native driver's class, code and words, with no The code mapping: sql.js throws a bare Unchanged behaviour, read in the code and pinned:
Public-surface changes the diff implies: Rejection pins assert ② Semver levelBoth packages are published (no
③ Boundary flagsDev report 6339572437,
Dev flags in the PR's acceptance notes:
Dev
Check-runs on this head as read 2026-10-22T11:34Z: 27 success, 7 skipped, 6 in_progress — the six Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对ACCEPT ·
Generated by Claude Code |
Requested by Bill · project thread
Fixes #25019
Clause-②: no (narrowing)
Metadata-protocol: none
Narrowing face: a connect that resolved over a file wasm SQLite cannot read now rejects, naming the file.
WasmSqliteConnection.open()no longer renames such a file toFILE.corrupt-TIMESTAMPand starts an empty database in its place;resolveSqliteDriverre-throws the refusal at the wasm rung instead of trying the in-memory driver. Nothing exported changes shape. It owes aCONTRACT_REVIEW_TIERreview before the queue (the claim's line).Member 3 of the SQLite file-failure family (the wasm quarantine), the card's last member, under the
domain:engineseat 2's ruling 6311031589 (route A, inherited from #24960's route A). Members 1 and 2 landed in PR #25080 (d6a5fa3ba,Part of #25019). Dev sessionsession_015pbbff7SHh7EEEWiasBdDe(a project card thread of seat sessionsession_01FaVyM6YvQLg9CsjPVCYc5K), claim 6335885549; the cross-lane line inservice-datasourceis declared on #21118 (6335909713).What changed
packages/drivers/driver-sqlite-wasm/src/wasm-connection.tsopen(): a file whose bytes sql.js cannot open or read (new SQL.Database(bytes)orPRAGMA quick_check(1)throws, orquick_checkdoes not answerok) is refused withunreadableDatabaseFileRefusal: anErrorcarryingcode(SQLITE_NOTADBwhen SQLite says "file is not a database",SQLITE_CORRUPTfor every other read failure),filename,cause(sql.js's own throw), and a message that says how to move the file aside. sql.js throws a bareErrorwith SQLite's message and no result code, so the producer maps SQLite's own errstr forSQLITE_NOTADB; nothing downstream reads message text.quarantineCorruptFileretires (it was private). The refused file is left byte-for-byte as found and nothing is written beside it.quarantineOrphanedWalnow runs only after the image is accepted, so a stale write-ahead log beside a refused file stays where it is too.packages/services/service-datasource/src/sqlite-driver-fallback.ts(the one cross-lane file): the wasm rung'scatchre-throws whenisSqliteFileOpenFailure(err)holds, exactly as the native rung does. The predicate needed no new code: the wasm refusal reaches it asSqlDriver's ownSqliteDatabaseFileErrorwithSQLITE_NOTADB/SQLITE_CORRUPT(below). Every other wasm failure, sql.js itself not loading first among them, still steps down to the in-memory driver. Module note and predicate doc updated.packages/drivers/driver-sqlite-wasm/src/sqlite-wasm-driver-durability.test.ts: the self-heal case is now a refusal pin (ruling item 3).packages/drivers/driver-sqlite-wasm/src/sqlite-wasm-25019-unreadable-file-refused.test.ts(relativesrcimport) andpackages/services/service-datasource/src/__tests__/sqlite-driver-fallback-wasm-unreadable-file.test.ts(resolves@objectstack/driver-sqlite-wasmthroughdist/)..changeset/25019-wasm-unreadable-file-refused.md:@objectstack/driver-sqlite-wasmand@objectstack/service-datasourcepatch,Clause-②: no (narrowing),Metadata-protocol: none, BREAKING banner (FROM, TO, Unchanged, fix) and ADR-0087 dispositionnot-required (no-migration-prescription).One vocabulary with the native driver, measured
SqliteWasmDriveris aSqlDriver, and wasm SQLite's connection is opened by knex's pool on the first statementSqlDriver.connect()runs (PRAGMA auto_vacuum). The pool propagates the create error (propagateCreateError: true), soapplyAutoVacuumIncrementalsees the refusal'scodeand answers it through the same privateSQLITE_FILE_REFUSALStable the native driver uses, wrapping it inSqliteDatabaseFileError. So a wasm refusal carries the native driver's class, code and words ("is not a SQLite database, and was left as it is" / "is damaged (SQLite cannot read its header or its schema), and was left as it is"), with no second phrase table and no edit todriver-sql. The prescription is wasm's own cause text, since the rename it replaces was wasm's: the native refusal carries none (the dispatch's assumption that the native family has a move-aside prescription to import did not hold; measured bygrep, zero hits indriver-sql/src).The line an operator sees (public door below):
Premise (base
f7d740376)quarantineCorruptFilewas atwasm-connection.ts, called fromopen()on a read failure, renaming toFILE.corrupt-plusDate.now(); the wasm rung ofresolveSqliteDriverwas a barecatchstepping down to memory. Both doors were measured by the dev report 6310958104 (open question 1): (a) a declaredsqlitedatasource whose native addon cannot load, over a plain-text or header-truncated file, renamed on a development boot; (b) a datasource declareddriver: 'sqlite-wasm'over a plain-text file, renamed andavailablein development and production. sql.js 1.14 answers, measured: plain text and magic-header junk throw "file is not a database" atquick_check; a real database truncated to 50 bytes or to half throws "database disk image is malformed"; a real database and an empty file answerok.Tests
driver-sqlite-wasm11/11 across the two files;service-datasource10/10: (a) the native-addon-cannot-load step-down (a realERR_DLOPEN_FAILEDat the fix(cli,service-datasource): under bin/run-dev.js (NODE_ENV=development) an unopenable SQLite datasource steps down to an in-memory engine, so a dev-mode os migrate audit-mail-bodies reports an unreachable archive as examined and clean #24950 suite's seam) over a plain-text and a header-truncated file rejects with the driver's refusal, no wasm or memory warning, file bytes unchanged, directory holds the file alone; the same throughDatasourceConnectionServicein development readsfailed, no driver registered; (b)driver: 'sqlite-wasm'over both files in development and production readsfailedwith the code's words naming the file, notavailable, nothing moved; (c) controls: a real database opens and answers (wasm, andsqlite-wasmdeclared, both modesavailable), an absent file is created, an empty file opens, a native addon that cannot load over an absent file still steps down to wasm.73dc5b1a, under the verify lock):driver-sqlite-wasm"Test Files 39 passed (39) · Tests 703 passed (703)";service-datasource"Test Files 55 passed (55) · Tests 955 passed (955)";typecheckfor both, exit 0.f8ffdfc0,scripts/ablation-replace.mjs): the refusal'sthrowreplaced by the old quarantine (rename to.corrupt-, empty database, plus a marker), blob07f7cb3e0fa3to15743567f704; rebuilt;ablation-dist-preflightmarker present in 2 built files. Predicted and observed:driver-sqlite-wasm"6 failed | 5 passed" (the 5 refusal pins and the durability refusal case red; 3 controls and 2 durability cases green);service-datasource"7 failed | 3 passed" (all of (a) and (b) red, the 3 controls green). Restore: blob back to07f7cb3e0fa3== HEAD,git diff HEADempty, rebuilt, preflight--absentin 0 of 6 built files, tree clean; both pins green again (11/11, 10/10).Public door, measured
bin/run-dev.js migrate apply --yes(tsx, built closure), a project declaringarchivewithdriver: 'sqlite-wasm',config.filename: './data/archive.db', one object bound, over a 25-byte plain-text file:✗ Plugin plugin.app.door_25019 failed to start; md5bcbdbe7ff2cba060ed588d3fca48b30cbefore and after;data/holdsarchive.dbalone.mv './data/archive.db' './data/archive.db.unreadable') and running the same command again: exit 0,datasource 'archive': connected (driver=sqlite-wasm, schemaMode=managed), the moved file's md5 unchanged.Gates and battery
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackate39b18e5derived 68; each run once with its exit captured to a file before any pipe. 65 exit 0 on the first pass. Three did not:node scripts/check-plugin-teardown-shape.mjs --self-testexit 3: this shallow clone lacked its pinned fixture commit; fetched it, re-run exit 0 (48 cases).pnpm check:dispatcher-error-vocabularyexit 1: itsobjlitpattern matched the refusal's code TYPE (code: 'SQLITE_NOTADB' | 'SQLITE_CORRUPT'in a type literal). The gate's own self-test declares a type annotation is not a stamp and passes a named type, so the type was named (UnreadableDatabaseFileCode,cda55e654); re-run exit 0 ("56 unregistered code-stamping site(s), all classified"). Disclosed below.pnpm check:dual-build-cjs-loadsexit 3, PREREQUISITE NOT MET (eight packages outside this closure have nodist/): NOT MEASURED locally, CI'sLint & Repo Gatesruns it.--ranatcda55e654: "67 derived famil(ies) accounted for — 67 run, 0 NOT-MEASURED".check:driver-conformancebefore (f7d740376) and after: "55 covered cell(s), 0 in the DEBT ledger, 0 exempt" both; dialect axis unchanged.check-platform-checklistnot owed (no exported symbol renamed or removed;quarantineCorruptFilewas private).test:repoand the CLI integration layer not run locally (no such file touched); CI owns them.e39b18e5(pnpm install --frozen-lockfile, the@objectstack/dogfood^...closure build,pnpm --filter @objectstack/dogfood test, no shard, no path): lock VERDICT command-exit 1, "Test Files 1 failed | 286 passed | 3 skipped (290) · Tests 2395 passed | 21 skipped (2416)". The one failure is a suite-levelbeforeAlltimeout (10 s,bootShowcase()) intest/showcase-single-platform-insert-default-organization.dogfood.test.ts, before any of its 3 tests. Re-run alone it times out the same way, and the CONTROL (the same file withwasm-connection.tsrestored to basef7d740376and rebuilt,quarantineCorruptFilepresent indist/) times out identically, so it is not this change: the showcase boots a fresh wasm database (no file to refuse) and runs out of hook time on this shared box. Tree restored and rebuilt after the control (dist/index.mjsmd5 back to11790b18…). CI's full battery is the reading of record.cda55e654(a type alias): rebuiltdist/index.mjsanddist/index.jsare byte-identical before and after it (md511790b18…,f3177051…).Acceptance notes
patch, not the ruling'sminor. The ruling's item 4 namesminor; the level now follows the metadata-protocol kind alone (AGENTS.md Post-Task Checklist step 3;check-changeset-no-major.mjs, maintainer ruling A on changeset levels: minor only when the metadata protocol changes (added / narrowed / semantic), patch otherwise; release notes grouped by what the reader must do (maintainer ruling A, 2026-10-21) #25105), and nothing a metadata author writes changes here, so the kind isnone, which the gate gradespatchand refuses atminor. BREAKING-ness is carried by theClause-②arm, the banner and the ADR-0087 disposition, as24837-managed-datasource-sync-refused.mddoes. The seat may re-grade.Acquire connection error:with the refusal's stack atwarnonce per acquire attempt (its own logger,knex/lib/client.js). The native refusal never fails an acquire, so it does not print this; every other dialect's acquire failure (a refused PG connection) does. Noted, not filed.packages/cli/src/utils/telemetry-datasource.ts) still catches every throw, so a damaged telemetry sibling is now neither renamed nor reported. Pre-existing best-effort shape, out of the claim.check:dispatcher-error-vocabulary: the refusal stamps SQLite's own result-code names (SQLITE_NOTADB/SQLITE_CORRUPT) through a ternary assignment, a position outside that gate's declared population, asdriver-sqlcarries the same names (copied from better-sqlite3's error, and asSQLITE_FILE_REFUSALSkeys). They are foreign vocabulary, not ADR-0112 codes. Aforeign-vocabularyrow inpackages/runtime/src/dispatcher-error-vocabulary.tswould make that explicit; it is outside this claim's file surface, so it is left to the seat.SQLITE_CORRUPT: the bytes were read off disk and sql.js could not read them as a database, which is the file's verdict. An operating-system failure reading the file (EACCES) is not this path and is unchanged.Generated by Claude Code