Repository navigation
Commit 27dc53b
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)
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgS2d2sPzGdbRqTa2fsR14Ze)_
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 to `FILE.corrupt-TIMESTAMP` and starts an
empty database in its place; `resolveSqliteDriver` re-throws the refusal
at the wasm rung instead of trying the in-memory driver. Nothing
exported changes shape. It owes a `CONTRACT_REVIEW_TIER` review 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:engine` seat 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 session
`session_015pbbff7SHh7EEEWiasBdDe` (a project card thread of seat
session `session_01FaVyM6YvQLg9CsjPVCYc5K`), claim 6335885549; the
cross-lane line in `service-datasource` is declared on #21118
(6335909713).
## What changed
- `packages/drivers/driver-sqlite-wasm/src/wasm-connection.ts`
- `open()`: a file whose bytes sql.js cannot open or read (`new
SQL.Database(bytes)` or `PRAGMA quick_check(1)` throws, or `quick_check`
does not answer `ok`) is refused with `unreadableDatabaseFileRefusal`:
an `Error` carrying `code` (`SQLITE_NOTADB` when SQLite says "file is
not a database", `SQLITE_CORRUPT` for 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 bare `Error` with SQLite's message
and no result code, so the producer maps SQLite's own errstr for
`SQLITE_NOTADB`; nothing downstream reads message text.
- `quarantineCorruptFile` retires (it was private). The refused file is
left byte-for-byte as found and nothing is written beside it.
- `quarantineOrphanedWal` now 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's `catch` re-throws when
`isSqliteFileOpenFailure(err)` holds, exactly as the native rung does.
The predicate needed no new code: the wasm refusal reaches it as
`SqlDriver`'s own `SqliteDatabaseFileError` with `SQLITE_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).
- Pins:
`packages/drivers/driver-sqlite-wasm/src/sqlite-wasm-25019-unreadable-file-refused.test.ts`
(relative `src` import) and
`packages/services/service-datasource/src/__tests__/sqlite-driver-fallback-wasm-unreadable-file.test.ts`
(resolves `@objectstack/driver-sqlite-wasm` through `dist/`).
- Changeset `.changeset/25019-wasm-unreadable-file-refused.md`:
`@objectstack/driver-sqlite-wasm` and `@objectstack/service-datasource`
`patch`, `Clause-②: no (narrowing)`, `Metadata-protocol: none`, BREAKING
banner (FROM, TO, Unchanged, fix) and ADR-0087 disposition `not-required
(no-migration-prescription)`.
## One vocabulary with the native driver, measured
`SqliteWasmDriver` is a `SqlDriver`, and wasm SQLite's connection is
opened by knex's pool on the first statement `SqlDriver.connect()` runs
(`PRAGMA auto_vacuum`). The pool propagates the create error
(`propagateCreateError: true`), so `applyAutoVacuumIncremental` sees the
refusal's `code` and answers it through the same private
`SQLITE_FILE_REFUSALS` table the native driver uses, wrapping it in
`SqliteDatabaseFileError`. 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 to
`driver-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 by `grep`, zero hits in
`driver-sql/src`).
The line an operator sees (public door below):
```text
datasource 'archive': connect failed — SQLite database file './data/archive.db' (resolved: '…/door/data/archive.db') is not a SQLite database, and was left as it is: wasm SQLite cannot read it (file is not a database). To start from an empty database, move the file aside yourself (mv './data/archive.db' './data/archive.db.unreadable') and start again; to keep its data, restore a copy that opens.
```
## Premise (base `f7d740376`)
`quarantineCorruptFile` was at `wasm-connection.ts`, called from
`open()` on a read failure, renaming to `FILE.corrupt-` plus
`Date.now()`; the wasm rung of `resolveSqliteDriver` was a bare `catch`
stepping down to memory. Both doors were measured by the dev report
6310958104 (open question 1): (a) a declared `sqlite` datasource whose
native addon cannot load, over a plain-text or header-truncated file,
renamed on a development boot; (b) a datasource declared `driver:
'sqlite-wasm'` over a plain-text file, renamed and `available` in
development and production. sql.js 1.14 answers, measured: plain text
and magic-header junk throw "file is not a database" at `quick_check`; a
real database truncated to 50 bytes or to half throws "database disk
image is malformed"; a real database and an empty file answer `ok`.
## Tests
- New pins: `driver-sqlite-wasm` 11/11 across the two files;
`service-datasource` 10/10: (a) the native-addon-cannot-load step-down
(a real `ERR_DLOPEN_FAILED` at the #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 through `DatasourceConnectionService` in
development reads `failed`, no driver registered; (b) `driver:
'sqlite-wasm'` over both files in development and production reads
`failed` with the code's words naming the file, not `available`, nothing
moved; (c) controls: a real database opens and answers (wasm, and
`sqlite-wasm` declared, both modes `available`), an absent file is
created, an empty file opens, a native addon that cannot load over an
absent file still steps down to wasm.
- Whole packages (pre-merge head `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)"; `typecheck` for both, exit 0.
- Ablation (committed fix at `f8ffdfc0`,
`scripts/ablation-replace.mjs`): the refusal's `throw` replaced by the
old quarantine (rename to `.corrupt-`, empty database, plus a marker),
blob `07f7cb3e0fa3` to `15743567f704`; rebuilt;
`ablation-dist-preflight` marker 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 to `07f7cb3e0fa3` ==
HEAD, `git diff HEAD` empty, rebuilt, preflight `--absent` in 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
declaring `archive` with `driver: 'sqlite-wasm'`, `config.filename:
'./data/archive.db'`, one object bound, over a 25-byte plain-text file:
- exit 1; the line above, from `✗ Plugin plugin.app.door_25019 failed to
start`; md5 `bcbdbe7ff2cba060ed588d3fca48b30c` before and after; `data/`
holds `archive.db` alone.
- Following the prescription (`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
- Derived set: `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` at `e39b18e5` derived 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-test` exit 3:
this shallow clone lacked its pinned fixture commit; fetched it, re-run
exit 0 (48 cases).
- `pnpm check:dispatcher-error-vocabulary` exit 1: its `objlit` pattern
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-loads` exit 3, PREREQUISITE NOT MET (eight
packages outside this closure have no `dist/`): NOT MEASURED locally,
CI's `Lint & Repo Gates` runs it.
- `--ran` at `cda55e654`: "67 derived famil(ies) accounted for — 67 run,
0 NOT-MEASURED".
- `check:driver-conformance` before (`f7d740376`) and after: "55 covered
cell(s), 0 in the DEBT ledger, 0 exempt" both; dialect axis unchanged.
- `check-platform-checklist` not owed (no exported symbol renamed or
removed; `quarantineCorruptFile` was private). `test:repo` and the CLI
integration layer not run locally (no such file touched); CI owns them.
- Full dogfood battery at `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-level
`beforeAll` timeout (10 s, `bootShowcase()`) in
`test/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 with `wasm-connection.ts` restored to base
`f7d740376` and rebuilt, `quarantineCorruptFile` present in `dist/`)
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.mjs` md5 back to `11790b18…`). CI's full battery is the
reading of record.
- The only commit after the battery is `cda55e654` (a type alias):
rebuilt `dist/index.mjs` and `dist/index.js` are byte-identical before
and after it (md5 `11790b18…`, `f3177051…`).
## Acceptance notes
- **Level `patch`, not the ruling's `minor`.** The ruling's item 4 names
`minor`; 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 #25105), and nothing a metadata author writes
changes here, so the kind is `none`, which the gate grades `patch` and
refuses at `minor`. BREAKING-ness is carried by the `Clause-②` arm, the
banner and the ADR-0087 disposition, as
`24837-managed-datasource-sync-refused.md` does. The seat may re-grade.
- **The ruling's named cost:** a development server whose own wasm image
is damaged no longer starts itself clean; the refusal's prescription
says how to move the file aside, and the public door above shows that
step working.
- knex logs `Acquire connection error:` with the refusal's stack at
`warn` once 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.
- Named only, per family note 6305864303: the development telemetry
sibling (`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.
- #25084 (the refused-connect handle) is not addressed here; a wasm
refusal holds no connection (the open failed before the pool kept one).
- `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, as
`driver-sql` carries the same names (copied from better-sqlite3's error,
and as `SQLITE_FILE_REFUSALS` keys). They are foreign vocabulary, not
ADR-0112 codes. A `foreign-vocabulary` row in
`packages/runtime/src/dispatcher-error-vocabulary.ts` would make that
explicit; it is outside this claim's file surface, so it is left to the
seat.
- Every read failure other than SQLite's "file is not a database" is
classified `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](https://claude.ai/code/session_015pbbff7SHh7EEEWiasBdDe)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent e5f194b commit 27dc53b
6 files changed
Lines changed: 527 additions & 63 deletions
File tree
- .changeset
- packages
- drivers/driver-sqlite-wasm/src
- services/service-datasource/src
- __tests__
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
24 | | - | |
25 | | - | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
26 | 28 | | |
27 | 29 | | |
28 | 30 | | |
| |||
70 | 72 | | |
71 | 73 | | |
72 | 74 | | |
73 | | - | |
| 75 | + | |
74 | 76 | | |
75 | 77 | | |
76 | 78 | | |
77 | | - | |
| 79 | + | |
78 | 80 | | |
79 | 81 | | |
80 | 82 | | |
| |||
90 | 92 | | |
91 | 93 | | |
92 | 94 | | |
93 | | - | |
94 | | - | |
95 | | - | |
96 | | - | |
97 | | - | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
98 | 105 | | |
99 | | - | |
100 | | - | |
101 | | - | |
102 | | - | |
103 | | - | |
104 | | - | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
105 | 110 | | |
106 | 111 | | |
107 | 112 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
86 | 86 | | |
87 | 87 | | |
88 | 88 | | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
89 | 138 | | |
90 | 139 | | |
91 | 140 | | |
| |||
198 | 247 | | |
199 | 248 | | |
200 | 249 | | |
201 | | - | |
202 | | - | |
203 | | - | |
204 | | - | |
205 | | - | |
206 | | - | |
207 | | - | |
208 | | - | |
209 | | - | |
210 | | - | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
211 | 265 | | |
212 | | - | |
| 266 | + | |
213 | 267 | | |
214 | | - | |
215 | 268 | | |
216 | | - | |
217 | | - | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
218 | 275 | | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
219 | 281 | | |
220 | 282 | | |
221 | 283 | | |
| |||
233 | 295 | | |
234 | 296 | | |
235 | 297 | | |
236 | | - | |
237 | | - | |
238 | | - | |
239 | | - | |
240 | | - | |
241 | | - | |
242 | | - | |
243 | | - | |
244 | | - | |
245 | | - | |
246 | | - | |
247 | | - | |
248 | | - | |
249 | | - | |
250 | | - | |
251 | | - | |
252 | | - | |
253 | | - | |
254 | | - | |
255 | | - | |
256 | | - | |
257 | | - | |
258 | | - | |
259 | | - | |
260 | | - | |
261 | | - | |
262 | | - | |
263 | | - | |
264 | | - | |
265 | 298 | | |
266 | 299 | | |
267 | 300 | | |
| |||
0 commit comments