Skip to content

Commit 4a7cb4d

Browse files
committed
docs(driver-turso): correct the pending remote-url refusal note, state 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>
1 parent e5e29cb commit 4a7cb4d

3 files changed

Lines changed: 46 additions & 6 deletions

File tree

‎.changeset/19893-turso-remote-url-replica-refusal.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,18 @@ file: + syncUrl (unchanged) -> 1 row back, 1 row after restart
1717

1818
With the driver building its own client and the default `sync.onConnect`, two of these did fail at `connect()`, but on a libsql error that did not say why: `libsql://` + `syncUrl` with `SYNC_NOT_SUPPORTED`, and `:memory:` + `syncUrl` with `URL_INVALID`.
1919

20-
**BREAKING** accept-set narrowing on a published driver option, shipped as `minor` under the repo's launch-window convention for breaking changes (`scripts/check-changeset-no-major.mjs`). **The constructor now refuses configurations it accepted before**, at `new TursoDriver()`, ahead of the Knex base and of any client, with the ADR-0112 envelope `code: 'VALIDATION_ERROR'`, `status: 400`. A remote url here means one of the lowercase schemes `TursoDriver.detectMode` classifies as remote: `libsql://`, `https://`, `http://`, `wss://`, `ws://`. Refused:
20+
**BREAKING** accept-set narrowing on a published driver option, shipped as `minor` under the repo's launch-window convention for breaking changes (`scripts/check-changeset-no-major.mjs`). **The constructor now refuses configurations it accepted before**, at `new TursoDriver()`, ahead of the Knex base and of any client, with the ADR-0112 envelope `code: 'VALIDATION_ERROR'`, `status: 400`. A remote url here means one of the schemes `TursoDriver.detectMode` classifies as remote: `libsql://`, `https://`, `http://`, `wss://`, `ws://`. The #19976 entry in this same version matches them in any letter case, so an uppercase `LIBSQL://` is a remote url too. Refused:
2121

2222
- a remote url beside `syncUrl`;
2323
- a remote url under a forced `mode: 'replica'` or `mode: 'local'`;
2424
- a replica on a url `@libsql/client` reads as in-memory (`:memory:`, or `file::memory:` with or without a query string), beside `syncUrl` or under a forced `mode: 'replica'`. `@libsql/client` refuses such an embedded replica itself. For `:memory:` and a bare `file::memory:` the local engine was a private in-memory database. With a query string it was a file literally named after the url's path (for example `:memory:?cache=shared`) in the working directory, which no sync reaches;
25-
- under a forced `mode: 'replica'` only, any `url` that is not a local `file:` path, such as a bare path or an uppercase scheme.
25+
- under a forced `mode: 'replica'`, a `url` that is none of `:memory:`, a `file:` url or a remote url, such as a bare path or an unsupported scheme. The #19976 entry in this same version refuses such a url in every local or replica mode, and matches the `file:` scheme in any letter case: an uppercase `FILE:` url naming a file is a `file:` url and is not refused, and the replica runs on that file (`FILE::memory:` is refused as in-memory, like `file::memory:`).
2626

