Repository navigation
Commit 7f99616
fix(driver-sql): a SQLite file that is not a database fails connect() at once, naming it, and a dev boot no longer renames and replaces it (#25006)
Fixes #24960
Clause-②: no (narrowing)
**Narrowing face:** a `SqlDriver.connect()` that resolved now refuses.
On a SQLite file whose bytes are not a SQLite database, `connect()` used
to log and swallow `SQLITE_NOTADB` and resolve; it now rejects with
`SqliteDatabaseFileError` (`code: 'SQLITE_NOTADB'`, `filename`, a
message naming the file), in development and production alike. On a
development boot, `resolveSqliteDriver` re-throws that refusal instead
of stepping down to wasm SQLite, so the operator's file is no longer
renamed to `FILE.corrupt-TIMESTAMP` and replaced with an empty database.
Nothing exported changes shape.
Route A, the card's seat ruling, as dispatched: the producer refuses the
file, and the step-down classifies the code. B (consumer-only sniffing)
and C (keep the wasm quarantine as dev recovery) are not taken.
## What changed
- `packages/drivers/driver-sql/src/sql-driver.ts`, the
`applyAutoVacuumIncremental` region only. The first-statement codes that
are a verdict on the file now sit in one table, `SQLITE_FILE_REFUSALS`:
`SQLITE_CANTOPEN` (as #23696 shipped it, same words) and `SQLITE_NOTADB`
(new, "is not a SQLite database, and was left as it is"). Either throws
`SqliteDatabaseFileError` before anything is written. Every other PRAGMA
failure keeps today's warning and `return false`.
- `packages/drivers/driver-sql/src/sqlite-database-file.ts`: the class
doc names the new code. Doc only.
- `packages/services/service-datasource/src/sqlite-driver-fallback.ts`:
`isSqliteFileOpenFailure` adds `SQLITE_NOTADB`, with its doc list and
one sentence in the module note. This is the cross-lane line for
`domain:services` that the dispatching seat declared.
`resolveSqliteAbsentFileTarget`, `knexConfigFor` and the probe body are
untouched, because #24961 is changing those in parallel. A local `git
merge-tree` of this head with that branch's head `28d5dd67e` is clean.
- `.changeset/24960-sqlite-notadb-connect.md`: `patch` for
`@objectstack/driver-sql` and `@objectstack/service-datasource`. It
carries the `Clause-②: no (narrowing)` line, the BREAKING banner with
the operator's fix, and an ADR-0087 `not-required
(no-migration-prescription)` disposition.
- Two new pin files:
`packages/drivers/driver-sql/src/sql-driver-24960-sqlite-not-a-database.test.ts`
and
`packages/services/service-datasource/src/__tests__/sqlite-driver-fallback-not-a-database.test.ts`.
## Premise, measured (better-sqlite3 13.0.3, Node v22.22.0)
- **H1 holds.** On a plain-text file, better-sqlite3's `new
Database(file)` succeeds, and the first statement, `PRAGMA auto_vacuum`,
throws `code: 'SQLITE_NOTADB'` ("file is not a database"). So do `PRAGMA
journal_mode` and `SELECT 1`. A file whose 100-byte header was
overwritten past the magic string answers the same code. On base,
`applyAutoVacuumIncremental` matched only `SQLITE_CANTOPEN`, logged
"Failed to set PRAGMA auto_vacuum=INCREMENTAL" and returned `false`, so
`connect()` resolved.
- **H2 holds.** With the code in `isSqliteFileOpenFailure`, the
development probe re-throws at the native rung, and wasm's
`quarantineCorruptFile` is never reached for a declared datasource whose
addon loads. The pins assert that the file's bytes are unchanged and
that the directory holds only that file: no `.corrupt-*` sibling, no
journal, no WAL.
- **H3 holds.** The card's "native better-sqlite3 unavailable (ABI
mismatch or not built)" line is the step-down's own
`NATIVE_SQLITE_WASM_FALLBACK_WARNING` ("dev using wasm SQLite"). The
driver's similar warning fires only for a native load failure, which
`SQLITE_NOTADB` is not. With this change the step-down never runs on
this path, so the warning no longer prints: the pin asserts `warn` was
never called, and the public-door run below prints neither line.
- **Does any caller rely on `connect()` resolving over a file that is
not a database?** I found none. I grepped the tree for `NOTADB` / "not a
database" / `SQLITE_CORRUPT` / "disk image is malformed", for tests that
write arbitrary bytes into a `.db` file, and read every
`resolveSqliteDriver` caller.
- `os db clean`: it called `resolveSqliteDriver({ dev: true })` and then
VACUUMed. On base, a garbage file was moved aside, and the "clean"
reported the empty replacement's size. Now it refuses the file, which is
the better answer.
- The development telemetry sibling (`telemetry-datasource.ts`): it
already drops its separation on any throw, `catch` then `undefined`, the
same as for #24950's codes. The boot continues.
- `dev-organization-ownership-upgrade.test.ts` writes text into `dev.db`
but drives a fake CLI, and no driver opens it.
- `TursoDriver` local and replica modes open the file through this
`SqlDriver.connect()`. Measured: a file written by `@libsql/client` with
an `encryptionKey` answers better-sqlite3 with `SQLITE_NOTADB`. Such a
datasource is now refused at connect, naming the file. Before, connect
resolved and every read through Knex failed with the same code, so
nothing that worked stops working.
## Evidence
**Pins** (all at head `6d3dd6247`, under `os-verify-lock`):
- driver-sql, `sql-driver-24960-sqlite-not-a-database.test.ts`: a
plain-text file is refused with `SqliteDatabaseFileError`. The pin
checks `code` = `SQLITE_NOTADB`, `filename`, a message naming the file
and carrying the cause, the same bytes after, and the directory listing
equal to the one file. It runs under both `NODE_ENV=development` and
`NODE_ENV=production`. Controls: a real database reconnects and answers,
and an empty file connects. Result: 8/8 with the #23696 file.
- service-datasource, `sqlite-driver-fallback-not-a-database.test.ts`:
- development `resolveSqliteDriver` rejects with the driver's refusal,
warns nothing, and leaves the file untouched;
- production returns native unprobed, and its `connect()` refuses the
file;
- `DatasourceConnectionService.connectDeclared` reports `availability:
'failed'` with a reason naming the file and registers no driver, in
development and production, and the file is untouched.
Result: 24/24 with the #24950 file and `sqlite-driver-fallback.test.ts`.
- Whole packages at `c5b499cf0`, before merging `main`: driver-sql "Test
Files 226 passed | 12 skipped (238) · Tests 3696 passed | 209 skipped
(3905)" (the skips are the live PG/MySQL cells); service-datasource
"Test Files 51 passed (51) · Tests 912 passed (912)". `tsc --noEmit`
exit 0 for both. `tsc --listFiles` includes each new test file (1 hit
each).
- The merge of `origin/main` at `4fe3af9d7` brought #24778, #24970 and
#24990. They touch spec, objectql, ui and docs, none of these packages,
and the targeted pins above were re-run on the merge head.
**Ablation** (`scripts/ablation-replace.mjs` WRAP with a trap restore,
at `c5b499cf0`):
- **Leg A, the driver.** The `SQLITE_NOTADB` table row was mutated to
`ABLATION_24960_NOTADB`: anchor 1 to 0, blob `b99ad91eeb7e` to
`3eb1e25b29ac`. driver-sql was rebuilt, and
`ablation-dist-preflight.mjs` found the marker in 2 built files.
- Predicted and observed: driver-sql "2 failed | 6 passed". Red: the
development and production refusal pins (`connect() resolved for a file
that is not a SQLite database`). Green: the controls and the #23696
file.
- Predicted and observed: service-datasource "4 failed | 9 passed". Red:
development resolved to engine `sqlite-wasm`; the production refusal was
`undefined`; both `DatasourceConnectionService` cases read
`'available'`.
- Restore proved: blob == HEAD, `git diff HEAD` empty. After the
rebuild, preflight `--absent` found the marker in none of the 6 built
files, and the tree was clean.
- **Leg B, the step-down.** `|| code === 'SQLITE_NOTADB'` was mutated to
`ABLATION_24960_SD`: blob `165d52e2d851` to `bda6c0c91769`. The subject
is a relative `src` import, so this leg needs no dist step.
- Predicted and observed: "2 failed | 11 passed". Red: the two
development cases. Green: the two production cases, which the driver
alone carries, and the 9 cases of #24950.
- Restore proved: blob == HEAD, `git diff HEAD` empty.
**Public door, the card's own:** `bin/run-dev.js migrate apply --yes
--database-url file:main.db` over a `dist/objectstack.json` that
declares `archive` (`driver: sqlite`, `autoConnect: true`) on a
plain-text file, at head `6d3dd6247` after a `turbo run build
--filter=@objectstack/cli^...` (60/60).
- **Object bound to `archive`:** the run prints `datasource 'archive':
connect failed — SQLite database file '…/data/archive.db' is not a
SQLite database, and was left as it is: PRAGMA auto_vacuum - file is not
a database. (1 object(s) bind to it explicitly … fail-fast per ADR-0062
D5)` and exits 1. The md5 is unchanged and `data/` holds `archive.db
main.db`.
- **autoConnect only:** the run prints `WARN datasource 'archive':
connect failed — … is not a SQLite database … — degrading (datasource
left unconnected)`, then `Applied 0 change(s)`, and exits 0. The md5 is
unchanged, there is no `.corrupt-*` sibling, and no step-down warning
prints.
- The card measured the base on the same door: the file was renamed, an
empty 16384-byte database was written, and the run read `connected`.
**Driver ledger** (`pnpm check:driver-conformance`):
- Before, at `4b8f597ba`: "55 covered cell(s), 0 in the DEBT ledger, 0
exempt"; dialect axis 9 suites, 8 matrix and 1 named, 0 in the DIALECT
ledger.
- After, at `6d3dd6247`: the same counts. The ledger did not move.
**Gates.** `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` (no paths) at `6d3dd6247` derived 72
commands: the seat's 55 plus 17 for the changeset and the new test
files. I ran each one once, sequentially, with its exit captured before
any pipe, and all 72 exit 0. Two answered exit 3 (`PREREQUISITE NOT
MET`) on the first pass and were re-run after their prerequisite:
- `check-plugin-teardown-shape.mjs --self-test` needed its pinned
fixture commit, fetched at depth 1;
- `check:dual-build-cjs-loads` needed every package's `dist/`, from a
`turbo run build` that was 75 of 76 cache hits.
`--ran` reconciliation: "72 derived, 72 run, 0 NOT-MEASURED, 0 UNRUN", a
derived zero because all 72 recorded an exit code. CI owns the full farm
and `test:repo`.
## Acceptance notes
- **`SQLITE_CORRUPT`, the same defect one code over, is not in this
PR.** It is reported to the seat; this PR leaves it alone. A SQLite
database truncated inside its 100-byte header answers the first `PRAGMA`
with `SQLITE_CORRUPT` ("database disk image is malformed"), which
`connect()` still swallows. I ran the same door at this head with an
object bound to `archive`: `archive.db` was moved to
`archive.db.corrupt-1792539192447`, an empty database was written in its
place, the run read `datasource 'archive': connected` under the "native
better-sqlite3 unavailable" warning, and it exited 0. The fix would be
one more row in `SQLITE_FILE_REFUSALS` and one more code in
`isSqliteFileOpenFailure`. It stays out because the ruling and the claim
name `SQLITE_NOTADB`, and `sqlite-driver-fallback.ts` is also held by
the #24961 claim, so the in-place exemption does not hold.
- **A residual rename this route cannot reach.** When the native addon
cannot LOAD (an ABI mismatch) and the declared file is not a database,
the native rung cannot read the file to classify it. The step-down still
goes to wasm, which renames the file. I measured this with the #24950
suite's own load-failure seam (`acquireRawConnection` answering a real
`ERR_DLOPEN_FAILED`): the run resolved to `sqlite-wasm`, and `data/`
held `archive.db` and `archive.db.corrupt-TIMESTAMP`. It lives in the
wasm quarantine, which the claim scopes out.
- The probe body's comments in `sqlite-driver-fallback.ts` ("refuses a
file it cannot open or create") still name only the open-failure half. I
left them unedited to stay off the #24961 region.
- After this branch's last merge, `main` gained #24992 (plugin-auth),
which this diff does not touch. I did not merge it, so the gate union
stays measured on the head it names.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01QK6pdbxKZiHdFApL8RYVh9)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 1a1f5c3 commit 7f99616
6 files changed
Lines changed: 302 additions & 22 deletions
File tree
- .changeset
- packages
- drivers/driver-sql/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 | + | |
| 19 | + | |
| 20 | + | |
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7059 | 7059 | | |
7060 | 7060 | | |
7061 | 7061 | | |
| 7062 | + | |
| 7063 | + | |
| 7064 | + | |
7062 | 7065 | | |
7063 | 7066 | | |
7064 | 7067 | | |
| |||
7082 | 7085 | | |
7083 | 7086 | | |
7084 | 7087 | | |
7085 | | - | |
7086 | | - | |
7087 | | - | |
7088 | | - | |
7089 | | - | |
7090 | | - | |
| 7088 | + | |
| 7089 | + | |
| 7090 | + | |
| 7091 | + | |
| 7092 | + | |
| 7093 | + | |
| 7094 | + | |
| 7095 | + | |
| 7096 | + | |
| 7097 | + | |
| 7098 | + | |
| 7099 | + | |
| 7100 | + | |
| 7101 | + | |
| 7102 | + | |
7091 | 7103 | | |
7092 | 7104 | | |
7093 | | - | |
| 7105 | + | |
7094 | 7106 | | |
7095 | 7107 | | |
7096 | 7108 | | |
| |||
7109 | 7121 | | |
7110 | 7122 | | |
7111 | 7123 | | |
| 7124 | + | |
| 7125 | + | |
| 7126 | + | |
| 7127 | + | |
| 7128 | + | |
| 7129 | + | |
| 7130 | + | |
| 7131 | + | |
| 7132 | + | |
| 7133 | + | |
| 7134 | + | |
| 7135 | + | |
| 7136 | + | |
7112 | 7137 | | |
7113 | 7138 | | |
7114 | 7139 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
8 | | - | |
| 8 | + | |
| 9 | + | |
9 | 10 | | |
10 | 11 | | |
11 | 12 | | |
12 | 13 | | |
13 | 14 | | |
14 | | - | |
| 15 | + | |
15 | 16 | | |
16 | 17 | | |
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 | + | |
0 commit comments