Skip to content

Commit 84cf695

Browse files
committed
fix(runtime): an explicit sqlite / sqlite-wasm selection refuses a URL that is not a file path
`sqliteFilenameFromUrl` is the only check an explicit selection's URL meets, and it refused only a `SCHEME://` URL on the `sqlite` kind. Both kinds now refuse an opaque scheme (`jdbc:postgresql://…`, `redis:h`) on the value as given, and the `sqlite-wasm` kind refuses a `SCHEME://` URL too. A value whose `file:` (or `wasm-sqlite:`) prefix the rule strips stays a file, so `file:backup:2026.db` still names `backup:2026.db`. One sentence for both kinds, naming the kind and the scheme only. The opaque-scheme expression `detectDriverFromUrl` gained in the merged opaque-scheme fix is now one module-private helper, `schemeOf`, that both functions call. Claude-Session: https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT Co-authored-by: Claude <noreply@anthropic.com>
1 parent d57f615 commit 84cf695

2 files changed

Lines changed: 64 additions & 18 deletions

File tree

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -644,11 +644,14 @@ export function resolveStorageDefinition(
644644
* the runtime host's own sentence, which names the scheme and never the URL,
645645
* minus its `[StandaloneStack]` tag — the shape this host's retired-engine
646646
* refusal already has (`driver-vocabulary-parity.test.ts` holds both).
647-
* Two inputs reach that: an explicit `sqlite` selection over a server URL
648-
* (`OS_DATABASE_DRIVER=sqlite` beside `postgres://user:password@…`), and a
649-
* `file:` that names no file. Both used to build a definition: the first opened
650-
* a file at a path made of the URL, credentials included, and the second
651-
* declared an empty file name. The type is
647+
* What reaches that is an explicit `sqlite` or `sqlite-wasm` selection over a
648+
* value that is not a file path, which this host's URL inference never sees —
649+
* a server URL (`OS_DATABASE_DRIVER=sqlite` beside
650+
* `postgres://user:password@…`), an unrecognised `<scheme>://`, or an opaque
651+
* scheme (`jdbc:postgresql://…`, `redis:h`) — and, on the `sqlite` kind, a
652+
* `file:` that names no file. Each used to build a definition: a non-file value
653+
* opened a file at a path made of the URL, credentials included, and the empty
654+
* `file:` declared an empty file name. The type is
652655
* what makes the refusal fatal: `serve.ts` re-throws this class and swallows
653656
* every other construction error, so a plain `Error` would boot with no
654657
* storage at all.

‎packages/runtime/src/standalone-stack.ts‎

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,25 @@ function urlSchemeOf(dbUrl: string): string | undefined {
427427
return /^[a-z][a-z0-9+.-]*:\/\//i.exec(dbUrl)?.[0];
428428
}
429429

430+
/**
431+
* The scheme a database URL opens with — its `<scheme>://` ({@link urlSchemeOf})
432+
* or an OPAQUE scheme, two or more characters and a `:` with no `//`
433+
* (`jdbc:`, `redis:`) — or `undefined` when it has neither and so is a path.
434+
*
435+
* A single letter and a `:` is a Windows drive (`C:\data\app.db`,
436+
* `C:/data/app.db`), never a scheme. An opaque scheme ends at its FIRST `:`, so
437+
* like a `<scheme>://` it cannot hold a userinfo, a host or a path, and a
438+
* refusal may name it. ONE answer for the two places this host asks whether a
439+
* value is a path: the URL classification ({@link detectDriverFromUrl}) and the
440+
* SQLite filename rule an explicit selection meets instead
441+
* ({@link sqliteFilenameFromUrl}). The CLI host draws the same line
442+
* (`inferDriverTypeFromUrl` in `utils/storage-driver.ts`), and
443+
* `driver-vocabulary-parity.test.ts` holds the hosts equal.
444+
*/
445+
function schemeOf(dbUrl: string): string | undefined {
446+
return urlSchemeOf(dbUrl) ?? /^[a-z][a-z0-9+.-]+:/i.exec(dbUrl)?.[0];
447+
}
448+
430449
function detectDriverFromUrl(dbUrl: string): ResolvedDriverKind {
431450
// The in-memory (mingo) engine's two schemes — character-for-character the
432451
// CLI's `inferDriverTypeFromUrl` regex, for the reason the mysql and turso
@@ -473,7 +492,7 @@ function detectDriverFromUrl(dbUrl: string): ResolvedDriverKind {
473492
// `C:/data/app.db`), not a scheme, and stays a path. The CLI host draws the
474493
// same line (`inferDriverTypeFromUrl` in `utils/storage-driver.ts`), and
475494
// `driver-vocabulary-parity.test.ts` holds the two equal.
476-
const scheme = urlSchemeOf(dbUrl) ?? /^[a-z][a-z0-9+.-]+:/i.exec(dbUrl)?.[0];
495+
const scheme = schemeOf(dbUrl);
477496
// Bare path without a scheme — treat as a sqlite file path.
478497
if (!scheme) return wasmFile ? 'sqlite-wasm' : 'sqlite';
479498
// Names the scheme, never the URL ({@link urlSchemeOf}). An opaque scheme
@@ -603,8 +622,32 @@ function refuseRetiredMemoryEngine(
603622
* URL→filename for the two sqlite kinds: the ONE rule both hosts turn a SQLite
604623
* database URL into a file with. Throws on a URL that isn't a path.
605624
*
606-
* Takes the URL as {@link resolveStandaloneDatabase} has it, i.e. after
607-
* `normalizeDatabaseUrl` (`sqlite:` / `sqlite://` are already `file:`).
625+
* Takes the URL as {@link resolveStandaloneDatabase} has it. The explicit and
626+
* `OS_DATABASE_URL` rungs have applied `normalizeDatabaseUrl` (`sqlite:` /
627+
* `sqlite://` are `file:` there); a declared datasource's `url` and
628+
* `TURSO_DATABASE_URL` arrive as written, so on those two rungs a `sqlite:`
629+
* alias is a scheme here and is refused.
630+
*
631+
* An explicit `sqlite` / `sqlite-wasm` selection skips
632+
* {@link detectDriverFromUrl}, so this rule is the only check its URL meets, and
633+
* on BOTH kinds it refuses every value that is not a file path:
634+
* - a `<scheme>://` left after the strip below: a server URL
635+
* (`postgres://user:password@…`) or a scheme nothing recognises
636+
* (`redis://h/x.db`);
637+
* - an OPAQUE scheme ({@link schemeOf}: two or more characters and a `:` with
638+
* no `//`, as in `jdbc:postgresql://…` or `redis:h`), judged on the value as
639+
* given. A value whose `file:` (or, on the WASM kind, `wasm-sqlite:`) prefix
640+
* the strip removed is a file, colons and all: `file:backup:2026.db` names
641+
* `backup:2026.db`, while `backup:2026.db` is the scheme `backup:`.
642+
* Both used to pass through as a file name, and the boot made directories out of
643+
* the URL: `./jdbc:postgresql:/127.0.0.1:5999/`, and on the WASM kind, which
644+
* refused nothing, `./postgres:/USER:SECRET@127.0.0.1:5999/`, the credentials
645+
* in a directory name. The refusal comes before any directory is made, and it
646+
* names the selected kind and the scheme, never the URL.
647+
*
648+
* An empty name is `:memory:` on the WASM kind; on the `sqlite` kind a `file:`
649+
* that names no file is refused.
650+
*
608651
* `file:` and `file://` are stripped case-insensitively on BOTH kinds, as
609652
* {@link detectDriverFromUrl} classifies them (`/^file:/i`): a prefix the
610653
* classifier accepts and this strip kept became part of the filename, so
@@ -618,21 +661,21 @@ function refuseRetiredMemoryEngine(
618661
* `//NAME` at the filesystem root there while this host opened `NAME`.
619662
*/
620663
export function sqliteFilenameFromUrl(dbUrl: string, kind: 'sqlite' | 'sqlite-wasm'): string {
621-
if (kind === 'sqlite-wasm') {
622-
return dbUrl
623-
.replace(/^wasm-sqlite:(\/\/)?/i, '')
624-
.replace(/^file:(\/\/)?/i, '') || ':memory:';
625-
}
626-
const filename = dbUrl.replace(/^file:(\/\/)?/i, '');
627-
const scheme = urlSchemeOf(filename);
664+
const filename = kind === 'sqlite-wasm'
665+
? dbUrl.replace(/^wasm-sqlite:(\/\/)?/i, '').replace(/^file:(\/\/)?/i, '')
666+
: dbUrl.replace(/^file:(\/\/)?/i, '');
667+
if (!filename && kind === 'sqlite-wasm') return ':memory:';
668+
// A prefix the strip removed made the value a file, so only a value left as
669+
// given is asked for an opaque scheme (`file:` is one itself).
670+
const scheme = filename === dbUrl ? schemeOf(dbUrl) : urlSchemeOf(filename);
628671
if (!filename || scheme) {
629-
// Names the scheme, never the URL ({@link urlSchemeOf}). This is the
630-
// refusal an explicit `sqlite` selection meets over a server URL
672+
// Names the scheme, never the URL ({@link schemeOf}). This is the
673+
// refusal an explicit selection meets over a server URL
631674
// (`OS_DATABASE_DRIVER=sqlite` beside a `postgres://user:password@…`),
632675
// so the URL it would otherwise echo is exactly the credentialed kind.
633676
const why = scheme ? `its scheme is "${scheme}"` : 'it names no file after "file:"';
634677
throw new Error(
635-
`[StandaloneStack] sqlite driver was selected but the URL does not look like a file path (${why}). ` +
678+
`[StandaloneStack] ${kind} driver was selected but the URL does not look like a file path (${why}). ` +
636679
`Use file:/path/to/db.sqlite, or set OS_DATABASE_DRIVER explicitly.`
637680
);
638681
}

0 commit comments

Comments
 (0)