2727
The remote-url refusal names the scheme it met. Neither refusal echoes the url, which may carry a token. Both loaders (`@objectstack/runtime`'s host factory and the datasource factory) reach this refusal through the same constructor, so a datasource declaring one of these configurations now fails by name when its loader builds the driver.
2828

2929
**What stays accepted**, pinned by preservation tests: a `file:` url with `syncUrl` (the embedded replica), a `file:` or `:memory:` local database, a remote url on its own or with `mode: 'remote'`. `TursoDriver.detectMode()` still classifies a remote url beside `syncUrl` as `'replica'`: the refusal sits in the constructor, not in a re-classification.
3030

31-
**Not refused, unchanged here:** 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 like `./data/app.db`, still auto-detects `'local'` and still runs on `:memory:`, with or without `syncUrl`. So does the same url under a forced `mode: 'local'`. That fall-through is tracked as #19976.
31+
**Not refused by this change:** a url with no `mode` that is none of a lowercase `file:` url, `:memory:` or a lowercase remote scheme, such as an uppercase `LIBSQL://`, an uppercase `FILE:` url or a bare path like `./data/app.db`, auto-detected `'local'` with or without `syncUrl`, and the local engine was handed `:memory:`. Under a forced `mode: 'local'` the same url got the same `:memory:` engine. The #19976 entry in this same version removes that fall-through: it matches every scheme in any letter case, so an uppercase remote url is a remote url and an uppercase `FILE:` url is a `file:` url, and it refuses every other url in a local or replica mode. No configuration runs on an in-memory database it did not name.
3232

3333
**What an affected author does.** Each refusal names its ways out. For a remote url in a local or replica mode:
3434

‎.changeset/19976-turso-unrecognised-url-refusal.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,13 @@ file:<path> (unchanged) -> local, 1 row back, 1 row after restart
2828

2929
The unrecognised-url refusal names the `file:` spelling (`url: 'file:./data/app.db'`, or `url: 'file:./data/replica.db'` beside `syncUrl`) and never echoes the url, which may carry a token. `TursoDriver.detectMode()` now answers `'replica'` for such a url beside `syncUrl` (it answered `'local'`), which is what the declaration asks for. The refusal sits in the constructor, not in a re-classification.
3030

31+
**Newly accepted:** an uppercase or mixed-case `FILE:` url naming a file, under a forced `mode: 'replica'`, with or without `syncUrl`. The #19893 change refused it, because under a forced `mode: 'replica'` it refused every url that did not start with a lowercase `file:`. With this change it is a `file:` url: the replica runs on that file, and its rows survive a restart (pinned). It is the one configuration the #19893 change refused that this change accepts.
32+
3133
**What stays accepted**, pinned by preservation tests: a lowercase `file:` url, alone or with `syncUrl`; `:memory:` as a local database; a lowercase remote url on its own or with `mode: 'remote'`. A forced `mode: 'remote'` runs no local engine, so this change does not judge its url: a bare path there still constructs, and `@libsql/client` refuses it at `connect()` as `URL_INVALID`.
3234

3335
**What an affected author does.** A local database file needs the `file:` prefix: `url: 'file:./data/app.db'`. For a throwaway in-memory database, `url: ':memory:'`. An uppercase remote url with no `mode` and no `syncUrl` now reaches the remote database and needs no change. Beside `syncUrl` or under a forced local or replica mode it is refused with the same ways out as the lowercase spelling.
3436

35-
This closes the fall-through that the changelog entry for #19893 (the constructor refusal of a remote url in a local or replica mode) lists as "not refused, unchanged here".
37+
The #19893 entry in this same version (the constructor refusal of a remote url in a local or replica mode) describes this fall-through as not refused by that change, and names this entry as the one that removes it.
3638

3739
Blast radius, measured on this tree: no example, template, hand-written doc, published skill or factory default, and no test fixture outside this package's own tests, spells a turso url with an uppercase scheme or as a bare path. Neither host url sniffer selects this driver for a bare path (both select it only for `libsql://` or an `http(s)://` url naming a `.turso.` host); only an explicit `OS_DATABASE_DRIVER=turso` or a datasource declaring `driver: 'turso'` hands it one. Whether any out-of-repo deployment declares such a url is NOT measured and is not claimed to be zero.
3840

‎packages/drivers/driver-turso/src/turso-driver-unrecognised-url-refusal.test.ts‎

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,20 @@
4747
* refusal's; the client refuses a bare path there itself, at `connect()`.
4848
* - PRESERVATION, beside this file's neighbours: `file:` local and replica,
4949
* `:memory:` local, a lowercase remote url, `mode: 'remote'`.
50+
* - The one WIDENED cell: an uppercase `FILE:` url naming a file under a
51+
* forced `mode: 'replica'`, with or without `syncUrl`. At `a7581b326` the
52+
* constructor refused it (a forced replica had to start with a lowercase
53+
* `file:`); it is now a `file:` url, so the replica runs on that file and
54+
* keeps its rows across a restart.
5055
*
5156
* # Reverse verification: direction predicted before it was run
5257
*
5358
* With `turso-driver.ts` restored to its `a7581b326` content, every refusal
5459
* case and every uppercase-classification case goes RED (the constructor
5560
* returns a `'local'` driver on `:memory:`, with no envelope to read, and the
5661
* `FILE:` rows do not survive the restart) and every preservation and scope
57-
* case stays GREEN. Measured; see the PR.
62+
* case stays GREEN, except the WIDENED cell's cases, which go RED the other
63+
* way: there the constructor refuses. Measured; see the PR.
5864
*/
5965

6066
import { afterAll, describe, expect, it } from 'vitest';
@@ -281,7 +287,7 @@ describe('SCOPE: a forced remote mode has no local engine, so this refusal does
281287
});
282288
});
283289

