Repository navigation
Commit 0142415
fix(driver-turso)!: refuse a remote url beside syncUrl, and any replica not on a local file, instead of running on :memory: (#19971)
Fixes #19893
Clause-②: no (narrowing)
## What was wrong
`TursoDriver.detectMode` classified a remote `url` (`libsql://`,
`https://`, `http://`, `wss://`, `ws://`) beside `syncUrl` as `replica`.
The last arm of `toKnexConfig` then handed that mode's local engine
(Knex + better-sqlite3) `:memory:`. Every write read back, then vanished
on restart, and nothing reached the remote. The code comment saying
`@libsql/client` "operates in embedded replica mode with an in-memory
local cache" was false.
## What this changes
- **The constructor refuses** these configurations before `super()`,
beside the two existing `timeout` refusals and with the same envelope
(`VALIDATION_ERROR` / 400):
- a remote url in a local or replica mode: auto-detected through
`syncUrl` (the card), or forced with `mode: 'replica'` / `mode:
'local'`;
- a replica whose `url` is not a local `file:` path: `:memory:`,
`file::memory:`, or a bare path under a forced `mode: 'replica'`.
- **The message** names the scheme it met and both ways out: drop
`syncUrl` (or the forced `mode`) for a remote database, or use `url:
'file:./data/replica.db'` beside `syncUrl` for an embedded replica. It
never echoes the url, which may carry a live `?authToken=` (pinned).
- **`detectMode` returns the same answers as before**: the pair still
classifies `replica`, and the refusal is a construction check, not a
re-classification (pinned). It now shares one remote-prefix list with
the refusal, and the false comment is replaced with the measured
reading.
- **README**: a paragraph under the auto-detection table, plus a
correction to the `syncUrl` config comment. The mode table never listed
the pair.
- **Test fixtures**: replica-face fixtures that rode `:memory:` +
`syncUrl` now get a fresh local file per driver
(`replica-file.testkit.ts`), keeping the per-test isolation `:memory:`
gave them. Two tests pinned the defect itself and were replaced (see
Acceptance notes).
**In-place fold, named per the bounded in-place fix rule.** A forced
`mode: 'local'` beside a remote url is not the card's arm, but it
reaches the identical `toKnexConfig` last arm (measured below: 1 row
back, 0 after restart). It is the same defect class, caught by the same
predicate (the classifier's own remote-prefix list), in the same file,
under the same gates. So it is folded into the one guard rather than
filed.
**Deliberately NOT changed.** A url with no `mode` that is none of
`file:`, `:memory:` or a lowercase remote scheme, such as an uppercase
`LIBSQL://` or a bare path, still auto-detects `local` and still runs on
`:memory:`. That fall-through is recorded at `ridesWebSocketTransport`
as deliberately left alone, and
`turso-driver-uppercase-ws-scheme-timeout-refusal.test.ts` (CONTROL 1)
pins it as constructing. Changing it needs its own decision, so it goes
back to the seat as a separate finding. `planMediaColumnMove` and the
remote media-column paths are untouched; #19894 remains open.
## Measurements, BASE `2c1011b01b`
A throwaway probe (not committed) ran `initObjects`, `create` and
`find`, then built a fresh driver on the same config (a restart):
| configuration | mode | knex engine | rows back | rows after restart |
|:---|:---|:---|:---|:---|
| `libsql://` + `syncUrl` + client stub, `sync.onConnect: false` (the
card's repro) | replica | `:memory:` | 1 (the stub held no tables) | 0 |
| `libsql://` / `https://` / `wss://` + `syncUrl`, real client,
`onConnect: false` | replica | `:memory:` | 1 | 0 |
| `libsql://` + `syncUrl`, real client, default sync | replica |
`:memory:` | `connect()` rejects `SYNC_NOT_SUPPORTED` | n/a |
| `libsql://` + `mode: 'replica'`, no `syncUrl` | replica | `:memory:` |
1 | 0 |
| `libsql://` + `mode: 'local'` | local | `:memory:` | 1 | 0 |
| `:memory:` + `syncUrl`, real client, default sync | replica |
`:memory:` | `connect()` rejects `URL_INVALID` | n/a |
| `:memory:` + `syncUrl` + client stub, `onConnect: false` | replica |
`:memory:` | 1 (the stub held no tables) | 0 |
| uppercase `LIBSQL://`, no mode (not changed here) | local | `:memory:`
| 1 | 0 |
| bare path, no mode (not changed here) | local | `:memory:` | 1; the
file was never created | 0 |
| CONTROL `file:` + `syncUrl` + client stub | replica | the file | 1 | 1
|
| CONTROL `file:` local | local | the file | 1 | 1 |
**Why `@libsql/client@0.17.4` builds no replica here.**
- `lib-esm/node.js` `_createClient` routes `wss`/`ws` to its WebSocket
client and `https`/`http` to its HTTP client; only anything else reaches
`sqlite3.js`.
- `syncUrl` is read only in `lib-esm/sqlite3.js`. A `syncUrl` grep over
`http.js` and `ws.js` returns 0; the control grep for `authToken`
returns 6 in each.
- `http.js` / `ws.js` `sync()`: `throw new LibsqlError("sync not
supported in http mode", "SYNC_NOT_SUPPORTED")`, and the ws variant is
the same.
- `sqlite3.js`: `if (isInMemory && config.syncUrl) throw new
LibsqlError("Embedded replica must use file for local db but URI with
in-memory mode were provided instead: …", "URL_INVALID")`.
**Where the config is parsed.** Neither factory parses a schema:
`packages/runtime/src/turso-driver-factory.ts` and
`packages/services/service-datasource/src/default-datasource-driver-factory.ts`
both go `buildTursoDriverConfig` then `new TursoDriver`. So this
constructor is the runtime's only gate. The spec-lane
`TursoConfigSchema` is parsed at authoring and wizard time through
`validateDriverConfig`. The driver-local copy has no in-repo parse
caller outside its own test. The matching authoring-time refinement is
outside this PR's surface (`packages/spec` untouched) and is handed to
the seat.
## Verification, HEAD `5fb95579`
- `pnpm --filter @objectstack/driver-turso typecheck`: `tsc --noEmit`,
exit 0. The program covers all 60 test files, including the new pin and
testkit (counted with `--listFiles`).
- `pnpm --filter @objectstack/driver-turso test`: `Test Files 60 passed
(60)`, `Tests 1358 passed (1358)`.
- **New pin** `turso-driver-remote-url-replica-refusal.test.ts`, 27
cases:
- 19 refusal cases assert the envelope (`code` + `status`), plus named
subjects in the message (the keys, the scheme, `url: 'file:`) and the
absence of an echoed token/host;
- 8 preservation cases: a `file:` replica that connects, writes and
keeps its row across a restart; the `file:` / `:memory:` local and
remote faces; `detectMode` unchanged.
- **Reverse verification**, direction predicted before running. With the
constructor's guard call deleted (`scripts/ablation-replace.mjs`, anchor
hit 1 to 0, blob `07363cdb` to `5ee02e4e`):
- `Tests 20 failed | 105 passed`: all 19 refusal cases plus the
converted `turso-driver.test.ts` case went red, each at "expected the
constructor to refuse, and it returned a driver";
- all 8 preservation cases stayed green;
- restore proven: blob equals HEAD (`07363cdb`) and `git diff HEAD` is
empty. The subject resolves from `src` (relative imports), so no `dist`
leg applies.
- **`pnpm check:driver-conformance`**: before `OK — 50 covered cell(s),
0 in the DEBT ledger, 0 exempt.`; after the same.
- **Changeset gates**:
- `check-adr-0087-registration`: `1 declared-breaking changeset(s), each
carrying an ADR-0087 disposition.`, with this changeset classified
`[BREAKING+bang+clause-②-narrowing]` `not-required
(no-migration-prescription)`;
- `check-changeset-no-major`: `This diff introduces no major bump.`;
- `check-empty-changeset`: green.
- **Gates**: `dispatch-gates --commands` on the real diff derived 61
commands; `--ran` reconciles `61 derived, 59 run, 2 NOT-MEASURED, 0
UNRUN`, and all 59 run exit 0. The four artifact-roster gates whose
roster sits under a touched directory were also run, all exit 0.
- **NOT MEASURED** (both `PREREQUISITE NOT MET`, needing a
whole-workspace build):
- `check:dual-build-cjs-loads`. Narrowed stand-in: the built
`driver-turso` CJS and ESM entries both load `TursoDriver`;
- `check:type-check-debt`. Narrowed stand-in: `driver-turso` has no
DEBT, TEST_DEBT or test-typecheck-debt entry, and its `tsc --noEmit` is
clean. CI runs both over the whole tree.
- **Lint, narrowed and proven.**
- Population, read from eslint itself: of the 14 changed paths it lints
the 12 `.ts` files and ignores the 2 `.md`.
- Count, from `--format json`: 12 files, 0 errors, 0 warnings.
- Invariance: `eslint.config.mjs` never enables type-aware linting (no
`parserOptions.project`, no typed rules; its own header says so), so
this diff cannot move any untouched file's verdict.
- Full `pnpm lint` is CI's.
## Acceptance notes
- **Replaced pins**:
- `turso-driver.test.ts` "should accept remote URL when syncUrl is
provided" pinned the defect and now asserts the refusal;
- its two "detect replica mode" cases for `:memory:` / `libsql://` +
`syncUrl` now pin `TursoDriver.detectMode` (unchanged) instead of
constructing;
- `turso-driver-ws-timeout-refusal.test.ts` built a `wss://` + `syncUrl`
replica (the card's pair). That half is removed and its successor lives
in the new pin file.
- **Loader fixtures outside this package** still pair `url:
'libsql://my-db.turso.io'` with `syncUrl` and `mode: 'replica'`:
`packages/cli/src/utils/storage-driver.test.ts` ("produces a config the
real driver accepts"),
`packages/runtime/src/turso-driver-factory.convergence.test.ts` and
`packages/services/service-datasource/src/__tests__/turso-driver-config.test.ts`.
They exercise the key-forwarding builder or a capturing constructor,
never the real driver, so they stay green. The first title now
overstates what the real driver accepts. They are outside this card's
file surface; there is no carrier.
- **Not measured**: whether writes on a `file:` + `syncUrl` replica
reach the remote primary. Knex writes the file through better-sqlite3,
not through the libsql connection. Measuring it needs a live sqld.
- **Reach, for the seat's grading**:
- in-repo, no example, template, published skill, hand-written doc or
factory default declares a remote url beside `syncUrl`; the host boot
path (`OS_DATABASE_URL`) passes none;
- with the driver's own client and the default `sync.onConnect`, the
card's pair failed at `connect()` rather than silently; silent loss
needed `sync.onConnect: false`, a supplied client, or a forced `mode`;
- out-of-repo deployments are not measured.
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 9bfbacb commit 0142415
14 files changed
Lines changed: 636 additions & 69 deletions
File tree
- .changeset
- packages/drivers/driver-turso
- src
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
130 | 130 | | |
131 | 131 | | |
132 | 132 | | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
133 | 152 | | |
134 | 153 | | |
135 | 154 | | |
| |||
216 | 235 | | |
217 | 236 | | |
218 | 237 | | |
219 | | - | |
| 238 | + | |
220 | 239 | | |
221 | 240 | | |
222 | 241 | | |
| |||
Lines changed: 44 additions & 0 deletions
| 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 | + | |
Lines changed: 220 additions & 0 deletions
| 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 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
0 commit comments