Skip to content

Commit e2798fa

Browse files
fix(spec,cli,runtime,service-datasource)!: one driver vocabulary — os start and os migrate stop disagreeing (#6345) (#6910)
* wip(spec): one driver vocabulary table; mongo→mongodb; turso contract * wip(cli,runtime): both hosts read the shared driver vocabulary; fork-2 refusals * wip(service-datasource): mongodb rename, turso arm, exhaustive factory dispatch * test(cli): cross-host driver vocabulary parity pin + 8-cell fork-2 matrix * test(cli): update the two pins fork 1/2 deliberately flip * test(spec): turso contract, mongo→mongodb migration proof, rename pins * chore(spec): regenerate api-surface, spec-changes, upgrade guide; liveness evidence * chore: changesets for the driver-vocabulary convergence * chore: register the mongo→mongodb conversion on the hosts changeset * test(cli): parity REFUSE cases run dev AND prod — reverse verification found the gap * test(service-datasource): rename-consequence pins for mongodb * chore(spec): turso schema strictness ledger row, TursoTransportMode alias, regenerated artifacts * chore(spec): regenerate spec-changes/upgrade-guide from the MERGED source * chore(spec): regenerate api-surface/docs/skill-refs from the MERGED source * fix(spec): three convention breaks in the new turso schema — docs-link ellipsis, ADR-0122 alias, colliding alias probes * fix(cli): keep the #6860 allowlist pin's oracle honest — an unrecognized spelling is not a driver kind #6345's CLI-side refusal of an explicitly-named unknown driver reused UnsupportedDriverError with the operator's RAW TOKEN in driverType. The #6860 pin uses resolveStorageDefinition as its oracle and reads driverType out of that error, so its deliberately over-broad candidate scan started reporting every lowercase literal in storage-driver.ts ('safe', 'on-disconnect', 'factory', 'string', ...) as a driver kind. The allowlist itself was already correct: #6860 landed the canonical seven (sqlite, sqlite-wasm, turso, postgres, mysql, mongodb, memory), mongodb included. start.ts and dev.ts are therefore untouched. UnsupportedDriverError now carries 'recognized', defaulting true so the pre-#6345 turso-with-no-URL call sites keep their meaning, and the pin returns null for the unrecognized case. The assertion is unchanged: both sides still derived, still required to be equal. Also regenerates spec-changes.json / protocol-upgrade-guide.md, which the merge brought stale (os-regen guard). --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3528878 commit e2798fa

41 files changed

Lines changed: 1857 additions & 136 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
---
2+
"@objectstack/cli": major
3+
"@objectstack/runtime": major
4+
---
5+
6+
fix(cli,runtime)!: `os start` and `os migrate` finally read the same driver vocabulary (#6345)
7+
8+
One environment variable had two answers. Measured on `main` by driving the real
9+
entry points — `resolveDriverType` + `resolveStorageDefinition` for the `os start`
10+
side, `resolveStandaloneDatabase` for the `os migrate` side — **10 of 21
11+
spellings disagreed**:
12+
13+
```
14+
OS_DATABASE_DRIVER=pg OS_DATABASE_URL=postgres://… os start → boots
15+
OS_DATABASE_DRIVER=pg OS_DATABASE_URL=postgres://… os migrate plan → refused by name
16+
```
17+
18+
`sql`, `wasm`, `wasm-sqlite`, `postgresql`, `pg`, `mysql2`, `mongo`, `mingo`,
19+
`in-memory` and `libsql` were accepted by the CLI and refused by the standalone
20+
stack. Both sides were separately correct and separately pinned; the missing test
21+
was the CROSS-host one, and it now exists
22+
(`packages/cli/src/utils/driver-vocabulary-parity.test.ts` — the only place that
23+
can import both).
24+
25+
**Both hosts now resolve through `@objectstack/spec`'s one driver table.** The
26+
CLI's hand-written `driverType === 'pg' || driverType === 'postgresql'` chains
27+
and the standalone stack's canonical-only `z.enum` are both gone; a driver added
28+
to the spec table appears on both hosts at once, which is the only shape in which
29+
this fork cannot re-open. The standalone `databaseDriver` CONFIG key accepts the
30+
same aliases as `OS_DATABASE_DRIVER`, so the fork cannot relocate to inside one
31+
host either.
32+
33+
**BREAKING ① — selecting a driver whose database lives elsewhere, without saying
34+
where, now refuses.** Four kinds have no local default (`postgres`, `mysql`,
35+
`mongodb`, `turso`), and before this change each side guessed, differently:
36+
37+
| selection, no URL | `os start` before | `os migrate` before | now, both |
38+
| :-- | :-- | :-- | :-- |
39+
| `postgres` | `config.url === undefined``pg` connects to ITS localhost:5432 | `file:<state>/data/objectstack.db` | typed refusal |
40+
| `mysql` | `config.url === undefined` | `file:…objectstack.db` | typed refusal |
41+
| `mongodb` | invented `mongodb://localhost:27017/objectstack` | `file:…objectstack.db` | typed refusal |
42+
| `turso` | typed refusal (#5602) | `file:…objectstack.db` | typed refusal |
43+
44+
Eight cells, seven of them wrong in one of two ways: connect the operator to a
45+
database they never named, or hand a server driver a `file:` DSN and let it fail
46+
two layers from the cause. `turso` already said the right sentence; this
47+
generalizes it rather than leaving one kind honest and three guessing. Only the
48+
FALLBACK rungs are refused — a URL from `--database`, `OS_DATABASE_URL`,
49+
`DATABASE_URL`, `TURSO_DATABASE_URL` or the project's declared default datasource
50+
is a statement about where the database is, and is honoured as before, `file:`
51+
DSN included.
52+
53+
**BREAKING ② — an explicitly-named unknown driver refuses on the CLI side too.**
54+
`os dev --database-driver sqlite3` used to fall through to the dev SQLite default
55+
and boot in silence, while `os migrate` refused the same value by name (#6344
56+
killed the silent fallback on that side only). `''` (nobody chose) keeps its old
57+
answer — dev default, `null` in production; a non-empty value can only have come
58+
from an operator, since URL inference yields a canonical id or `''`. The refusal
59+
enumerates the spellings that actually work, from the shared table.
60+
61+
**Widened, not narrowed:** every spelling either host accepted before is accepted
62+
by both now. `sqlite3` / `better-sqlite3` / `mariadb` / `inmemory` stay out of the
63+
selection face on both — neither host ever accepted them as a boot selection, and
64+
converging two hosts is not a licence to widen the flag. They keep resolving a
65+
config CONTRACT, so a stored `driver: 'sqlite3'` datasource is unaffected.
66+
67+
**Why `major` on both.** ① and ② each turn a boot that started into a boot that
68+
refuses. A deployment that really did run postgres on localhost with trust auth,
69+
or that relied on `mongodb://localhost:27017/objectstack`, was working by
70+
accident and now gets a message telling it what to set — but it was working, and
71+
calling that a `patch` because the old behaviour was a bug would let the change
72+
arrive unannounced in a changelog. The alias widening on its own would be
73+
`minor`; the refusals are what price this at `major`.
74+
75+
**Migration.** The stored half of this change is the `mongo``mongodb`
76+
canonical-id rename, which both hosts now resolve through the shared table; it is
77+
registered as the ADR-0087 D2 conversion `datasource-driver-mongo-to-mongodb`
78+
and needs no action from anyone — `migrate meta` converges the rows and `mongo`
79+
stays accepted meanwhile. The two refusals have no stored form and no codemod:
80+
they prescribe an operator action (set the database URL, or fix the driver
81+
value) whose correct answer is a fact only the operator has, which is why the
82+
messages name the variable, show the target shape, and say what booting anyway
83+
would have cost.
84+
85+
<!-- adr-0087: registered datasource-driver-mongo-to-mongodb -->
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
---
2+
"@objectstack/service-datasource": major
3+
---
4+
5+
feat(service-datasource)!: `DRIVER_CATALOG` publishes `mongodb`, and the factory can no longer fall through to `memory` (#6345)
6+
7+
**BREAKING — `DRIVER_CATALOG`'s MongoDB entry publishes `id: 'mongodb'`.** That
8+
field is documented as "used as `datasource.driver`" and it is literally what the
9+
Studio connection form writes into a datasource row, so this is the face of
10+
#6345's `mongo``mongodb` rename that reaches stored data. Rows written before
11+
the rename carry `mongo`; the ADR-0087 D2 conversion
12+
`datasource-driver-mongo-to-mongodb` converges them at every rehydration seam,
13+
and `mongo` remains an accepted alias so a deployment that skipped the migration
14+
still connects. The factory's dispatch arm renames with it (`kind === 'mongodb'`).
15+
16+
**A `turso` construction arm — which the rename made mandatory, not optional.**
17+
`createDefaultDatasourceDriverFactory().supports()` is
18+
`resolveDriverId(id) !== undefined`, so the moment `turso` gained a config
19+
contract in `@objectstack/spec` this factory began claiming it. Before this arm,
20+
that claim was answered by `create()`'s trailing `memory` fall-through: a libSQL
21+
datasource would have been built as an ephemeral in-process store that accepts
22+
writes, reports success and loses everything — the #3276 silent-wrong-engine
23+
class with a new spelling. The arm is the same shape `mongodb` and `sqlite-wasm`
24+
already use (lazy import, typed not-installed error), because all three ride in
25+
optional packages and being an optional INSTALL has never meant lacking a
26+
contract.
27+
28+
The CLI and standalone stack still inject their own turso factory for the
29+
`default` datasource (#5602's host-factory seam), and an injected factory
30+
replaces this one — so this arm serves every OTHER door: a runtime datasource
31+
created in Setup, `testConnection`, a declared non-default datasource. Those
32+
doors previously got `supports() === false` and degraded; they now build.
33+
34+
**The fall-through itself is gone.** `memory` was the last arm's *implicit*
35+
position — no `if`, just the end of the function — so any `BuiltinDriverId` the
36+
switch did not handle silently became an in-memory store. It is now an explicit
37+
`kind === 'memory'` arm followed by an exhaustiveness stop typed `never`: adding
38+
a builtin without an arm is a compile error, and if a stale published
39+
`@objectstack/spec` ever reaches a newer consumer at run time, the result is a
40+
named refusal rather than a different engine. This is the trap the next driver
41+
would have inherited; turso is simply the one that found it.
42+
43+
**Why `major`.** The published `DRIVER_CATALOG[].id` value changes. Any consumer
44+
that compares a stored `datasource.driver` against the catalog id — a form
45+
pre-selecting the current driver, a grouped list, an equality filter — stops
46+
matching pre-rename rows until the conversion has run. Nothing throws, which is
47+
precisely why this is not a `minor`: the failure is a dropdown that silently
48+
shows no selection, and a bump that lets it arrive unannounced would be the same
49+
class of quiet as the defect the rename fixes.
50+
51+
**Not renamed, deliberately:** `SqlDialect`'s `'mongo'` member
52+
(`data/type-compat.ts`). That is a different vocabulary — it names the type
53+
system of an EXTERNAL schema being introspected, alongside `snowflake` and
54+
`bigquery`, and is never a `datasource.driver`. Renaming it would have been
55+
sympathetic magic on a matching string.
56+
57+
<!-- adr-0087: registered datasource-driver-mongo-to-mongodb -->
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
---
2+
"@objectstack/spec": major
3+
---
4+
5+
feat(spec)!: one driver vocabulary — `mongo``mongodb`, `turso` gets a config contract (#6345)
6+
7+
`packages/spec` has owned the driver alias table since #4410, for one reason
8+
stated in its own module comment: two tables would let the id that SELECTS a
9+
driver and the id that selects that driver's CONFIG CONTRACT disagree. That
10+
argument was right and the table was right; it just never reached the two boot
11+
hosts. Measured on `main` before this change, driving the real entry points:
12+
13+
| | `os start` | `os migrate` |
14+
| :-- | :-- | :-- |
15+
| `OS_DATABASE_DRIVER=pg` | accepted (`postgres`) | **refused by name** |
16+
| `OS_DATABASE_DRIVER=libsql` | accepted (`turso`) | **refused by name** |
17+
18+
**10 of 21 spellings disagreed.** Three prior cards (#3276, #5820, #6265) each
19+
fixed one spelling on one side, each with a green pin — and every pin drove
20+
exactly one host, which is why the fork survived all three.
21+
22+
**What this changeset changes in `@objectstack/spec`.**
23+
24+
The flat `Record<string, BuiltinDriverId>` becomes one table with a row per
25+
driver carrying `id`, `aliases`, `contractOnlyAliases` and `hasLocalDefault`.
26+
`BUILTIN_DRIVER_IDS`, `DRIVER_ID_ALIASES` and `resolveDriverId` are projections
27+
of it — `BUILTIN_DRIVER_IDS` keeps its exact tuple type, so the api-surface delta
28+
for this PR is purely additive (10 new exports, nothing removed or renamed).
29+
30+
Three faces are new, and they are what the two hosts consume:
31+
`resolveDatabaseDriverId()` (the selection face), `driverHasLocalDefault()` (does
32+
this driver have anything to fall back on with no URL) and
33+
`DATABASE_DRIVER_SELECTION_ALIASES` (what a refusal message enumerates).
34+
35+
**BREAKING — the canonical mongo id is `mongodb`.** `resolveDriverId('mongo')`
36+
now returns `'mongodb'`; `BuiltinDriverId` no longer includes `'mongo'`;
37+
`DRIVER_CONFIG_SCHEMAS` and `MongoDriverSpec.id` follow. The old canon was the
38+
one string on the platform that said `mongo` while both hosts, the npm package
39+
(`@objectstack/driver-mongodb`) and every URL scheme said `mongodb`, and the
40+
maintainer's ruling renames it rather than adding a mapping layer, so that
41+
selection canon and contract canon are one string.
42+
43+
`mongo` **stays an accepted alias**, deliberately: nothing that authored it
44+
breaks, and a deployment that never replays the conversion still resolves the
45+
same contract and builds the same driver. What needs migrating is the STORED
46+
value, because the canonical id is published as `DRIVER_CATALOG.id` — what Studio
47+
writes into `datasource.driver` — so after the rename the form emits `mongodb`
48+
while older rows carry `mongo`, and a reader matching stored rows against the
49+
catalog id silently misses them. The ADR-0087 D2 conversion
50+
`datasource-driver-mongo-to-mongodb` converges them at every rehydration seam.
51+
52+
**`turso`/libSQL becomes a complete builtin.** It was the mirror image of the
53+
mongo problem: both hosts dispatched it while spec shipped no contract, so
54+
`validateDriverConfig('turso', …)` answered `{ known: false }` and a libSQL
55+
`config` was the one connection block on the platform with no gate — `{ token }`
56+
(the wrong key; it is `authToken`) was accepted in silence and the connection
57+
attempted unauthenticated. `TursoConfigSchema` closes that. The keys are drawn
58+
from what `TursoDriverConfig` actually READS, not from what libSQL supports, so
59+
the fix does not open a new inert slot: `client` (a live object, unauthorable),
60+
`pool` and `schemaMode`/`readOnly` (datasource-level) are deliberately absent.
61+
62+
**Consumers of the `{ known: false }` answer, and what the flip does to each**
63+
established before making it, since a consumer depending on the negative answer
64+
would have been a stop condition:
65+
66+
1. `DatasourceSchema`'s `reportDriverConfigIssues` — was a no-op for turso, now
67+
parses. An authored turso `config` gains a real verdict.
68+
2. `service-datasource`'s `assertValidConfig` (the Setup wizard's door) — same
69+
flip, same reason.
70+
3. `DRIVER_CATALOG` — turso is deliberately NOT curated into the connection form,
71+
the same call `sqlite-wasm` has carried since #4410. No visible change.
72+
4. `driverReadsDeclaredPool` — answers `true` for turso before AND after (via the
73+
unknown-id branch before, the not-rejected branch now). Verdict unchanged.
74+
75+
**`sql` and `wasm` join the selection face; `sqlite3`, `better-sqlite3`,
76+
`mariadb` and `inmemory` do not.** The ruling fixes the selection face as the
77+
union of what the two hosts accepted, and those four were accepted by neither —
78+
so they stay `contractOnlyAliases`: they keep resolving a config contract
79+
(dropping that would silently un-validate a stored `driver: 'sqlite3'` row) while
80+
`resolveDatabaseDriverId` refuses them, because converging two hosts is not a
81+
licence to widen a boot flag on no ruling. That distinction is the thing the flat
82+
`Record` could not express and is why the table has two alias columns.
83+
84+
**Why `major` and not `minor`.** The alias widening alone would be `minor` — it
85+
only accepts more. The rename is what forces `major`: `BuiltinDriverId` loses a
86+
member, so every TypeScript consumer that switches on it or types a variable as
87+
it fails to compile, and `DRIVER_CONFIG_SCHEMAS['mongo']` is gone. That is a
88+
compile-time break even though the runtime behaviour is compatible, and pricing
89+
it as `minor` because "nothing breaks at run time" would be exactly the
90+
half-truth a consumer discovers at build time.
91+
92+
<!-- adr-0087: registered datasource-driver-mongo-to-mongodb -->
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
---
2+
title: Driver Turso
3+
description: Driver Turso protocol schemas
4+
---
5+
6+
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
7+
8+
Turso / libSQL Driver Protocol (#6345).
9+
10+
## Why this arrives late, and what it closes
11+
12+
`turso` was the one connection block on the platform with NO gate. #4410 gave
13+
every built-in driver's `datasource.config` a contract and made
14+
`DatasourceSchema` parse against it, but turso was not a builtin: its driver
15+
ships in an OPTIONAL package (`@objectstack/driver-turso`, #5602), so
16+
`resolveDriverId('turso')` returned `undefined` and `validateDriverConfig`
17+
answered `{ known: false }` — "nothing to check against". Meanwhile both boot
18+
hosts dispatched `turso` for real. So a libSQL datasource could carry
19+
`{ token: … }` (the wrong key — it is `authToken`) and be accepted in silence,
20+
then connect unauthenticated, which is precisely the failure #4410 exists to
21+
end, surviving in the one driver #4410 could not see.
22+
23+
The maintainer's #6345 ruling closes it by making turso a complete builtin
24+
rather than a permanent exception. Optionality of the PACKAGE is orthogonal to
25+
existence of the CONTRACT — `mongodb` and `sqlite-wasm` are optional installs
26+
too, and both have had a contract since #4410.
27+
28+
## What is declared here, and what is deliberately not
29+
30+
The keys below are exactly the `TursoDriverConfig` fields the driver reads and
31+
that an author can express as data. Three are deliberately absent:
32+
33+
- `client` (a pre-constructed `@libsql/client` instance) — a live object, not
34+
authorable metadata; declaring it would promise a JSON slot that can never
35+
be filled from a `sys_metadata` row.
36+
- `pool` — connection pooling is the datasource's own block, not driver
37+
config, exactly as on postgres/mysql/mongo.
38+
- `schemaMode` / `readOnly` — datasource-level, same as every other driver.
39+
40+
ADR-0049 (enforce-or-remove) is why the list is drawn from what the driver
41+
READS rather than from what libSQL supports: a key declared here that no
42+
driver consults would be a new inert slot, and this file exists to close one.
43+
44+
<Callout type="info">
45+
**Source:** `packages/spec/src/data/driver/turso.zod.ts`
46+
</Callout>
47+
48+
## TypeScript Usage
49+
50+
```typescript
51+
import { TursoConfigSchema, TursoTransportModeSchema } from '@objectstack/spec/data';
52+
import type { TursoConfig, TursoTransportMode } from '@objectstack/spec/data';
53+
54+
// Validate data
55+
const result = TursoConfigSchema.parse(data);
56+
```
57+
58+
---
59+
60+
## TursoConfig
61+
62+
Turso / libSQL Connection Configuration
63+
64+
### Properties
65+
66+
| Property | Type | Required | Description |
67+
| :--- | :--- | :--- | :--- |
68+
| **url** | `string` || libSQL endpoint or local file: a remote libsql/https Turso URL, a file path, or :memory: |
69+
| **authToken** | `string` | optional | JWT auth token for a remote libSQL database (prefer external.credentialsRef) |
70+
| **encryptionKey** | `string` | optional | AES-256 encryption key for the local database file (local/replica modes) |
71+
| **concurrency** | `integer` | optional | Maximum concurrent requests to the remote database |
72+
| **syncUrl** | `string` | optional | Remote sync URL for embedded-replica mode: a libsql or https Turso endpoint |
73+
| **sync** | `{ intervalSeconds?: integer; onConnect?: boolean }` | optional | Embedded-replica sync configuration (requires `syncUrl`) |
74+
| **timeout** | `integer` | optional | Operation timeout in milliseconds for remote operations |
75+
| **mode** | `Enum<'local' \| 'replica' \| 'remote'>` | optional | Force a transport mode instead of inferring it from `url` |
76+
77+
78+
---
79+
80+
## TursoTransportMode
81+
82+
Force a transport mode instead of inferring it from `url`
83+
84+
### Allowed Values
85+
86+
* `local`
87+
* `replica`
88+
* `remote`
89+
90+
91+
---
92+

content/docs/references/data/index.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ This section contains all protocol schemas for the data layer of ObjectStack.
2121
<Card href="/docs/references/data/driver-postgres" title="Driver Postgres" description="Source: packages/spec/src/data/driver/postgres.zod.ts" />
2222
<Card href="/docs/references/data/driver-sql" title="Driver Sql" description="Source: packages/spec/src/data/driver-sql.zod.ts" />
2323
<Card href="/docs/references/data/driver-sqlite" title="Driver Sqlite" description="Source: packages/spec/src/data/driver/sqlite.zod.ts" />
24+
<Card href="/docs/references/data/driver-turso" title="Driver Turso" description="Source: packages/spec/src/data/driver/turso.zod.ts" />
2425
<Card href="/docs/references/data/external-catalog" title="External Catalog" description="Source: packages/spec/src/data/external-catalog.zod.ts" />
2526
<Card href="/docs/references/data/external-lookup" title="External Lookup" description="Source: packages/spec/src/data/external-lookup.zod.ts" />
2627
<Card href="/docs/references/data/feed" title="Feed" description="Source: packages/spec/src/data/feed.zod.ts" />

content/docs/references/data/meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"driver-mysql",
3535
"driver-postgres",
3636
"driver-sqlite",
37+
"driver-turso",
3738
"field-value"
3839
]
3940
}

0 commit comments

Comments
 (0)