Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .changeset/23898-sqlite-refusal-remedy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
'@objectstack/runtime': patch
---

fix(runtime): the SQLite "does not look like a file path" refusal names what the operator can change, instead of telling an operator who set `OS_DATABASE_DRIVER` to set it explicitly

Clause-②: no

`sqliteFilenameFromUrl` raises this refusal, and both hosts print it: the runtime's standalone host with its `[StandaloneStack]` tag, and the CLI's legacy `os serve` path, untagged, as the fatal `UnsupportedDriverError`. The first clause is unchanged and only the advice after it changes. The same URLs are refused, with the same error class and the same exit, and the message still names the URL's scheme and never the URL. Nothing that resolved before resolves differently.

- **A URL with a scheme.** This is how an explicit `sqlite` or `sqlite-wasm` selection (`OS_DATABASE_DRIVER`, `--database-driver`, or the `databaseDriver` option) meets a server URL, an opaque scheme such as `jdbc:`, or a `<scheme>://` nothing recognises.
- FROM: `… (its scheme is "jdbc:"). Use file:/path/to/db.sqlite, or set OS_DATABASE_DRIVER explicitly.` The operator who reads this has already set the driver explicitly: that selection is what led here.
- TO: `… (its scheme is "jdbc:"). Use file:/path/to/db.sqlite to keep the sqlite driver, or use a driver that matches the URL: unset the driver selection (OS_DATABASE_DRIVER, --database-driver or the databaseDriver option) so the URL selects it, or set the selection to the URL's driver.` With the selection unset, a scheme no driver claims, such as `jdbc:`, gets the unsupported-scheme refusal, which lists the supported schemes.
- **A `file:` that names no file** (`OS_DATABASE_URL=file:`, whether the URL or an explicit `sqlite` selection chose the driver).
- FROM: `… (it names no file after "file:"). Use file:/path/to/db.sqlite, or set OS_DATABASE_DRIVER explicitly.`
- TO: `… (it names no file after "file:"). Name the file after "file:", as in file:/path/to/db.sqlite.` A `file:` URL is a SQLite URL whichever way the driver was chosen, so changing the driver never fixed it.
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@
// 5. The boundary: the declared-datasource and `TURSO_DATABASE_URL` rungs hand
// the rule their URL without `normalizeDatabaseUrl`, so there a `sqlite:`
// alias is refused as a scheme rather than opened as a file of that name.
// 6. The remedy after the first clause names what the operator can change.
// Over a scheme: a `file:` path for the selected kind, or a driver that
// matches the URL, by unsetting the selection or setting it to the URL's
// driver, naming every door a selection comes through. It used to tell an
// operator who had set `OS_DATABASE_DRIVER` to "set OS_DATABASE_DRIVER
// explicitly". Over a `file:` that names no file: the file, and no driver
// change, whether the URL or a selection chose the kind.

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
Expand Down Expand Up @@ -242,6 +249,71 @@ describe('the one file name that changes answer: a first segment holding a colon
});
});

/**
* The advice after the refusal's first clause. Fails the case when the message
* does not open with that clause for `kind` and `why`, so a remedy read here is
* never the remainder of some other sentence.
*/
function remedyOf(err: Error, kind: SqliteKind, why: string): string {
const firstClause = `[StandaloneStack] ${kind} driver was selected but the URL does not look like a file path (${why}). `;
expect(err.message.startsWith(firstClause), `the refusal does not open with: ${firstClause}`).toBe(true);
return err.message.slice(firstClause.length);
}

