Repository navigation
Commit 3557f85
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
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
10 | | - | |
| 10 | + | |
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| |||
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
20 | | - | |
| 20 | + | |
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
24 | 24 | | |
25 | | - | |
26 | | - | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
27 | 28 | | |
28 | 29 | | |
29 | 30 | | |
| |||
114 | 115 | | |
115 | 116 | | |
116 | 117 | | |
117 | | - | |
| 118 | + | |
118 | 119 | | |
119 | 120 | | |
120 | 121 | | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
121 | 153 | | |
122 | 154 | | |
123 | 155 | | |
| |||
0 commit comments