Repository navigation
fix(driver-turso): remote mode constructs without better-sqlite3 — its Knex base is built with no connection (#20054) - #20073
Conversation
…ion, so it never loads better-sqlite3 Claude-Session: https://claude.ai/code/session_01Bvd69VPa6puiNzzPUroDBx Co-authored-by: Claude <noreply@anthropic.com>
…qlite3; refusal texts stop naming the placeholder Claude-Session: https://claude.ai/code/session_01Bvd69VPa6puiNzzPUroDBx Co-authored-by: Claude <noreply@anthropic.com>
…-sqlite3; record the measured reverse verification Claude-Session: https://claude.ai/code/session_01Bvd69VPa6puiNzzPUroDBx Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a75ccd92361f0e60765a4b14f3a282275b3c0958 && git checkout a75ccd92361f0e60765a4b14f3a282275b3c0958
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b76aad5f6fac71cbbcd4ea6bfdf21a59e2fd4d56 93d49b886e8e7d53dc5ee9e77e0d109bc89b7db6 && git checkout -B drift-repro b76aad5f6fac71cbbcd4ea6bfdf21a59e2fd4d56 && git merge --no-ff 93d49b886e8e7d53dc5ee9e77e0d109bc89b7db6
node scripts/docs-audit/affected-docs.mjs --json b76aad5f6fac71cbbcd4ea6bfdf21a59e2fd4d56
|
… narrowing — minor, Clause-② no (narrowing), ADR-0087 not-required Claude-Session: https://claude.ai/code/session_01Bvd69VPa6puiNzzPUroDBx Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Scope: 5 files (+343/−31) on merge base
No governed surface is touched. Review is owed for the Read: cards #20054 and #20055, the PR body, the diff, all 41 check-runs, AGENTS.md, and the code at head and base. Measured in two detached worktrees (head and base, each with the driver-turso closure built and each dist verified to carry its own remote arm, both removed afterwards), under knex 3.3.0 and ① Derived judgments
② Semver level
The dev's original ③ Boundary flags
Implemented-by: VERDICT: PASS Isolated reviewer: a contract-review-tier subagent, fed only the cards, the PR and AGENTS.md; adopted by the seat. |
…erited SqlDriver member (objectstack-ai#20055) (objectstack-ai#20104) Fixes objectstack-ai#20055 Clause-②: yes (narrowing) ## What this changes `TursoDriver extends SqlDriver`, and on the remote face (`transportMode === 'remote'`) 23 public `SqlDriver` members had no remote arm. Since PR objectstack-ai#20073 the remote face builds its Knex with no connection, so those members either failed with knex's `Unable to acquire a connection` (a connectivity reading of a capability gap) or answered from state no remote door fills. Each one is now answered on the remote database, refused with `NOT_IMPLEMENTED` / 501, or shown to be true on this face as inherited. - `packages/drivers/driver-turso/src/turso-driver.ts` - `REMOTE_FACE_ANSWERS`: every public `SqlDriver` member (52) mapped to `remote`, `refused` or `inherited`, each `inherited` row with its reason. The object `satisfies` a record keyed by `keyof SqlDriver`, which is exactly the public instance members, so a member added to `SqlDriver` fails this package's typecheck and build until its remote answer is decided. Exported from the module, not from the package index. - `refuseRemoteInheritedMember()`: one builder in the objectstack-ai#19845 / objectstack-ai#19894 shape (operation, what the caller cannot have, why, the 501 rationale, the alternative). No new error code. - New overrides, each with the local / replica arm unchanged: `distinct`, `findWithWindowFunctions`, `analyzeQuery` (which `explain` calls), `introspectSchema`, `getKnex`, `reclaimSpace`, `supportsRotation`, `rotateShards`, `applyMigrationEntries`, `setFileColumnsMovedResolver`. - `packages/drivers/driver-turso/src/remote-transport.ts`: one member, `compileDistinct(object, field, where)`, which compiles the `SELECT DISTINCT` with the transport's own where compiler and does not execute it. The `buildWhereSQL` comment's count of external call sites goes from five to six. - `packages/drivers/driver-turso/src/turso-remote-inherited-members.test.ts`: new pin, 76 tests. - `packages/drivers/driver-turso/src/turso-remote-media-column-move-refusal.test.ts`: one assertion flips from `true` to `false` (`setFileColumnsMovedResolver` on the remote face; see the table). - `packages/drivers/driver-turso/README.md`: the "What remote mode refuses" table gains seven rows, a new list covers the members answered differently from local mode, and the Capabilities section is rewritten from the code at this head (triage point 6, pointer 5824443053). - `.changeset/20055-turso-remote-inherited-members.md`: `@objectstack/driver-turso` minor, `fix(driver-turso)!:`. ## The 23 inherited members: measured at base and at head Base is `d4c897e0e7` (past `249172975b`, PR objectstack-ai#20073). Each reading was taken on a remote face over a real `@libsql/client` `file:` client holding the rows a local control (`file:` URL, Knex) answered from, driver dist built at each tree. The probe is a node script over the built `dist/index.mjs`, kept in the session scratchpad and not committed, with one fresh database per call and per face. | member | remote at base | remote at head | answer | narrowing | |:--|:--|:--|:--|:--| | `distinct` | `DATABASE_ERROR` / 500 | the local values (5 call shapes); unknown column `INVALID_FIELD` / 400 as local; tenant-scoped call 501 | remote | no | | `findWithWindowFunctions` | bare `Unable to acquire a connection` | 501 | refused | no | | `analyzeQuery` | resolved: the local compiler's SQL plus `error` | 501 | refused | **yes** | | `explain` | resolved, as `analyzeQuery` | 501 (it calls `analyzeQuery`) | inherited | **yes** | | `introspectSchema` | bare `Unable to acquire a connection` | 501 | refused | no | | `getKnex` | returned a Knex whose `raw('select 1')` failed | throws 501 | refused | **yes** | | `reclaimSpace` | bare `Unable to acquire a connection` | resolves; freelist 50 to 49 on an INCREMENTAL database | remote | no | | `supportsRotation` | `true` | `false` | remote | **yes** | | `rotateShards` | bare `Unable to acquire a connection` | 501 | refused | no | | `applyMigrationEntries` | resolved; a destructive entry with `allowDestructive: true` came back `skipped` | 501, with or without entries | refused | **yes** | | `setFileColumnsMovedResolver` | `true`, then asked 0 times | `false` | remote | **yes** | | `commitTransaction`, `rollbackTransaction` | 501 through `commit()` / `rollback()` | same | inherited | no | | `registerExternalObject`, `registerObjectMetadata` | answer, no Knex | same | inherited | no | | `deferredSchemaObjectCount`, `previewDeferredSchemaWork`, `flushDeferredSchemaDdl` | `0`, `[]`, `[]` (arming is refused) | same | inherited | no | | `getSchemaSyncStats` | `{ created: 0, existing: 0 }` (local control: `{ created: 2, existing: 0 }`) | same | inherited | no | | `dialectName`, `sqliteOpenedEmptyInMemory` | `'sqlite'`, `false` | same | inherited | no | | `temporalFilterValue`, `temporalFilterColumnSql` | equal to local | same | inherited | no | Loud to silent: 0. Answered to refused, or a declaration narrowed: 6 (bold). Failed to answered: 2 (`distinct` unscoped, `reclaimSpace`). Failed to refused with the envelope: 5. **Enumeration.** A TypeScript-parser walk (`ts.createSourceFile`) over the `SqlDriver` class at base: 52 public instance members (45 methods, 5 getters, 2 fields). The `TursoDriver` class declared 29 of them, including the control `find`, so 23 were inherited (19 methods, 4 getters). The compiler repeats the count at head: `REMOTE_FACE_ANSWERS` has 52 rows and satisfies the `keyof SqlDriver` record (`tsc` exit 0). ## PM hypotheses - **H1, confirmed and extended.** All 12 named members are inherited. The walk adds 11: `registerExternalObject`, `registerObjectMetadata`, `getSchemaSyncStats`, `getKnex`, `temporalFilterValue`, `temporalFilterColumnSql`, `setFileColumnsMovedResolver` and 4 getters (`sqliteOpenedEmptyInMemory`, `supportsRotation`, `deferredSchemaObjectCount`, `dialectName`). "The ones that touch Knex reject with `Unable to acquire a connection`" held for 4 (`introspectSchema`, `findWithWindowFunctions`, `reclaimSpace`, `rotateShards`) and not for 3: `analyzeQuery` / `explain` and `applyMigrationEntries` resolved, catching the knex failure into their answers. `commitTransaction` / `rollbackTransaction` never reached Knex: they call the overridden `commit` / `rollback`, which already refuse. - **H2.** - `introspectSchema`: refused. The inherited member reads each table through `columnInfo()` and three SQLite pragma readers (columns, foreign keys, primary keys, unique constraints), so a remote answer is a second copy of four Knex readers, the copy objectstack-ai#19845 declined for drift. - `distinct`: routed, and equal to the local control on 5 shapes (one column, under a filter, a declared boolean presented as `true` / `false`, a tenant object unscoped, `tenantId` on an object with no tenant column). One shape is refused instead: `tenantId` on an object with a tenant column. The local face applies the tenant scope there and answers `['A', 'P']`, while no remote read applies it (see Acceptance notes), so a routed answer would list every organization's values. - Window functions: libSQL runs them (a raw `row_number() over (...)` through `execute()` answers on both faces), but `findWithWindowFunctions` compiles its spec through the Knex builder, so routing means a second window compiler. Refused, and the message names `execute()` as the remote alternative. - Beyond H2: `analyzeQuery` / `explain` are refused rather than routed. Neither has a caller in this repository (grep: 0), and a remote plan needs the transport to hand out its compiled read statement, a second new member. The route is about 40 lines if the seat wants it (open question in the report). - **H3, measured.** The real caller code ran against the head remote driver and against a double that replays the base answer measured above. The callers are unchanged by this PR, so the double gives the base reading. - `ExternalDatasourceService.testConnection`: base `ok: false`, error `Unable to acquire a connection`. Head `ok: false`, error `Schema introspection (introspectSchema()) is not supported by the Turso REMOTE transport ...`. Local control `ok: true, tableCount: 1`. - Its validation sweep (`validateAll`): base, one diff of kind `unreachable` carrying the knex message. Head, the same kind carrying the 501 message. - `LifecycleService.sweep()`, one retention object and one rotation object (`storage.strategy: 'rotation'`, no `ttl`): - base: `report.errors` holds `sys_activity: Unable to acquire a connection` (the rotation object is never bounded), and `reclaimed` is `[]`, with two warns; - head: the rotation object takes the `rotation-fallback` age reap, `reclaimed` is `['com.objectstack.driver.turso']`, and there are no warns. - **H4.** Six members narrow (bold rows). The claim carries `Clause-②: no (narrowing)`. This body declares `yes (narrowing)` instead, because `RemoteTransport` is exported from the package and gains a public method (`compileDistinct`), which widens the public surface. See Deviations. ## Tests At head `3c1cc7a316`: - `pnpm --filter @objectstack/driver-turso test`: 68 files, 1586 passed. The new file contributes 76 of those tests. - `pnpm --filter @objectstack/driver-turso typecheck`: exit 0. `tsc --listFiles` includes all four changed `.ts` files. - Consumer files that construct a `TursoDriver`, in `service-datasource` (8 files that mention Turso): 226 passed. `runtime` and `dogfood` consumers are declared to CI. Their closures were not built here. - `pnpm check:driver-conformance`: `OK — 50 covered cell(s), 0 in the DEBT ledger, 0 exempt`, both at base (a detached worktree at `d4c897e0e7`) and at head. **Reverse verification.** The fix was committed first. Each leg went through `node scripts/ablation-replace.mjs`, which restored the file to the HEAD blob (`git diff HEAD` empty) after each run. The test imports `./turso-driver.js` from source, so no leg needed a rebuild. - Table pin, leg 1: deleting the `introspectSchema` row gave `TS1360 ... does not satisfy the expected type` (typecheck exit 2). - Table pin, leg 2: adding a row `getKnexTwice` gave `TS2353 ... 'getKnexTwice' does not exist in type` (exit 2). The first spelling of leg 2 was refused by the tool as a no-op anchor and never ran; it was re-run with a new anchor. - Behaviour, leg 3: removing the `introspectSchema` remote arm reddened 1 of 76 tests, the `introspectSchema` refusal case (`expected undefined to be 'NOT_IMPLEMENTED'`), as predicted. - Behaviour, leg 4: `supportsRotation` answering the base reddened 1 of 76, the `supportsRotation` case (`expected true to be false`), as predicted. - Behaviour, leg 5: removing the tenant-scoped `distinct` refusal reddened 1 of 76, the tenant-scoped case (`expected a refusal, but the call answered`), as predicted. ## Gates Derived at head `3c1cc7a316` from the real diff with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (no paths): 62 commands. The dispatch's list (derived at `2c1011b`) had 54, and all 54 are included. The 8 new ones are `check-adr-0087-registration` x2, `check-empty-changeset` x2, `release-rehearsal-clone --self-test`, `check:dispatcher-error-vocabulary`, `check:objectui-changeset` and `check:pm-changeset-deadline-census`. - 60 exited 0. - 2 are NOT MEASURED (exit 3, `PREREQUISITE NOT MET`): `check:dual-build-cjs-loads` and `check:type-check-debt`. Both need the whole workspace built. CI builds it. - `--ran` reconciliation, exit codes recorded: `62 derived, 60 run, 2 NOT-MEASURED, 0 UNRUN`. - `check-adr-0087-registration`: `✓ check-adr-0087-registration: 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition.` - `check-changeset-no-major`: `✓ This diff introduces no major bump.` With this body as the `--event` payload: `✓ LEVEL AXIS: this PR declares clause-② yes (narrowing), and no package whose packages/**/src/** it moves is graded patch.` `check-adr-0087-registration` tags the changeset `[BREAKING+bang+clause-②-narrowing] not-required (no-migration-prescription)`. - `node scripts/check-issue-citations.mjs --base d4c897e`: exit 0, 6 citations resolve. - Lint, narrowed and proven: - the population is read from eslint itself: of the 6 changed files, the 2 Markdown files are outside the config ("no matching configuration"); - the 4 `.ts` files: 0 errors, 0 warnings (`--no-inline-config --format json`, at `e004f9ce81`, with the same `.ts` bytes as head); - invariance: `eslint.config.mjs` enables no type-aware linting (no `parserOptions.project`), so this diff cannot change a verdict on an untouched file. - Out-of-gate control-byte scan of the changed files: no match. - CI owns the repo-wide `pnpm lint` and the path-scheduled jobs. ## Deviations - **Clause-② reads `yes (narrowing)`, where the claim read `no (narrowing)`.** `RemoteTransport` is a public export (`src/index.ts`), and it gains `compileDistinct()`. The narrowing half is the six bold rows. The bump is `minor` either way. - **The file surface stayed inside the claim.** One line outside the new overrides moved in an existing test (the `setFileColumnsMovedResolver` assertion in the media-column-move test). It pinned the old `true`, the answer this card measured as false. - **`explain` / `analyzeQuery` are refused, not routed.** They have no in-repo caller, and a route needs a second transport member. Offered as an open question. ## Acceptance notes Found on the way, measured, not changed here (reported to the seat with evidence): - The remote read doors ignore `options.tenantId`. Remote `find('probe_org', {}, { tenantId: 'org_a' })` answered `t1, t2, t3` (org_b's row included); the local control answered `t1, t3`. This is why the scoped `distinct()` is refused. - The remote face ignores a federated object's `external.remoteName`. After `registerExternalObject({ name: 'ext_t', external: { remoteName: 'probe_t' } })`, remote `find('ext_t')` threw a bare `LibsqlError` (`no such table: ext_t`, no envelope); local read `probe_t`. - `SqlDriver.reclaimSpace()` returns ONE free page per call on both faces (freelist 300 to 299 locally, 50 to 49 remotely; the same `PRAGMA incremental_vacuum` over the libSQL client directly: 300 to 299). - Traced, not run: `ObjectQL.wasDatastoreCreatedFromEmpty()` counts a driver answering `{ created: 0, existing: 0 }` as reporting, with nothing to veto. Next to a driver that created tables, a remote Turso datasource therefore does not make the answer "no", while the contract reads zero-zero as "cannot say". - Raw `SAVEPOINT` statements through `execute()` work on a libSQL `file:` client. Against a remote Turso server they remain unmeasured, and the README says so. --- _Generated by [Claude Code](https://claude.ai/code/session_01Bvd69VPa6puiNzzPUroDBx)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #20054
Clause-②: no (narrowing)
What this changes
A remote
TursoDriver(libsql://,https://,wss://and the other remote schemes) now constructs, connects and runs CRUD withbetter-sqlite3not installed. That is the installpackage.json(an optional peer) and the README's "Dependencies by Mode" table give remote mode, for Vercel and Edge deployments.The fix is one arm of
TursoDriver.toKnexConfig. TheSqlDriverbase constructor always builds a Knex instance. Remote mode used to hand it{ client: 'better-sqlite3', connection: { filename: ':memory:' } }. knex'sClientconstructor loads the dialect's native driver (initializeDriver, which runsrequire('better-sqlite3')) and builds a pool only when the config carries aconnection. Remote mode now passes{ client: 'better-sqlite3', useNullAsDefault: true }with noconnection: the SQLite dialect's compiler, with no native module, no pool and no private:memory:database. The client is still spelledbetter-sqlite3, soisSqliteand every dialect-keyed rule the remote arms borrow from the base answer as before.SqlDriveris not touched, and no export changes.Files:
packages/drivers/driver-turso/src/turso-driver.ts: the remote arm oftoKnexConfigand its comment. Also the text of two remote refusals this change made false (see Deviations).packages/drivers/driver-turso/src/turso-remote-no-native-driver.test.ts: new pin..changeset/20054-turso-remote-no-native-driver.md:@objectstack/driver-tursominor, BREAKING (patch round 1).Measurements
All readings were taken in
objectstack-ai/objectstack. Base:3557f85fa5. Head: this branch. The probes used dist built at each tree. "Absent" meansbetter-sqlite3made unresolvable in a separate node process (aModule._resolveFilenamehook). The pin uses aModule._loadhook instead.H1 (the throw), confirmed at base. knex 3.3.0
lib/client.js:if (this.driverName && config.connection) this.initializeDriver();, which callsClient_BetterSQLite3._driver()(require('better-sqlite3')).libsql://probe-db.example.turso.ioKnex: run $ npm install better-sqlite3 --savetransportMode = 'remote':memory:(control)file:(control)H2: what the remote face uses
this.knexfor. I wrappeddriver.knexin a Proxy that records every property read and call after construction. The remote driver ran over a real@libsql/clientfile:client.SqlDriverconstructor'sknex(config)call, andinstallQueryTiming(this.knex.onfor three events). Read from the source, not recorded.connect,checkHealth,initObjects,syncSchema,syncSchemasBatch,create,bulkCreate,find,findOne,count,aggregate,update,upsert,bulkUpdate,updateMany,execute,paginationTieBreaker,bulkDelete,deleteMany,delete,dropTableanddisconnect. Plus the 4 refusals:beginTransaction,detectManagedDrift,planMediaColumnMove,setDeferredDdl(true). Zero reads, at base and at head.this.knexreference in their bodies (dist, 14 names):temporalFilterValue,temporalFilterColumnSql, the threesqlite*Sqlrules,isNonTextColumn,formatInput,formatOutput,computeTenantField,calendarDayUpperBoundRewrite,orderKeysFor,registerExternalObject,toDateOnlyandrawStatementFault.SqlDrivermethods remote mode does not override reach it:introspectSchema(.raw),distinct(builder),findWithWindowFunctions(builder,.raw,.client),analyzeQueryandexplain(builder,.raw), andreclaimSpace(.raw).previewDeferredSchemaWork,flushDeferredSchemaDdl,applyMigrationEntries([])andgetSchemaSyncStatsmade zero reads.rotateShardsthrew before it reached Knex.disconnect()never callssuper.disconnect(), so knex is never destroyed on the remote face.So nothing on the remote face needs a live Knex, and nothing needs a compiling one either. A Knex with no connection was the smallest seam: it lives in
toKnexConfig, andSqlDriver's constructor contract stays as it is.H3: the inherited methods, before and after. A remote face over a libsql
file:client holding rows inprobe_t.introspectSchema(){ tables: {} }(silent)Unable to acquire a connectiondistinct('probe_t', 'name')DATABASE_ERROR/ 500findWithWindowFunctions(…)SqliteErrorno such table: probe_tUnable to acquire a connectionanalyzeQuery/explain{ sql, bindings, client, error: 'EXPLAIN QUERY PLAN … no such table …' }error: 'Unable to acquire a connection'reclaimSpace()Unable to acquire a connectionpreviewDeferredSchemaWork,flushDeferredSchemaDdl,applyMigrationEntries([]),getSchemaSyncStatsrotateShards('probe_t')introspectSchema,reclaimSpace).introspectSchema()kept the process alive afterdisconnect().timeout 20killed it (exit 124), because a pooled better-sqlite3 connection was never destroyed. At head, the process exits (no pool is built).H4: in-repo constructions.
git grep 'new TursoDriver('on non-test.tsfiles: 9 hits, 3 of them real calls:createTursoDriverand the plugin'sonEnableindriver-turso/src/index.ts;service-datasource/src/default-datasource-driver-factory.ts.The runtime loader adds
new TursoDriverCtor(inruntime/src/turso-driver-factory.ts. None of the four reads.knex. The datasource factory'ssqlServerVersiongoes throughdriver.execute, which is the remote transport.Repo-wide reads of a driver's Knex outside
driver-sql, non-test:getKnex(): 0 calls (1 comment). Control:sql-driver.tshas 1 hit..knexreads: 2 files.metadata-protocol'sresolveDriverClientNamereadsdriver.config.clientfirst, and that answer is unchanged (better-sqlite3).runtime/src/raw-foreign-key-fixture.tsis used by two SqlDriver integration tests and not by any Turso test.Tests
New pin
turso-remote-no-native-driver.test.ts: 10 tests.:memory:andfile:still throw without the module. The hook counted at least one load attempt, which proves it is live.@libsql/clientfile:client without the module: 0 load attempts, and the rows read back from that file.introspectSchema,distinctandfindWithWindowFunctions, each with the module present and absent. Each call must either fail or answer from the remote database, never "no tables". The envelope is left to driver-turso remote: inheritedSqlDrivermethods the remote face does not override answer from the placeholder:memory:Knex database —introspectSchema()returns no tables,distinct()a 500,findWithWindowFunctions()a raw SQLite error #20055.Reverse verification. The fix was committed first.
node scripts/ablation-replace.mjsrestoredconnection: { filename: ':memory:' }in the remote arm: anchor 1 to 0, blob227df64b3046toa5b36d46ed8f. The pin went 6 failed, 4 passed, the direction predicted before the run:introspectSchema, module present, failed onexpected [] to include 'probe_t';distinctandfindWithWindowFunctionswith the module present, which were already loud at base.After the restore, the blob equals HEAD and
git diff HEADis empty. The test imports./index.js(source), so no rebuild was part of either leg.pnpm --filter @objectstack/driver-turso test: 67 files, 1510 passed.typecheck: exit 0.tsc --listFilesincludes all 4 changed.tsfiles.pnpm --filter @objectstack/driver-sql test: 184 files passed, 11 skipped. 2845 tests passed, 170 skipped.Consumers that construct a
TursoDriver(targeted files):service-datasource:default-datasource-driver-factory,datasource-pool-support,turso-bound-secret-authoringandturso-driver-config. 4 files, 147 passed.runtime:turso-driver-factory.convergenceandstandalone-stack.libsql. 2 files, 38 passed.dogfood:date-bucket-parity-turso, which builds a real remote driver. 1 file, 5 passed.storage-driver.test.tsbuilds a fakeTursoDriverclass, not this one. It is declared to CI, not run here.pnpm check:driver-conformance, before (base, in a detached worktree) and after:OK — 50 covered cell(s), 0 in the DEBT ledger, 0 exempt, both.Gates at head
014b58a689, derived from the real diff withnode scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(merge base3557f85fa): 61 commands. The dispatch's list (derived at2c1011b) had 54, and all 54 are included. The 7 new ones arecheck-adr-0087-registrationx2,check-empty-changesetx2,release-rehearsal-clone --self-test,check:objectui-changesetandcheck:pm-changeset-deadline-census.PREREQUISITE NOT MET):check:dual-build-cjs-loadsandcheck:type-check-debt. Both read the whole workspace's built output, and this worktree built only the closures listed above. lint.yml builds the whole closure first, so CI measures them.check:dts-closurefirst exited 1. It named 26 packages whosedist/I had built locally withOS_SKIP_DTS=1to run the consumer suites. I removed thosedist/directories and re-ran it: exit 0,46/46 declared declaration file(s) present across 8 package(s),driver-tursoamong them.--ranreconciliation with exit codes recorded:61 derived, 59 run, 2 NOT-MEASURED, 0 UNRUN.node scripts/check-issue-citations.mjs --base 3557f85fa5: exit 0, 3 citations resolve.pnpm lint, the CLI unit layer, and the path-scheduled CI jobsdispatch-gatesnames.Deviations
Two runtime strings in
turso-driver.tsoutside the claimed arm. TheNOT_IMPLEMENTED/ 501 messages of the remotedetectManagedDrift()andplanMediaColumnMove()said "in remote mode that connection is a placeholder in-memory database holding none of this datasource's tables". This change made that false. They now say remote mode has no Knex connection. Each still gives a reason that holds at head, measured by calling the inherited methods on a head remote driver:[]from the emptymanagedObjectFields;hasTable.First sentences, codes and statuses are unchanged, so both existing pins stay green. Their docblocks and the two comments on the overrides were edited to match.
Acceptance notes
introspectSchema()on a remote driver now fails with knex'sUnable to acquire a connection. That message points at connectivity. The real answer belongs to driver-turso remote: inheritedSqlDrivermethods the remote face does not override answer from the placeholder:memory:Knex database —introspectSchema()returns no tables,distinct()a 500,findWithWindowFunctions()a raw SQLite error #20055, which will either route the call to the transport or refuse it withNOT_IMPLEMENTED. driver-turso remote: inheritedSqlDrivermethods the remote face does not override answer from the placeholder:memory:Knex database —introspectSchema()returns no tables,distinct()a 500,findWithWindowFunctions()a raw SQLite error #20055 remains open for all three methods. The callers I traced (not run):ExternalDatasourceService.testConnectionnow reportsok: falsewith that message, where it reportedok: true, tableCount: 0;unreachable(logged at warn, boot continues), where it rowedmissing_tableagainst the empty answer.The datasource admin's
testConnectionpreferscheckHealth, so it is unaffected.The pending changesets
19845-turso-remote-drift-detection-refusal.mdand19894-turso-remote-media-column-move-refusal.mddescribe the placeholder in the present tense. They narrate what those refusals replaced. I left them alone, and this PR's changeset says the placeholder is gone.Banner.
os serve's banner reads a registered driver'sconfig. For a remote Turso driver,describeDriverConnectionreturned:memory:at base (a false address) and returnsundefinedat head.describeRegisteredDriverthen prints(unknown). The renderer was measured and the fallback traced.The CLI's migrate
describeDbfalls back to the client name. At head it would printbetter-sqlite3for a remote Turso datasource instead of:memory:. Traced only.Semver
@objectstack/driver-turso: minor,fix(driver-turso)!:,Clause-②: no (narrowing), with an ADR-0087not-required (no-migration-prescription)disposition in the changeset. This was ruled in patch round 1, below.Patch round 1
The PM seat answered the open question with B, for consistency with this lane's precedents on the same driver face: #19893 (PR #19971) and #19894 (PR #20014). On a remote driver,
introspectSchema()andreclaimSpace()resolved before and reject now. That is a call the published driver answered and no longer answers, so it is declared, whatever the old answer's quality.What changed, in
.changeset/20054-turso-remote-no-native-driver.mdonly (commit93d49b886e, no code change):minorand the summary readsfix(driver-turso)!:;Clause-②: no (narrowing);introspectSchema()(used to resolve{ tables: {} }) andreclaimSpace()(used to resolve). On a remote driver both now reject with knex'sUnable to acquire a connection;findWithWindowFunctions(),analyzeQuery()/explain()anddistinct()are listed apart, as changing their error wording only or not at all;not-required (no-migration-prescription)disposition in the driver-turso: a remote url plus syncUrl is classified replica and handed a :memory: Knex connection, so every write lands in process memory and never reaches the remote #19893 shape. No key, schema, object definition or stored representation moves, only which calls a remote driver answers. driver-turso remote: inheritedSqlDrivermethods the remote face does not override answer from the placeholder:memory:Knex database —introspectSchema()returns no tables,distinct()a 500,findWithWindowFunctions()a raw SQLite error #20055 carries the per-method answer or refusal.The version axis is unchanged:
.changeset/19894-turso-remote-media-column-move-refusal.mdalready bumps@objectstack/driver-tursominorin the pending release.Gate verdicts at
93d49b886e, against merge base3557f85fa5:check-changeset-no-major:✓ This diff introduces no `major` bump.With this body as the--eventpayload:✓ LEVEL AXIS: this PR declares clause-② `no (narrowing)`, and no package whose `packages/**/src/**` it moves is graded `patch`.check-adr-0087-registration:✓ check-adr-0087-registration: 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition.The changeset is tagged[BREAKING+bang+clause-②-narrowing] not-required (no-migration-prescription).check-empty-changeset:✓ No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added).Generated by Claude Code