284-
describe('PRESERVATION: the recognised spellings construct exactly as before', () => {
290+
describe('PRESERVATION: the recognised spellings construct, and the one cell this change widens', () => {
285291
it.each<[string, TursoDriverConfig, string]>([
286292
['file: local', { url: 'file:./data/app.db' }, 'local'],
287293
[':memory: local (ephemeral by declaration)', { url: ':memory:' }, 'local'],
@@ -292,8 +298,40 @@ describe('PRESERVATION: the recognised spellings construct exactly as before', (
292298
['lowercase https:// remote', { url: `https://${HOST}`, authToken: 't' }, 'remote'],
293299
["libsql:// + mode 'remote'", { url: `libsql://${HOST}`, mode: 'remote' }, 'remote'],
294300
["file: + mode 'remote'", { url: 'file:./data/app.db', mode: 'remote' }, 'remote'],
301+
// WIDENED: refused at a7581b326, where a forced replica had to start with a
302+
// lowercase `file:`. An uppercase `FILE:` url is a `file:` url now.
303+
["WIDENED: FILE: + mode 'replica', no syncUrl", { url: 'FILE:./data/replica.db', mode: 'replica' }, 'replica'],
304+
["WIDENED: FILE: + mode 'replica' + syncUrl", { url: 'FILE:./data/replica.db', syncUrl: PRIMARY, mode: 'replica' }, 'replica'],
295305
])('%s', (_label, config, mode) => {
296306
// Knex opens its connection lazily, so constructing never touches the file.
297307
expect(new TursoDriver(config).transportMode).toBe(mode);
298308
});
309+
310+
it.each<[string, boolean]>([
311+
['no syncUrl', false],
312+
['with syncUrl', true],
313+
])("WIDENED: FILE: + mode 'replica', %s, runs on the named file and keeps its rows across a restart", async (_label, withSync) => {
314+
const url = upperFile(files.next());
315+
const stub = makeLibsqlSqliteStub();
316+
const make = () =>
317+
new TursoDriver({
318+
url,
319+
mode: 'replica',
320+
...(withSync ? { syncUrl: PRIMARY, client: stub as never, sync: { onConnect: false } } : {}),
321+
});
322+
323+
const first = make();
324+
expect(first.transportMode).toBe('replica');
325+
await first.connect();
326+
await first.initObjects([NOTE as never]);
327+
await first.create('note', { id: 'n1', title: 'kept' });
328+
await first.disconnect();
329+
330+
const second = make();
331+
await second.connect();
332+
await second.initObjects([NOTE as never]);
333+
expect((await second.find('note', {})).map((r: { title?: unknown }) => r.title)).toEqual(['kept']);
334+
await second.disconnect();
335+
stub.close();
336+
});
299337
});

0 commit comments

Comments
 (0)