Skip to content

Commit 76b8bdc

Browse files
committed
fix(cli): refuse an opaque OS_DATABASE_URL scheme, and translate sqlite:/sqlite:// with the runtime host's normalizeDatabaseUrl
A value whose scheme is two or more characters and a colon with no `//` (jdbc:postgresql://..., redis:h) is refused with the same UnsupportedDriverError and sentence, naming the scheme only; a one-letter drive (C:\data\app) stays a path. The sqlite arm now derives its file through normalizeDatabaseUrl, so sqlite://x.db opens x.db, not //x.db. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FLwgbTSmGMrqsXkBDsiwfG
1 parent 683bf95 commit 76b8bdc

1 file changed

Lines changed: 34 additions & 19 deletions

File tree

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

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ import {
104104
devAutoMigrateConfig,
105105
loadTursoDriverFactory as loadRuntimeTursoDriverFactory,
106106
namesRetiredMemoryEngine,
107+
normalizeDatabaseUrl,
107108
retiredMemoryEngineMessage,
108109
type LoadTursoDriverFactoryOptions,
109110
} from '@objectstack/runtime';
@@ -291,9 +292,11 @@ function unsupportedUrlSchemeMessage(scheme: string): string {
291292
*
292293
* The arms are the runtime host's (`detectDriverFromUrl` in
293294
* `packages/runtime/src/standalone-stack.ts`), so the two hosts give one answer
294-
* per URL: `pg://` is postgres, and a value with no `<scheme>://` is a SQLite
295-
* file path, whatever its extension. The runtime refuses every other
296-
* `<scheme>://`, so refusing it here takes away nothing that host accepts.
295+
* per URL: `pg://` is postgres, and a value with no scheme is a SQLite file
296+
* path, whatever its extension. The runtime refuses every other
297+
* `<scheme>://`, so refusing it here takes away nothing that host means to
298+
* accept. The one difference is an OPAQUE scheme (`jdbc:…`, `redis:h`), which
299+
* this host refuses and the runtime host still opens as a file (#23765).
297300
*/
298301
export function inferDriverTypeFromUrl(url: string | undefined): string {
299302
if (!url) return '';
@@ -330,20 +333,29 @@ export function inferDriverTypeFromUrl(url: string | undefined): string {
330333
);
331334
}
332335
if (/^file:/i.test(u) || /^sqlite:/i.test(u)) return 'sqlite';
333-
// No `<scheme>://` at all: a SQLite file path — `:memory:`, `./app.db`,
334-
// `data/app`. The runtime host's arm, character for character. It is what
335-
// keeps a `.db` name from rescuing an unrecognised scheme: `redis://h/x.db`
336-
// is refused below, as the runtime refuses it, rather than handed to SQLite
337-
// as a file path.
338-
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(u)) return 'sqlite';
339-
const scheme = `${/^[a-z][a-z0-9+.-]*/i.exec(u)![0]}://`;
340-
throw new UnsupportedDriverError(
341-
scheme,
342-
unsupportedUrlSchemeMessage(scheme),
343-
// Not a kind this resolver can produce — `database-driver-allowlist.pin`
344-
// reads `recognized` to tell the two apart.
345-
{ recognized: false },
346-
);
336+
// What is left with a scheme is refused, naming the scheme and never the URL:
337+
// - an unrecognised `<scheme>://` (`redis://…`). Refusing it here, rather
338+
// than letting a name that ends `.db` reach SQLite, is what keeps
339+
// `redis://h/x.db` refused, as the runtime host refuses it;
340+
// - an OPAQUE scheme, two or more characters and a `:` with no `//`
341+
// (`jdbc:postgresql://…`, `redis:h`). The runtime host's scheme-less arm
342+
// opens these as a SQLite file at a path made of the URL. That is an
343+
// implementation gap on that host (#23765), against its own rule that an
344+
// unknown scheme never falls back to SQLite, so it is not copied here.
345+
// A single letter and a `:` is a drive (`C:\data\app`, `C:/data/app`), not
346+
// a scheme, and stays a path.
347+
const scheme = /^[a-z][a-z0-9+.-]*:\/\//i.exec(u)?.[0] ?? /^[a-z][a-z0-9+.-]+:/i.exec(u)?.[0];
348+
if (scheme) {
349+
throw new UnsupportedDriverError(
350+
scheme,
351+
unsupportedUrlSchemeMessage(scheme),
352+
// Not a kind this resolver can produce — `database-driver-allowlist.pin`
353+
// reads `recognized` to tell the two apart.
354+
{ recognized: false },
355+
);
356+
}
357+
// No scheme at all: a SQLite file path — `:memory:`, `./app.db`, `data/app`.
358+
return 'sqlite';
347359
}
348360

349361
/**
@@ -500,9 +512,12 @@ export function resolveStorageDefinition(
500512
}
501513

502514
if (kind === 'sqlite') {
503-
const filePath = (databaseUrl ?? ':memory:')
515+
// `sqlite:` and `sqlite://` are aliases of `file:`, translated by the runtime
516+
// host's own rule (`normalizeDatabaseUrl`) rather than a second copy of it,
517+
// so both hosts open one file: `sqlite://x.db` is `x.db`, never `//x.db`
518+
// (the filesystem root), and `sqlite:///abs/x.db` is `/abs/x.db`.
519+
const filePath = normalizeDatabaseUrl(databaseUrl ?? ':memory:')
504520
.replace(/^file:/, '')
505-
.replace(/^sqlite:/, '')
506521
.replace(/^sql:\/\//, '');
507522
return {
508523
driverId: 'sqlite',

0 commit comments

Comments
 (0)