Skip to content

Commit eff7af2

Browse files
committed
fix(cli): a *.wasm.db name selects the WASM engine for a file only, and the sqlite-wasm arm takes the same alias rule
A `.wasm.db` ending was tested ahead of every scheme check, so it rescued a scheme nothing recognises (redis://h/x.wasm.db, jdbc:x/x.wasm.db) into a SQLite file. It now applies to a file:/sqlite: URL or a scheme-less value only. The sqlite-wasm arm derives its file through normalizeDatabaseUrl as the sqlite arm does, so sqlite://x.wasm.db opens x.wasm.db. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FLwgbTSmGMrqsXkBDsiwfG
1 parent d343c71 commit eff7af2

3 files changed

Lines changed: 40 additions & 19 deletions

File tree

‎packages/cli/src/utils/driver-vocabulary-parity.test.ts‎

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,7 @@ describe('the OS_DATABASE_URL door: both hosts, one verdict per URL, dev and pro
449449
['https://my-db.turso.io', 'turso'],
450450
['wasm-sqlite://data.db', 'sqlite-wasm'],
451451
['./local.wasm.db', 'sqlite-wasm'],
452+
['file:./data/app.wasm.db', 'sqlite-wasm'],
452453
['file:./data/app.db', 'sqlite'],
453454
['sqlite:./data/app.db', 'sqlite'],
454455
[':memory:', 'sqlite'],
@@ -487,16 +488,18 @@ describe('the OS_DATABASE_URL door: both hosts, one verdict per URL, dev and pro
487488
});
488489

489490
// An OPAQUE scheme — two or more characters and a `:`, no `//`
490-
// (`jdbc:postgresql://…`, `redis:h`). The CLI host refuses it, typed, in dev
491-
// and prod. The runtime host's answer is deliberately NOT pinned here: today
492-
// its scheme-less arm opens these as a SQLite file at a path made of the URL,
493-
// an implementation gap against its own "unknown schemes throw" rule that is
494-
// #23765's to close, so asserting either agreement or its current answer
495-
// would pin that gap.
491+
// (`jdbc:postgresql://…`, `redis:h`) — and any scheme a `*.wasm.db` file name
492+
// used to rescue. The CLI host refuses them, typed, in dev and prod. The
493+
// runtime host's answer is deliberately NOT pinned here: today it opens these
494+
// as a SQLite file at a path made of the URL, an implementation gap against
495+
// its own "unknown schemes throw" rule that is #23765's to close, so asserting
496+
// either agreement or its current answer would pin that gap.
496497
it.each([
497498
['jdbc:postgresql://localhost:5432/db', 'jdbc:'],
498499
['redis:cache', 'redis:'],
499-
])('the CLI host REFUSES the opaque scheme of %s, in dev AND in prod', (url, scheme) => {
500+
['redis://h/app.wasm.db', 'redis://'],
501+
['jdbc:x/app.wasm.db', 'jdbc:'],
502+
])('the CLI host REFUSES %s (scheme %s), in dev AND in prod', (url, scheme) => {
500503
for (const isDev of [false, true]) {
501504
const cli = cliUrlVerdict(url, isDev);
502505
expect(cli.accepted, `the CLI host (isDev=${isDev}) accepted ${url}`).toBe(false);
@@ -513,17 +516,18 @@ describe('the OS_DATABASE_URL door: both hosts, one verdict per URL, dev and pro
513516
// hosts must name the same FILE: `sqlite://x.db` used to open `//x.db` (the
514517
// filesystem root) on the CLI host while the runtime host opened `x.db`.
515518
it.each([
516-
['sqlite://x.db', 'x.db'],
517-
['sqlite:x.db', 'x.db'],
518-
['sqlite:///var/lib/objectstack/x.db', '/var/lib/objectstack/x.db'],
519-
])('both hosts open %s as the sqlite file %s, in dev and prod', (url, file) => {
519+
['sqlite://x.db', 'x.db', 'sqlite'],
520+
['sqlite:x.db', 'x.db', 'sqlite'],
521+
['sqlite:///var/lib/objectstack/x.db', '/var/lib/objectstack/x.db', 'sqlite'],
522+
['sqlite://x.wasm.db', 'x.wasm.db', 'sqlite-wasm'],
523+
])('both hosts open %s as the file %s (%s), in dev and prod', (url, file, kind) => {
520524
process.env.OS_DATABASE_URL = url;
521525
const runtime = resolveStandaloneDatabase({ artifactPath: '/nonexistent/objectstack.json' });
522-
expect(runtime.driver).toBe('sqlite');
526+
expect(runtime.driver).toBe(kind);
523527
expect(runtime.sqliteFile, `the runtime host's file for ${url}`).toBe(file);
524528
for (const isDev of [false, true]) {
525529
const definition = resolveStorageDefinition(resolveDriverType(undefined, url), { databaseUrl: url, isDev });
526-
expect(definition!.driverId).toBe('sqlite');
530+
expect(definition!.driverId).toBe(kind);
527531
expect(definition!.config.filename, `the CLI host's file for ${url} (isDev=${isDev})`).toBe(runtime.sqliteFile);
528532
}
529533
});

‎packages/cli/src/utils/storage-driver.test.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ describe('inferDriverTypeFromUrl', () => {
5252
expect(inferDriverTypeFromUrl('https://x.turso.io')).toBe('turso');
5353
expect(inferDriverTypeFromUrl('wasm-sqlite://data.db')).toBe('sqlite-wasm');
5454
expect(inferDriverTypeFromUrl('./local.wasm.db')).toBe('sqlite-wasm');
55+
expect(inferDriverTypeFromUrl('file:./local.wasm.db')).toBe('sqlite-wasm');
56+
expect(inferDriverTypeFromUrl('sqlite:./local.wasm.db')).toBe('sqlite-wasm');
5557
expect(inferDriverTypeFromUrl('file:./app.db')).toBe('sqlite');
5658
expect(inferDriverTypeFromUrl('sqlite:./app.db')).toBe('sqlite');
5759
expect(inferDriverTypeFromUrl('./app.sqlite')).toBe('sqlite');
@@ -117,6 +119,9 @@ describe('inferDriverTypeFromUrl', () => {
117119
// too, rather than opened as a file at a path made of the URL.
118120
['jdbc:postgresql://localhost:5432/db', 'jdbc:'],
119121
['redis:cache', 'redis:'],
122+
// Nor does a `.wasm.db` name: it selects the WASM engine for a FILE only.
123+
['redis://h/app.wasm.db', 'redis://'],
124+
['jdbc:x/app.wasm.db', 'jdbc:'],
120125
])('refuses %s at inference, naming the scheme and the recognised ones', (url, scheme) => {
121126
let err: unknown;
122127
try {
@@ -278,6 +283,13 @@ describe('resolveStorageDefinition (#3826 — a definition, not a driver)', () =
278283
expect(r!.displayUrl).toBe(url);
279284
});
280285

286+
it('reads sqlite://x.wasm.db as the WASM engine on the file x.wasm.db, by the same alias rule', () => {
287+
const url = 'sqlite://x.wasm.db';
288+
const r = resolveStorageDefinition(resolveDriverType(undefined, url), { databaseUrl: url, isDev: false });
289+
expect(r!.driverId).toBe('sqlite-wasm');
290+
expect(r!.config.filename).toBe('x.wasm.db');
291+
});
292+
281293
it('dev default (no driver) declares sqlite :memory: with NO telemetry sibling', () => {
282294
const r = resolveStorageDefinition('', { isDev: true });
283295
expect(r!.driverId).toBe('sqlite');

‎packages/cli/src/utils/storage-driver.ts‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,12 @@ export function inferDriverTypeFromUrl(url: string | undefined): string {
317317
// MissingDriverPackageError).
318318
if (/^libsql:\/\//i.test(u)) return 'turso';
319319
if (/^https?:\/\//i.test(u) && /\.turso\./i.test(u)) return 'turso';
320-
if (/^wasm-sqlite:\/\//i.test(u) || /\.wasm\.db$/i.test(u)) return 'sqlite-wasm';
320+
if (/^wasm-sqlite:\/\//i.test(u)) return 'sqlite-wasm';
321+
// A `*.wasm.db` file name selects the WASM engine only for a FILE: a `file:` /
322+
// `sqlite:` URL or a value with no scheme (both returns below). Tested here,
323+
// ahead of every scheme check, it let the name rescue a scheme nothing
324+
// recognises (`redis://h/x.wasm.db`, `jdbc:x/x.wasm.db`) into a SQLite file.
325+
const wasmFile = /\.wasm\.db$/i.test(u);
321326
// The retired in-memory engine's two schemes — character-for-character the
322327
// runtime's `detectDriverFromUrl` arm, so the two hosts recognise and refuse
323328
// the same URLs. Kept ABOVE the sqlite test so it is not shadowed, and
@@ -332,7 +337,7 @@ export function inferDriverTypeFromUrl(url: string | undefined): string {
332337
{ recognized: false },
333338
);
334339
}
335-
if (/^file:/i.test(u) || /^sqlite:/i.test(u)) return 'sqlite';
340+
if (/^file:/i.test(u) || /^sqlite:/i.test(u)) return wasmFile ? 'sqlite-wasm' : 'sqlite';
336341
// What is left with a scheme is refused, naming the scheme and never the URL:
337342
// - an unrecognised `<scheme>://` (`redis://…`). Refusing it here, rather
338343
// than letting a name that ends `.db` reach SQLite, is what keeps
@@ -355,7 +360,7 @@ export function inferDriverTypeFromUrl(url: string | undefined): string {
355360
);
356361
}
357362
// No scheme at all: a SQLite file path — `:memory:`, `./app.db`, `data/app`.
358-
return 'sqlite';
363+
return wasmFile ? 'sqlite-wasm' : 'sqlite';
359364
}
360365

361366
/**
@@ -533,10 +538,10 @@ export function resolveStorageDefinition(
533538
}
534539

535540
if (kind === 'sqlite-wasm') {
536-
const filePath = (databaseUrl ?? ':memory:')
541+
// The same alias rule as the sqlite arm: `sqlite://x.wasm.db` is `x.wasm.db`.
542+
const filePath = normalizeDatabaseUrl(databaseUrl ?? ':memory:')
537543
.replace(/^file:/, '')
538-
.replace(/^wasm-sqlite:\/\//, '')
539-
.replace(/^sqlite:/, '');
544+
.replace(/^wasm-sqlite:\/\//, '');
540545
return {
541546
driverId: 'sqlite-wasm',
542547
// `persist` passthrough: the CLI kept `on-disconnect` semantics here

0 commit comments

Comments
 (0)