Skip to content

Commit 3557f85

Browse files
docs(driver-turso): the README lists what remote mode refuses instead of transactions it does not run (#20053)
Fixes #20016 Clause-②: no `packages/drivers/driver-turso/README.md` said that `RemoteTransport` runs `beginTransaction`, `commit` and `rollback`. It does not: `RemoteTransport` declares no transaction methods, and `TursoDriver` refuses all three on the remote face with `NOT_IMPLEMENTED` / 501. The README also named none of the remote face's other refusals. This PR corrects the remote branch of the architecture tree and adds one list, **What remote mode refuses**, written from `turso-driver.ts` and `remote-transport.ts` at `adbbc5d01e`. Docs and a changeset only: no source file changes. ## What changes (each sentence, before and after, with the code it was written from) 1. **Architecture bullet, remote mode.** - Before: "Delegates all operations to `RemoteTransport` which uses `@libsql/client` SDK directly (HTTP/WebSocket). No local SQLite or Knex dependency needed." - After: "Sends CRUD, bulk writes, `aggregate`, raw `execute`, schema sync and `dropTable` to `RemoteTransport`, which uses the `@libsql/client` SDK directly (HTTP/WebSocket). Transactions and the other calls listed under What remote mode refuses are refused instead." - From: the `if (this.isRemote)` arms of `TursoDriver.find` / `findOne` / `create` / `update` / `upsert` / `delete` / `count` / `aggregate` / `bulkCreate` / `bulkUpdate` / `bulkDelete` / `updateMany` / `deleteMany` / `execute` / `syncSchema` / `syncSchemasBatch` / `initObjects` / `dropTable`, each forwarding to `this.remoteTransport`. "All operations" was false: `beginTransaction`, `commit`, `rollback`, `setDeferredDdl(true)`, `detectManagedDrift` and `planMediaColumnMove` refuse in `TursoDriver` and never reach `RemoteTransport`. - The dropped sentence "No local SQLite or Knex dependency needed" was false too. `TursoDriver.toKnexConfig` hands the `SqlDriver` constructor a `better-sqlite3` Knex config in remote mode, and that constructor builds Knex straight away. Measured end to end (see the out-of-scope findings): with `better-sqlite3` unresolvable, `new TursoDriver({ url: 'libsql://…' })` throws. I did not replace that sentence with a new dependency claim; the finding is for the code. 2. **Tree, local branch.** "Inherited: applyFilters (MongoDB-style + array-style)" → "Inherited: applyFilters (MongoDB-style)". From: `SqlDriver.compileFilters` in `driver-sql`, which throws `filterArrayReachedDriverError` for any non-empty filter array. The dispatch allowed this fix because the line is in the same tree and plainly false. 3. **Tree, remote branch.** Removed "RemoteTransport: beginTransaction, commit, rollback". Added "Refused, NOT_IMPLEMENTED / 501: beginTransaction, commit, rollback, setDeferredDdl(true), detectManagedDrift, planMediaColumnMove". From: `TursoDriver.beginTransaction` / `commit` / `rollback` (each calls `refuseRemoteTransaction`), `setDeferredDdl` (`refuseRemoteDeferredDdl` when armed), `detectManagedDrift` (`refuseRemoteDriftDetection`) and `planMediaColumnMove` (`refuseRemoteMediaColumnMove`). A grep for a `beginTransaction`, `commit` or `rollback` method declaration in `remote-transport.ts` finds 0; the same pattern shape finds its `find`, `create` and `execute` declarations (3). 4. **Remote example comment.** "// All CRUD operations work the same as local mode" → "// The CRUD methods are the same as in local mode; some calls are refused (below)". The old comment was false for `create` / `bulkCreate` / `upsert` on an object with an empty `autonumber` field (`refuseUngeneratableRemoteAutonumber`), and for any CRUD call that carries `options.transaction` (`assertRemoteTransactionUnsupported`). 5. **New subsection "What remote mode refuses".** Every sentence and where it comes from: - "refused … with `code: 'NOT_IMPLEMENTED'` and `status: 501`": the five builders in `turso-driver.ts` and the two reachable ones in `remote-transport.ts` all set `StandardErrorCode.enum.NOT_IMPLEMENTED` and `501`. - "the call is valid, and the remote transport does not have the capability": the reason each builder's message gives (a capability gap, not a mistake in the request). This is described, not quoted. - "The check runs before the call reads or writes anything": in each method the refusal is the first thing that runs, or it runs before the first `remoteTransport` call. The pins assert this: the transaction suite asserts "the client is never asked for one", and the groupBy suite asserts no statement is captured. - Transactions row: `refuseRemoteTransaction` is reached from the three transaction methods, and from `assertRemoteTransactionUnsupported`, which the 15 `RemoteTransport` methods in the tree plus `aggregate` and `syncSchemasBatch` call first. That is 17 doors, counted by grep. `initObjects` takes no `options`, and the inherited `distinct` / `findWithWindowFunctions` are not guarded, so the row names the 17 doors and does not say "every method". The alternative ("run Knex transactions and honour `options.transaction`") comes from the local arms calling `super` and from the refusal's own remedy text. - Record-numbers row: `refuseUngeneratableRemoteAutonumber` is called from `create`, `bulkCreate` and `upsert` when `mayMerge` is false. `mayMerge` is true when the data carries `id` or `_id`, or when `conflictKeys` is non-empty. The empty test is `isEmptyAutoNumberSlot` (`undefined` / `null` / `''`). The alternatives come from the refusal message. - Deferring-DDL row: `setDeferredDdl` refuses only when `deferred && this.isRemote`. `os migrate plan` boots with `deferSchemaDdl: true` (`commands/migrate/plan.ts`), and that makes the CLI's `DeferSchemaDdlPlugin` call `setDeferredDdl(true)`. The alternative comes from the refusal message. - Drift row: `detectManagedDrift` refuses on remote with or without `objects`. The alternative comes from the refusal message. - Media-column row: `os migrate files-to-references` calls `planMediaColumnMove` from its `runColumnStep`. The local and replica arms call `super.planMediaColumnMove()`. - Transactions bullet: the remote arm of `supports` sets `transactionsUnsupported: this.transportMode === 'remote'`. `driverSupportsTransactions` (spec `driver.zod.ts`) returns false when that is set. `engine.transaction()` then throws `TransactionUnsupportedError` under `require: true`, or calls `warnTransactionUnsupported`, which warns once per datasource name, and runs the callback without a transaction (`objectql/src/engine.ts`). - Record-numbers bullet: the registry is `autoNumberFields`, filled by `registerRemoteFieldMetadata` → `SqlDriver.registerExternalObject` from `completeRemoteSchemaSync`, which the three remote schema doors call. The "logs a warning naming the row" behaviour is `reportUnnumberedRemoteRow`. I watched it fire during the test run below (row id `never-seen`). - Aggregation bullet: `RemoteTransport.aggregate` calls `refuseDateBucketedGroupBy` for any `dateGranularity`, and `refusePerAggregationFilter` for a non-empty `filter`. `engine.aggregate` takes its in-memory path when `allStructuredSupported` is false (remote publishes `queryDateGranularity: {}`) or when `hasAggregationFilter` is true. On that path it strips `groupBy` / `aggregations` / `having` before `driver.find`. The local face (`SqlDriver.aggregate`) throws `unsupportedAggregationFilterError` for the same `filter`. The tree's other remote lines (`find … upsert`, the bulk line, `syncSchema, dropTable`, `execute (raw SQL)`) are true and unchanged. The other local-branch lines are true: each local arm calls `super`, or the method is not overridden. ## The remote refusal set, and the proof that it is complete Tree: `origin/main` `adbbc5d01e` (`packages/drivers/driver-turso/src`, excluding `*.test.ts` and `*.testkit.ts`). - `git grep -n "status = 501"` → **8 hits**: `turso-driver.ts` 5 (`refuseRemoteAutonumber`, `refuseRemoteTransaction`, `refuseRemoteDeferredDdl`, `refuseRemoteDriftDetection`, `refuseRemoteMediaColumnMove`) and `remote-transport.ts` 3 (`uncompilableAggregateFunctionError`, `refusePerAggregationFilter`, `refuseDateBucketedGroupBy`). - `git grep -c "StandardErrorCode.enum.NOT_IMPLEMENTED"` → the same 5 + 3. A census of every `StandardErrorCode.enum.X` in the package's source finds no other 501 route: INVALID_FILTER 1, INVALID_QUERY 2, NOT_IMPLEMENTED 8, TIMEOUT 1, VALIDATION_ERROR 4, plus the unsafe-identifier constants `INVALID_REQUEST` / 400. - A wider spelling sweep (`status: 501`, a bare `501` in code position) finds only 2 hits, both in docblock prose. - Positive control: the same `status = 501` grep over `packages/drivers/driver-sql/src` finds its 3 known producers. The built bundle `dist/index.js`, built at `031e3a45e`, carries exactly 8 `status = 501`. - `driver-sql`'s own three 501 producers are reachable only from `SqlDriver.aggregate`, through `mapAggregateFunc`, whose only caller is that method. Remote `TursoDriver.aggregate` never calls `super.aggregate`, so none of them reaches the remote face. - **Not listed, on purpose:** `uncompilableAggregateFunctionError` cannot fire today. `DECLARED_AGGREGATE_FUNCTIONS` (spec `AggregationFunction`: count, sum, avg, min, max, count_distinct) equals `REMOTE_AGGREGATE_FUNCTIONS` key for key, so the declared-but-uncompiled class is empty (`remote-transport-aggregate-function-refusal.test.ts` asserts that). Listing it would name a refusal the code never raises. ## Local verification (all at `031e3a45e`) - **Tests that read this README: 0.** `git grep` for `driver-turso/README` across tests and scripts finds only `package.json` `files`. No test file names the README. - **Build:** `pnpm --workspace-concurrency=2 --filter '@objectstack/driver-turso...' build`, run under the verify lock, ended `VERDICT command-exit 0`. It was needed for the dist-reading gates and the probes. - **The refusal pins the new list draws on, re-run at this head:** 8 files (`turso-remote-transaction-refusal`, `turso-remote-autonumber-refusal`, `turso-remote-deferred-ddl`, `turso-remote-drift-detection-refusal`, `turso-remote-media-column-move-refusal`, `remote-transport-groupby-node`, `remote-transport-aggregate-function-refusal`, `turso-transactions-unsupported-declaration`). Result: `Test Files 8 passed (8)`, `Tests 117 passed (117)`. - **Gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 51 commands at `031e3a45e`: 41 before the changeset existed, plus 10 changeset families after. I ran all 51 and recorded each exit code. Reconciled with `--ran`: "51 derived famil(ies) accounted for — 49 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3)". 49 exited 0, including `check:nul-bytes`, `check:doc-authoring`, `check:issue-citations`, `check:published-files`, `check:dts-closure`, `check:driver-conformance`, `check-changeset-no-major`, `check-empty-changeset` and `check-adr-0087-registration`. - NOT MEASURED: `check:dual-build-cjs-loads`, reason: PREREQUISITE NOT MET (exit 3). It loads every built entry across 78 packages, and only the driver-turso closure is built here. - NOT MEASURED: `check:lean-entry-closure`, reason: PREREQUISITE NOT MET (exit 3). It needs `packages/objectql/dist`. - Neither gate reads a README or a changeset. CI runs both on a full build. - `node scripts/check-issue-citations.mjs --base adbbc5d` → exit 0: "no issue citations added against adbbc5d (0 file(s) read)". - `node scripts/check-published-readme-links.mjs` → exit 0: "178 outbound link(s) across 61 published markdown file(s)". - `node scripts/check-published-readme-exports.mjs` → exit 3, PREREQUISITE NOT MET (39 unbuilt packages). NOT MEASURED. This diff adds no `import` line to any README code block (0 added lines match `import`). ## Changeset `.changeset/20016-turso-readme-remote-refusals.md`, `"@objectstack/driver-turso": patch`. The rule applied: `.claude/agents/os-dev.md` Definition of done, "发布面动了要 changeset;`skip-changeset` 判据是没动:已发布 = 各包 `files[]` 实际发运内容". `README.md` is in `@objectstack/driver-turso`'s `files` (`["dist", "README.md", "CHANGELOG.md"]`), so it ships, which is route 1 of the Check Changeset job ("It releases something -> run 'pnpm changeset'"). Route 2's "docs/" means the repository's `docs/` tree, not a packaged README. Repo precedent: the README-only `patch` changeset in `a90272ae6` (`@objectstack/metadata` README). The level is `patch` because no public surface moves. ## In-flight overlap PR #20037 (head `5fb4b25b12`, draft) edits `remote-transport.ts`. Its patch changes the filter-refusal doors (`nonListCombinator`, `nonBooleanNullComparand`, `nonBooleanExistsComparand`, `unsupportedOperator` gain a provenance argument). A grep of that patch for `NOT_IMPLEMENTED`, `501` or `StandardErrorCode` finds no changed line. It touches none of the 501 builders and not `aggregate`, so no sentence written here goes false when it lands. ## Out-of-scope findings (for the seat to file; not fixed here) 1. **Class (a): remote mode cannot be constructed without `better-sqlite3`, which the README and the package's own manifest say it does not need.** Probe (end to end, `dist/index.js` built at `031e3a45e`): make `better-sqlite3` unresolvable (`Module._resolveFilename` throws `MODULE_NOT_FOUND` for it), then call `new TursoDriver({ url: 'libsql://probe-db.example.turso.io', authToken: 'x' })`. It throws "Knex: run / $ npm install better-sqlite3 --save". Control: the same construction with the module resolvable returns `transportMode=remote`. Mechanism: the remote arm of `TursoDriver.toKnexConfig` returns `client: 'better-sqlite3'` with a `connection`, then `SqlDriver`'s constructor calls `knex(...)`, whose `Client` constructor calls `initializeDriver()` → `require('better-sqlite3')` (knex 3.3.0 `lib/client.js`). Shipped face: the README's "Dependencies by Mode" row for Remote, "better-sqlite3 is NOT required", "you don't need to install it", the Remote (Cloud-Only) section's "no local SQLite needed", and `package.json` `peerDependenciesMeta.better-sqlite3.optional: true`. Following the Vercel/Edge instructions verbatim ends in the throw above. Dedupe words: `turso remote better-sqlite3 required` · `toKnexConfig remote better-sqlite3` · `driver-turso optional peer better-sqlite3 remote`. 2. **Class (a): the inherited `SqlDriver` methods that `TursoDriver` does not override answer from the placeholder `:memory:` Knex database in remote mode.** Probe (`dist` at `031e3a45e`; remote = `mode: 'remote'` over a libsql `file:` client; control = local `:memory:` face, same two rows): - `introspectSchema()`: remote `[]` (silently empty, although `probe_t` exists and `find` returns 2 rows); local `["probe_t"]`. - `distinct('probe_t','name')`: remote throws `DATABASE_ERROR`/500 ("no such table: probe_t" kept server-side); local `["x","y"]`. - `findWithWindowFunctions`: remote throws a raw `SQLITE_ERROR` with no envelope; local returns 2 rows. - This is the same placeholder shape that `detectManagedDrift` and `planMediaColumnMove` were refused for. The other un-overridden public async methods are `analyzeQuery`, `explain`, `applyMigrationEntries`, `previewDeferredSchemaWork`, `flushDeferredSchemaDdl`, `reclaimSpace`, `rotateShards`, `commitTransaction` and `rollbackTransaction` (not probed). - Dedupe words: `turso remote introspectSchema empty` · `turso remote inherited placeholder knex` · `turso remote distinct no such table`. ## Acceptance notes - **Capabilities table, not changed.** Its header says "TursoDriver declares enhanced capabilities beyond the base SqlDriver". The `supports` getter in `turso-driver.ts` records that those bits (`savepoints`, `queryCTE`, `fullTextSearch`, `jsonQuery`, `connectionPooling`) were retired from `DriverCapabilities`. The table reads as libSQL SQL features reachable through raw `execute`, and I did not measure those over a remote client. Its remote "Savepoints ✅" cell has no driver path, because the driver's savepoints are Knex nested transactions and remote refuses transactions. I left the table alone rather than assert an unmeasured ❌. Carrier: none. - "Ideal for Vercel, Cloudflare Workers …" is not measured. For Vercel/Edge it depends on finding 1. - The probe process in finding 2 did not exit after `disconnect()` and was killed by PID. I did not isolate the cause, and it is not claimed as a finding. --- _Generated by [Claude Code](https://claude.ai/code/session_01Bvd69VPa6puiNzzPUroDBx)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 980bc05 commit 3557f85

2 files changed

Lines changed: 46 additions & 5 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@objectstack/driver-turso": patch
3+
---
4+
5+
`README.md` — the remote branch of the architecture tree no longer lists `beginTransaction`, `commit` and `rollback` as `RemoteTransport` operations. `RemoteTransport` has no transaction methods, and remote mode refuses all three with `NOT_IMPLEMENTED` / 501. A new "What remote mode refuses" section lists every `NOT_IMPLEMENTED` / 501 the remote face raises: transactions (including `options.transaction` passed to the remote data and schema methods), a record number for an empty `autonumber` field on `create`, `bulkCreate` and an `upsert` with no `id`, `_id` or `conflictKeys`, `setDeferredDdl(true)`, `detectManagedDrift()`, `planMediaColumnMove()`, and two `aggregate()` shapes that `engine.aggregate()` computes in memory instead (a `groupBy` entry with a `dateGranularity`, an `aggregations` entry with a non-empty `filter`).
6+
7+
Also corrected: the remote-mode bullet no longer says every operation is delegated to `RemoteTransport`, the remote example no longer says every CRUD call works as in local mode, and the local branch no longer lists array-style filters, which the driver refuses.
8+
9+
- **No behaviour moves.** The driver's source and every published export are byte-identical; only the README text shipped in this package's `files[]` changes.

‎packages/drivers/driver-turso/README.md‎

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Turso/libSQL driver for ObjectStack — edge-first SQLite with embedded replicas
77
`TursoDriver` implements a **dual-transport architecture**:
88

99
- **Local/Replica modes:** Extends `SqlDriver` from `@objectstack/driver-sql`. All CRUD, schema, filtering, aggregation, window functions, introspection, and transactions are **inherited** via Knex + better-sqlite3.
10-
- **Remote mode:** Delegates all operations to `RemoteTransport` which uses `@libsql/client` SDK directly (HTTP/WebSocket). No local SQLite or Knex dependency needed.
10+
- **Remote mode:** Sends CRUD, bulk writes, `aggregate`, raw `execute`, schema sync and `dropTable` to `RemoteTransport`, which uses the `@libsql/client` SDK directly (HTTP/WebSocket). Transactions and the other calls listed under [What remote mode refuses](#what-remote-mode-refuses) are refused instead.
1111

1212
```
1313
TursoDriver extends SqlDriver (dual transport)
@@ -17,13 +17,14 @@ TursoDriver extends SqlDriver (dual transport)
1717
│ ├── Inherited: syncSchema, dropTable, introspectSchema
1818
│ ├── Inherited: aggregate, distinct, findWithWindowFunctions
1919
│ ├── Inherited: beginTransaction, commit, rollback
20-
│ └── Inherited: applyFilters (MongoDB-style + array-style)
20+
│ └── Inherited: applyFilters (MongoDB-style)
2121
├── Transport: remote (via @libsql/client)
2222
│ ├── RemoteTransport: find, findOne, create, update, delete, count, upsert
2323
│ ├── RemoteTransport: bulkCreate, bulkUpdate, bulkDelete, updateMany, deleteMany
2424
│ ├── RemoteTransport: syncSchema, dropTable
25-
│ ├── RemoteTransport: beginTransaction, commit, rollback
26-
│ └── RemoteTransport: execute (raw SQL)
25+
│ ├── RemoteTransport: execute (raw SQL)
26+
│ └── Refused, NOT_IMPLEMENTED / 501: beginTransaction, commit, rollback,
27+
│ setDeferredDdl(true), detectManagedDrift, planMediaColumnMove
2728
├── Override: name, version, supports (Turso-specific capabilities)
2829
├── Override: connect / disconnect (transport-aware lifecycle)
2930
├── Added: transportMode ('local' | 'replica' | 'remote')
@@ -114,10 +115,41 @@ const driver = new TursoDriver({
114115
});
115116
await driver.connect();
116117

117-
// All CRUD operations work the same as local mode
118+
// The CRUD methods are the same as in local mode; some calls are refused (below)
118119
const users = await driver.find('users', { where: { active: true } });
119120
```
120121

122+
#### What remote mode refuses
123+
124+
Each call below is refused in remote mode with `code: 'NOT_IMPLEMENTED'` and
125+
`status: 501`: the call is valid, and the remote transport does not have the
126+
capability. The check runs before the call reads or writes anything.
127+
128+
| Operation | Refused call | Use instead |
129+
|:---|:---|:---|
130+
| Transactions | `beginTransaction()`, `commit()`, `rollback()`, and `options.transaction` passed to any `RemoteTransport` method in the tree above, to `aggregate()` or to `syncSchemasBatch()` | The local or embedded-replica transport, which run Knex transactions and honour `options.transaction` |
131+
| Record numbers | `create()`, `bulkCreate()`, and an `upsert()` with no `id`, `_id` or `conflictKeys`, when a row leaves an `autonumber` field empty (`undefined`, `null` or `''`) | The local or embedded-replica transport, which generate record numbers, or a value you supply, which is written unchanged |
132+
| Deferring schema DDL | `setDeferredDdl(true)`, which `os migrate plan` calls. `setDeferredDdl(false)` is accepted | Run the command against a local SQLite copy of the database (a `file:` URL) |
133+
| Schema drift detection | `detectManagedDrift()` | `os migrate plan` against a local SQLite copy of the database (a `file:` URL) |
134+
| Planning the ADR-0104 media column move | `planMediaColumnMove()`, the column step of `os migrate files-to-references` | The local or embedded-replica transport, which plan it |
135+
136+
- **Transactions.** Remote mode declares `supports.transactionsUnsupported: true`.
137+
When the remote driver is the engine's default datasource, `engine.transaction()`
138+
reads that and does not call `beginTransaction()`: it runs the callback with
139+
no transaction and logs a warning once per datasource, or, called with
140+
`require: true`, throws `TransactionUnsupportedError` before the callback runs.
141+
- **Record numbers.** Remote mode never generates one. The check uses the
142+
`autonumber` fields this driver recorded when it synced the object's schema
143+
(`syncSchema`, `syncSchemasBatch` or `initObjects`). An `upsert()` that
144+
carries an `id`, `_id` or `conflictKeys` is not refused, because it may merge
145+
into an existing row. If it inserts instead, the row is written with the field
146+
empty and the driver logs a warning naming the row.
147+
- **Aggregation.** `aggregate()` called on the driver directly also refuses,
148+
with the same code, a `groupBy` entry that has a `dateGranularity`, and an
149+
`aggregations` entry with a non-empty `filter`. `engine.aggregate()` never
150+
sends either one to this driver: it fetches the rows and computes both in
151+
memory. The local transport also refuses a per-aggregation `filter`.
152+
121153
### Auto-Detection
122154

123155
Transport mode is automatically detected from the URL:

0 commit comments

Comments
 (0)