describe('the remedy names what the operator can change', () => {
// Over a scheme the selected kind cannot open. The selection reaches this
// refusal through OS_DATABASE_DRIVER (which `--database-driver` sets) or the
// databaseDriver option, so the remedy names all three and assumes none.
it.each(KINDS.flatMap((kind) => Object.keys(DOORS).flatMap((door) => [
[kind, door, 'jdbc:postgresql://127.0.0.1:5999/db', 'jdbc:'],
[kind, door, `postgres://${USER}:${SECRET}@127.0.0.1:5999/db`, 'postgres://'],
] as const)))('%s via %s over %s: a file: path for the kind, or a driver that matches the URL', (kind, door, url, scheme) => {
const err = refusalOf(() => DOORS[door as keyof typeof DOORS](kind, url));
const remedy = remedyOf(err, kind, `its scheme is "${scheme}"`);
expect(remedy).toContain(`file:/path/to/db.sqlite to keep the ${kind} driver`);
// Both moves that hand the URL a driver it matches: unset, or set to it.
expect(remedy).toContain('use a driver that matches the URL');
expect(remedy).toContain('unset the driver selection');
expect(remedy).toContain('so the URL selects it');
expect(remedy).toContain("set the selection to the URL's driver");
for (const selection of ['OS_DATABASE_DRIVER', '--database-driver', 'databaseDriver']) {
expect(remedy, `the remedy does not name ${selection}`).toContain(selection);
}
expect(remedy).not.toContain('explicitly');
});

it.each(KINDS)('the exported rule gives %s the same remedy the doors give', (kind) => {
const viaRule = refusalOf(() => sqliteFilenameFromUrl('redis:h', kind)).message;
const viaDoor = refusalOf(() => DOORS['OS_DATABASE_DRIVER and OS_DATABASE_URL'](kind, 'redis:h')).message;
expect(viaRule).toBe(viaDoor);
expect(remedyOf(new Error(viaRule), kind, 'its scheme is "redis:"')).toContain('unset the driver selection');
});

// A `file:` that names no file. A `file:` URL is a SQLite URL whichever way
// the kind was chosen, so the remedy is the file and never a driver change:
// unsetting the selection would hand the URL straight back to the `sqlite`
// kind. Only the `sqlite` kind meets this arm; on `sqlite-wasm` an empty name
// is `:memory:` (CONTROL above).
const NO_FILE = {
'the databaseUrl option, no selection': (url: string) =>
resolveStandaloneDatabase({ databaseUrl: url, artifactPath: join(root, 'none.json') }),
'OS_DATABASE_URL, no selection': (url: string) => {
process.env.OS_DATABASE_URL = url;
return resolveStandaloneDatabase({ artifactPath: join(root, 'none.json') });
},
...Object.fromEntries(Object.entries(DOORS).map(([door, run]) => [`an explicit sqlite selection, ${door}`, (url: string) => run('sqlite', url)])),
} as Record<string, (url: string) => unknown>;
it.each(Object.keys(NO_FILE).flatMap((door) => [[door, 'file:'], [door, 'FILE:']] as const))(
'via %s, %s names the file to write and no driver change',
(door, url) => {
const remedy = remedyOf(refusalOf(() => NO_FILE[door](url)), 'sqlite', 'it names no file after "file:"');
expect(remedy).toContain('file:/path/to/db.sqlite');
expect(remedy).toContain('Name the file after "file:"');
expect(remedy).not.toContain('driver');
},
);
});

describe('the boundary: rungs that do not normalize the URL', () => {
// `resolveProjectDatabaseUrl` applies `normalizeDatabaseUrl` (`sqlite:` and
// `sqlite://` become `file:`) on the explicit and OS_DATABASE_URL rungs only.
Expand Down
14 changes: 13 additions & 1 deletion packages/runtime/src/standalone-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -674,9 +674,21 @@ export function sqliteFilenameFromUrl(dbUrl: string, kind: 'sqlite' | 'sqlite-wa
// (`OS_DATABASE_DRIVER=sqlite` beside a `postgres://user:password@…`),
// so the URL it would otherwise echo is exactly the credentialed kind.
const why = scheme ? `its scheme is "${scheme}"` : 'it names no file after "file:"';
// The remedy names what the reader can change, and assumes no door:
// the kind may have come from OS_DATABASE_DRIVER (which
// `--database-driver` sets), the `databaseDriver` option, or the URL
// itself (`file:` alone). Over a scheme, keep the kind with a file
// path, or hand the URL a driver it matches. Over a `file:` that names
// no file, only the file helps: a `file:` URL selects the `sqlite`
// kind on its own, so no driver change gets past this rule.
const remedy = scheme
? `Use file:/path/to/db.sqlite to keep the ${kind} driver, or use a driver that matches the URL: ` +
`unset the driver selection (OS_DATABASE_DRIVER, --database-driver or the databaseDriver option) ` +
`so the URL selects it, or set the selection to the URL's driver.`
: 'Name the file after "file:", as in file:/path/to/db.sqlite.';
throw new Error(
`[StandaloneStack] ${kind} driver was selected but the URL does not look like a file path (${why}). ` +
`Use file:/path/to/db.sqlite, or set OS_DATABASE_DRIVER explicitly.`
remedy
);
}
return filename;
Expand Down
Loading