Repository navigation
Commit 0cb45e9
fix(runtime): an explicit sqlite / sqlite-wasm selection refuses a URL that is not a file path, on both hosts (#23896)
Fixes #23863
Clause-②: no
Under an explicit `sqlite` or `sqlite-wasm` driver selection, the one
SQLite file-name rule both hosts call (`sqliteFilenameFromUrl` in
`@objectstack/runtime`) now refuses every value that is not a file path.
It refuses an opaque scheme (`jdbc:postgresql://…`, `redis:h`) on both
kinds, and on the `sqlite-wasm` kind it also refuses a `SCHEME://` URL
(`postgres://…`, `redis://h/x.db`), as the `sqlite` kind already did.
The refusal names the selected kind and the scheme only, never the URL,
and the boot stops before it creates anything on disk.
## Why
An explicit selection skips the URL classification
(`resolveStandaloneDatabase`: `explicitDriver ||
detectDriverFromUrl(url)`), so the opaque-scheme refusal that PR #23861
added to that classification for #23765 (merged) never sees it. The
file-name rule was the only check left, and it recognised only
`SCHEME://`, and only for the `sqlite` kind. Measured at the public door
on base `34b740f2d1` (each boot in a fresh directory outside the repo,
`OS_HOME` pinned inside it):
| boot (`os serve --dev`, plain config) | base `34b740f2d1` | head
`c80f0ceaa` |
|---|---|---|
| `OS_DATABASE_DRIVER=sqlite
OS_DATABASE_URL=jdbc:postgresql://127.0.0.1:5999/db` | `✓ Server is
ready`, `Driver: SqlDriver(better-sqlite3) →
jdbc:postgresql://127.0.0.1:5999/db`, created
`./jdbc:postgresql:/127.0.0.1:5999/db` plus its `.telemetry.db`, `-shm`
and `-wal` siblings | exit 1, `✗ [StandaloneStack] sqlite driver was
selected but the URL does not look like a file path (its scheme is
"jdbc:"). Use file:/path/to/db.sqlite, or set OS_DATABASE_DRIVER
explicitly.`, directory empty |
| `OS_DATABASE_DRIVER=sqlite-wasm
OS_DATABASE_URL=postgres://PLACEHOLDER-USER:PLACEHOLDER-SECRET@127.0.0.1:5999/db`
| `✓ Server is ready`, created
`./postgres:/PLACEHOLDER-USER:PLACEHOLDER-SECRET@127.0.0.1:5999/db` (the
credentials in a directory name) | exit 1, `✗ [StandaloneStack]
sqlite-wasm driver was selected but the URL does not look like a file
path (its scheme is "postgres://"). …`, directory empty, 0 placeholder
hits in the log |
| CLI legacy path, `OS_MODE=off OS_DATABASE_DRIVER=sqlite-wasm
OS_DATABASE_URL=jdbc:postgresql://127.0.0.1:5999/db` | not run on base |
exit 1, `✗ sqlite-wasm driver was selected but the URL does not look
like a file path (its scheme is "jdbc:"). …` (the typed
`UnsupportedDriverError`, untagged), directory empty |
## Changes
- `packages/runtime/src/standalone-stack.ts`
- `schemeOf` (new, module-private, no new export): the opaque-scheme
expression PR #23861 added inline to `detectDriverFromUrl`
(`urlSchemeOf(dbUrl) ?? /^[a-z][a-z0-9+.-]+:/i.exec(dbUrl)?.[0]`), now
one helper. `detectDriverFromUrl` calls it in place of the inline
expression; nothing else in that function changed.
- `sqliteFilenameFromUrl`: both kinds strip their prefixes as before,
then ask `schemeOf` of a value the strip left as given, and
`urlSchemeOf` of what a `file:` (or `wasm-sqlite:`) strip left. So
`file:backup:2026.db` still names `backup:2026.db`. One sentence for
both kinds, naming the kind; the `sqlite` kind's words are unchanged. An
empty name is still `:memory:` on the WASM kind.
- Its docblock now states the refusal, and it corrects the claim that
every caller passes a normalized URL (see Acceptance notes).
- `packages/cli/src/utils/storage-driver.ts`: `sqliteFileFor`'s docblock
only. Its "Two inputs reach that" sentence was no longer true. No CLI
code change: the CLI host already calls the runtime rule and re-raises
its refusal as `UnsupportedDriverError`.
-
`packages/runtime/src/standalone-stack-explicit-sqlite-non-file-refusal.test.ts`
(new, 80 cases): both kinds through both selection doors
(`databaseDriver` + `databaseUrl`, `OS_DATABASE_DRIVER` +
`OS_DATABASE_URL`) and through the exported rule itself. The refused
rows assert a plain `Error` naming the kind and the scheme, and that no
own property of the error carries `PLACEHOLDER-USER` /
`PLACEHOLDER-SECRET` / `PLACEHOLDER-TOKEN`, with a positive control that
the serialisation holds the message. The boot (`createStandaloneStack`)
is refused in an empty cwd and leaves it empty. CONTROL rows, the one
changed file name and the un-normalized rungs (Acceptance notes) are
pinned too.
- `packages/cli/src/utils/driver-vocabulary-parity.test.ts`:
explicit-driver rows under both kinds. Both hosts refuse the same values
in dev AND prod, with the CLI host's message equal to the runtime host's
minus its `[StandaloneStack] ` tag, and `driverType` equal to the kind.
Placeholder credentials are absent from every own property of both
errors. Every CONTROL value names the same file on both hosts. The rows
of the classification path (PR #23861) are untouched.
- `.changeset/23863-explicit-sqlite-non-file-refusal.md`:
`@objectstack/runtime` patch, `Clause-②: no`. It names the one file name
that changes answer under an explicit selection: `backup:2026.db` is now
refused, and `./backup:2026.db` and `file:backup:2026.db` still name it.
## Verification (head `c80f0ceaa`)
- Build: `pnpm turbo run build --filter=@objectstack/cli...
--concurrency=2` → 61/61 tasks, exit 0 (the CLI's dependency closure
plus the CLI itself).
- Both hosts through the built `dist`, under each kind, in dev and prod
(`resolveStorageDefinition` vs `resolveStandaloneDatabase`). Both refuse
`jdbc:postgresql://127.0.0.1:5999/db` (`jdbc:`), `redis:h` (`redis:`),
`redis://h/x.db` (`redis://`),
`postgres://PLACEHOLDER-USER:PLACEHOLDER-SECRET@…` (`postgres://`) and
`backup:2026.db` (`backup:`). Both open `file:./x.db`, `./x.db`,
`C:\data\x.db`, `C:/data/x.db`, `:memory:`, `file:backup:2026.db`,
`./backup:2026.db`, `sqlite:./x.db` and `sqlite:///abs/x.db` as the same
file. Under `sqlite-wasm`, both open `wasm-sqlite:///tmp/x.db` as
`/tmp/x.db`. On base, both hosts opened every refused value as a file
under `sqlite-wasm`, and every opaque one under `sqlite` too.
- `pnpm --filter @objectstack/runtime exec vitest run --project local
--maxWorkers=2` with the new file,
`standalone-stack-opaque-scheme-refusal.test.ts` and
`standalone-stack-url-refusal-credentials.test.ts` → 3 files, 157
passed, exit 0.
- `pnpm --filter @objectstack/cli exec vitest run --project unit
--maxWorkers=2` with `driver-vocabulary-parity.test.ts` and
`storage-driver.test.ts` → 2 files, 204 passed, exit 0.
- `pnpm --filter @objectstack/runtime exec vitest run --project local
--maxWorkers=2` (the runtime's whole local project) → 402 files, 6121
passed, 19 skipped, exit 0.
- `pnpm --filter @objectstack/runtime typecheck` → exit 0. `pnpm
--filter @objectstack/cli typecheck` → exit 0.
- `pnpm exec eslint --no-inline-config` over the four changed source and
test files → exit 0.
- Gate families: `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 65 commands. 63 exited 0. `pnpm
check:dual-build-cjs-loads` and `pnpm check:i18n-coverage` exited 3
(`PREREQUISITE NOT MET`: packages outside the CLI closure, such as
`connector-slack`, have no `dist/` in this worktree), so they are NOT
MEASURED locally and left to CI. `--ran` reconciliation: 65 derived, 63
run, 2 NOT-MEASURED, 0 UNRUN. The repo-wide lint run is CI's `Lint &
Repo Gates`.
### Ablation (each new refusal once; the CLI suite resolves
`@objectstack/runtime` through `dist/`, so every leg rebuilt it)
Each mutation went through `scripts/ablation-replace.mjs`: the anchor
hit once, the blob changed, and the restore was proven by blob == HEAD
with an empty `git diff HEAD`. Each leg rebuilt the runtime and ran
`scripts/ablation-dist-preflight.mjs`.
| leg | mutation | dist/ | new runtime file (80) | CLI parity (130) |
|---|---|---|---|---|
| A1, opaque scheme | `filename === dbUrl ? schemeOf(dbUrl) :
urlSchemeOf(filename)` → `urlSchemeOf(filename)` | marker absent (was 1)
| 25 red: every opaque row on both kinds and both doors, the
exported-rule rows, `backup:2026.db`, the `sqlite:` boundary rows, and
the `sqlite` boot over `jdbc:`. `SCHEME://` rows and CONTROL green | 8
red: the opaque rows on both kinds. The rest green |
| A1 restore | — | marker present, 1 | 80 green | 130 green |
| A2, WASM kind refuses | `if (!filename \|\| scheme)` → `if (!filename
\|\| (scheme && kind.length === 6))` | marker present, 1 | 20 red: every
`sqlite-wasm` refusal row (opaque and `SCHEME://`), the `sqlite-wasm`
boot, and the `sqlite-wasm` boundary rows. All `sqlite` rows and CONTROL
green | 6 red: the `sqlite-wasm` rows. The rest green |
| A2 restore | — | marker absent, 0 | 80 green | 130 green |
After both legs: file blob `f3f84355f733` == HEAD, `git status
--porcelain` empty.
## Acceptance notes
- **Mechanism assumption 1 (every caller passes a normalized URL) is
false for two rungs, and this PR does not normalize them.**
`resolveProjectDatabaseUrl` applies `normalizeDatabaseUrl` on the
explicit and `OS_DATABASE_URL` / `DATABASE_URL` rungs. A declared
default datasource's `url` (non-SQLite drivers only; a declared `sqlite`
datasource is always expressed as `file:`) and `TURSO_DATABASE_URL`
reach the rule as written. Under an explicit selection there,
`sqlite:./x.db` is now refused as the scheme `sqlite:`. On base it
opened a file named `sqlite:./x.db`, which meant a `sqlite:.` directory.
`sqlite:///…` was already refused there under `sqlite`, and is now
refused under `sqlite-wasm` too. The one-line alternative, calling
`normalizeDatabaseUrl` inside `sqliteFilenameFromUrl`, would have made
`sqlite:///abs/x.db` open on those rungs where it is refused today. That
widens the accept set, against this card's ruled `Clause-②: no`, so the
boundary is pinned instead (`the boundary: rungs that do not normalize
the URL`). The classification path answers these values the same way
since PR #23861, whose own Acceptance notes record the un-normalized
declared-datasource tier ("No in-repo producer writes it"). No producer
was found here either.
- **Mechanism assumptions 2 and 3 held.** The CLI host follows the
export in dev and prod (table above), and both hosts refuse before any
directory is made (door rows above; the boot pin runs
`createStandaloneStack` in an empty cwd).
- **The refusal's remedy clause, unchanged by ruling, reads oddly under
an explicit selection.** It says "or set OS_DATABASE_DRIVER explicitly"
to an operator who just did. Reported to the seat, not changed here.
- **`main` moved after the merge.** Six commits landed past `c5cf546a6`
(spec, plugin-audit, docs, CI header). None touches
`standalone-stack.ts`, `resolve-project-database.ts` or
`packages/cli/src/utils/`. PR CI runs on the merge ref.
---
_Generated by [Claude
Code](https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent dd11563 commit 0cb45e9
5 files changed
Lines changed: 447 additions & 22 deletions
File tree
- .changeset
- packages
- cli/src/utils
- runtime/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
558 | 558 | | |
559 | 559 | | |
560 | 560 | | |
561 | | - | |
562 | | - | |
563 | | - | |
564 | | - | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
565 | 566 | | |
566 | 567 | | |
567 | 568 | | |
| |||
588 | 589 | | |
589 | 590 | | |
590 | 591 | | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
| 604 | + | |
| 605 | + | |
| 606 | + | |
| 607 | + | |
| 608 | + | |
| 609 | + | |
| 610 | + | |
| 611 | + | |
| 612 | + | |
| 613 | + | |
| 614 | + | |
| 615 | + | |
| 616 | + | |
| 617 | + | |
| 618 | + | |
| 619 | + | |
| 620 | + | |
| 621 | + | |
| 622 | + | |
| 623 | + | |
| 624 | + | |
| 625 | + | |
| 626 | + | |
| 627 | + | |
| 628 | + | |
| 629 | + | |
| 630 | + | |
| 631 | + | |
| 632 | + | |
| 633 | + | |
| 634 | + | |
| 635 | + | |
| 636 | + | |
| 637 | + | |
| 638 | + | |
| 639 | + | |
| 640 | + | |
| 641 | + | |
| 642 | + | |
| 643 | + | |
| 644 | + | |
| 645 | + | |
| 646 | + | |
| 647 | + | |
| 648 | + | |
| 649 | + | |
| 650 | + | |
| 651 | + | |
| 652 | + | |
| 653 | + | |
| 654 | + | |
| 655 | + | |
| 656 | + | |
| 657 | + | |
| 658 | + | |
| 659 | + | |
| 660 | + | |
| 661 | + | |
| 662 | + | |
| 663 | + | |
| 664 | + | |
| 665 | + | |
| 666 | + | |
| 667 | + | |
| 668 | + | |
| 669 | + | |
| 670 | + | |
| 671 | + | |
| 672 | + | |
| 673 | + | |
| 674 | + | |
| 675 | + | |
| 676 | + | |
| 677 | + | |
| 678 | + | |
| 679 | + | |
| 680 | + | |
| 681 | + | |
| 682 | + | |
| 683 | + | |
| 684 | + | |
| 685 | + | |
| 686 | + | |
| 687 | + | |
591 | 688 | | |
592 | 689 | | |
593 | 690 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
644 | 644 | | |
645 | 645 | | |
646 | 646 | | |
647 | | - | |
648 | | - | |
649 | | - | |
650 | | - | |
651 | | - | |
| 647 | + | |
| 648 | + | |
| 649 | + | |
| 650 | + | |
| 651 | + | |
| 652 | + | |
| 653 | + | |
| 654 | + | |
652 | 655 | | |
653 | 656 | | |
654 | 657 | | |
| |||
0 commit comments