Skip to content

Commit 5ba2ec3

Browse files
os-litantclaude
andauthored
feat(spec,objectql,driver-sql,driver-turso): a transport can declare it has no transactions, and the engine gates on the declaration (#18890)
Fixes #18063 Clause-②: yes Governing text: maintainer decision batch **#148 item 3, letter B**, 「同意」 2026-09-17T14:26Z (issue comment 5716042163). It supersedes batch #133's route C. Quoted verbatim and untranslated, because the spelling delegation is the part this PR had to execute: > `packages/spec`: the driver contract gains a way for a transport to **declare 「no transactions」** (the dev picks the smallest spelling the existing capability/contract surface already has — a capability bit is preferred over a new key), and the engine's transaction gating reads the declaration instead of method presence (`driver.zod.ts:266` re-keyed). **Notation.** This body writes generic types bracket-free — `Promise[Knex.Transaction]` means the declaration `SqlDriver` publishes. That is a spelling choice against body sanitization, not a different type. --- ## What landed 1. **`packages/spec`** — `DriverCapabilities` gains one live bit, `transactionsUnsupported`, plus the predicate that reads it, `driverSupportsTransactions()`, exported from `@objectstack/spec/data`. 2. **`packages/objectql` and `packages/core`** — ⚠️ **corrected by the seat: all FOUR transaction gates**, not three. They read that predicate instead of `typeof driver.beginTransaction`: `ObjectQL.transaction`, `ScopedContext.transaction`, `ScopedContext.txDriver()` behind the discrete begin/commit/rollback trio, and **`engineCanRollBack`** — the ADR-0119 D4 gate that metadata-protocol's atomic `batchData` / `updateManyData` / `deleteManyData` and `runMigrationJournal` share. The degrade warning now says WHICH of the two reasons it fired for. ⭐ **The fourth gate was found by the at-tier review and is why this count changed.** With only three re-keyed, the gates DISAGREED: `driverSupportsTransactions` said false while `engineCanRollBack` still read method presence and said true — and the D1 degrade swallowed the driver's refusal. Measured on the real chain: an atomic `batchData` returned a 「rollback」 with **one record persisted** and `begins = 0`, against a lit control (same double, bit removed) that threw 501 with 0 rows; the migration runner ran to `completed` and wrote the `chunk_done` marker its own header says 「would not mean committed」. ⇒ this PR briefly re-opened the 「rollback does not roll back」 defect one layer up. Fixed, with a pin that fails without it — and ⚠️ the pre-existing pin stayed GREEN under that ablation, which is why it never caught this. 3. **`driver-sql`** — `SqlDriver.supports` spells `transactionsUnsupported: false`. `SqlDriver.beginTransaction()` keeps its narrow `Promise[Knex.Transaction]`; nothing in the base was widened. 4. **`driver-turso`** — the remote face declares the bit; `TursoDriver.beginTransaction()` publishes the inherited declaration instead of `Promise[any]`; `RemoteTransport` loses its three decorative transaction members. --- ## The spelling, priced — because the ruling's preferred spelling points at a tombstone `driver.zod.ts:266` is the prescription line of a **retired-key tombstone**: `transactions` was removed in `@objectstack/spec` 17.0.0 under ADR-0049 enforce-or-remove, and `savepoints` / `isolationLevels` beside it are the same retired family. Three spellings were priced before one was chosen. **(a) Revive the name `transactions`.** Rejected, and the cost is measurable rather than aesthetic: - It needs **`packages/spec/src/migrations/registry.ts`** edited — the D3 entry `driver-capabilities-inert-bits-removed` names `data.DriverCapabilities.transactions` in its `surface` list and states the count ("of 34 declared bits, THREE have a decision-making reader … THIRTY-ONE were written by every driver and read by nothing") in its `reason` and `acceptanceCriteria`. That file is MIXED and deliberately not routed to the os-regen merge driver, so it is the one file in this area that a merge cannot resolve mechanically. **The chosen spelling touches it zero times** (verified: `git diff origin/main..HEAD` does not name it). - It inverts the record's own convention. Every optional bit here means `false` when absent; a revived `transactions` must mean "yes, transactions" when absent, or every existing driver silently loses them on upgrade. That is a tri-state boolean in a record where nothing else is one. - It converts a documented refusal into silent acceptance of a value whose **meaning changed underneath it**. The old bit claimed "I support transactions" and nothing read it; the new declaration must express "I have none", and it is load-bearing. An old inert value becoming load-bearing with the opposite sense is the ADR-0104 silent-strip class one level up — the class the tombstones exist to prevent. - It makes the tombstone's own published text false. That text ("no code in any repository ever read it, so its value never changed which code path ran") is what an upgrading author actually reads. - It also costs the pins that hold the retired set: `RETIRED_BITS` in `driver.test.ts`, the prescription case, the 31-tombstone counts in two docblocks. **(b) A new key outside `supports`** — dispreferred by the ruling by name, and larger: a second place to look for one fact. **(c) A new inverse-polarity bit on the live `DriverCapabilities` record.** **Chosen.** It keeps `absence = false`, leaves the tombstone true and refusing, touches neither MIXED file, and costs one key plus its reader. ⭐ **The ruling's stated preference and the tombstone do not conflict, and that is the finding worth stating plainly.** "A capability bit is preferred over a new key" asks for a bit on the existing `DriverCapabilities` record — it does not ask for the retired NAME back. Spelling (c) satisfies the preference in full while the tombstone stays exactly as published. No seat question is escalated here because there is no fork to escalate. ### Why adding a bit SATISFIES enforce-or-remove rather than reversing it The 17.0.0 audit removed thirty-one bits for one stated reason: **no code anywhere read them.** It kept the three where method presence provably cannot carry the signal. Ruling B's entire content is the creation of the missing reader. The bit arrives **with** the engine dispatch that consumes it, in the same change — the honest order the ADR asks for — and the record's own docblock now states that bar for the next author. ### Why method presence could not carry this `TursoDriver extends SqlDriver`, whose `beginTransaction()` opens a real knex transaction, so the inherited method reported the libSQL REMOTE transport as transactional. It is not: `RemoteTransport`'s data methods take **no `options` argument at all**, so a handle cannot reach the statement that would have to join it. **A subclass cannot opt out of a door it did not open.** This is the exact mirror of `batchSchemaSync`, which exists because a subclass can inherit `syncSchemasBatch` from a base whose transport batches while its own cannot — and which the engine likewise ANDs with method presence. --- ## Premise check: part of this card was consumed while it sat in the box Reported rather than quietly absorbed, because it changes what clause 3 still owed. `62bce5c297d` — "refuse transactions on the remote face instead of silently dropping them (PR #18717)", 2026-09-17T17:13:06Z, four hours after the ruling — landed the **driver-level** loud refusal for card #18616: `TursoDriver` refuses `beginTransaction()` / `commit()` / `rollback()` and any `options.transaction` on the remote arm. That card is already `completed`; this PR does not re-open or re-decide it, and deliberately does not add a second, engine-level `options.transaction` refusal beside the driver's — a second mechanism for zero additional drivers is the shape this whole card is about. What that leaves for this PR is the half nothing had built, and it is load-bearing: ⭐ **The refusal's own remedy was unreachable.** `refuseRemoteTransaction`'s message tells callers to "take the non-transactional path deliberately: `engine.transaction()` without `require: true` on a datasource whose driver has no transactions runs the callback with no rollback and says so (ADR-0119 D1)". With the gate reading method presence, that path could never be taken for this driver — the method is there, so the engine opened a transaction and the callback got a 501 out of `beginTransaction()` instead of the declared degrade. **The message made a promise only this change can keep.** Two more premise readings, both against `origin/main`: - `RemoteTransport`'s three transaction members were **unreachable** once the driver refused: `remoteTransport.beginTransaction|commit|rollback` — **0** call sites repo-wide, against a lit control of **7** lines calling other members of the same field. They are deleted here. - `TursoDriver.beginTransaction()`'s `any` dissolves without paying either price #17690 priced. `refuseRemoteTransaction` returns `never`, so the remote branch is assignable to any declared return type and the only arm that still returns is `super.beginTransaction()`. The override republishes the base's type. **It is spelled `ReturnType[SqlDriver['beginTransaction']]` and not the knex type directly, because `knex` is not a dependency of `driver-turso`** (`check:undeclared-dep-imports`; the same constraint the doors suite's `KnexSlice` works around) — and deriving it from the base is the stronger pin. ⚠️ **#17690 could not be read** — it, #17876 and #17878 all answer 404 (the `os-musk` account is deactivated). Lit control: #18063 and #18116 read fine through the same instrument in the same round, so the 404s are a reading. Clause 4 of the ruling — "`SqlDriver.beginTransaction` keeps its narrow `Promise[Knex.Transaction]` (the honest narrowing #17690 protected)" — is therefore treated as **the governing restatement**. No claim is made here about #17690's original text. --- ## Behaviour change for a caller On a datasource whose driver declares the bit, `engine.transaction()` takes the DECLARED non-transactional path (ADR-0119 D1) instead of opening a transaction it cannot honour: - without `require`: the callback runs with no transaction, `owned: false`, and the degrade warns **once per datasource** — naming the declaration, not a missing method, because sending an operator to look for a method this class publishes wastes the report; - with `require: true`: `TransactionUnsupportedError` **before the callback writes anything**; - `ScopedContext.transaction` answers identically, and the discrete trio's `begin` returns `null`. Every one of those is the answer a driver with no `beginTransaction` already received. Nothing that worked stops working — which is why the changesets are **minor**: the remote transport never honoured a transaction, so no working behaviour is withdrawn (the ruling's own stated ground, and the ground on which `RemoteTransport`'s three published members are removed at minor). --- ## Verification All readings on the merged tree, `0c6eeea0f6f`, against `origin/main` `0b31d90fb37`. Every exit code captured by redirect, never through a pipe. | run | result | |:--|:--| | build closure (`turbo run build`, driver-turso + objectql closures) | **exit 0** — 15/15 tasks | | typecheck — spec, objectql, driver-sql, driver-turso | **exit 0** — 17/17 tasks | | `@objectstack/spec check:generated` | **exit 0** — all 15 artifacts current | | `spec` `src/data/driver.test.ts` | **58 passed** | | `objectql` — 6 transaction suites | **66 passed** | | `driver-turso` `pnpm test` (whole package) | **1282 passed / 55 files** | | `driver-sql` `pnpm test` (whole package) | **2627 passed, 168 skipped** | | `pnpm lint` (`eslint . --no-inline-config`, whole repo) | **exit 0** — complete population, no narrowing claimed | | 24 further gate families run locally | **all exit 0** | `pnpm check:type-check-debt` returned **exit 3, `PREREQUISITE NOT MET`** — it refuses to measure without the whole workspace built, which is a farm-scale build. Recorded as **NOT MEASURED**, ⛔ not as a pass and ⛔ not as a red. The rest of the derived gate roster is CI's run. ### Reverse verification — two legs, both dist-aware **Leg A — the spec predicate** (`objectql` resolves `@objectstack/spec` through `exports`, i.e. `dist/`, per the `KNOWN_UNALIASED_TEST_IMPORTS` ledger, so the mutation had to reach `dist/` to mean anything): | | reading | |:--|:--| | `driver.zod.ts` blob at HEAD | `63ab873394848c025325ac234e8bd62b361c9ce0` | | blob after mutation (declaration clause deleted) | `481e373c37ec774e03b6c127b6ceb6d8527c03d1` — differs, so it reached disk | | rebuild, then `ablation-dist-preflight @objectstack/spec … --absent` | **exit 0** — the guard is gone from `dist/` | | `engine-transaction-declared-unsupported.test.ts` | **8 failed / 8** | | `spec` `driver.test.ts` | **1 failed / 58** — only the predicate case, as predicted | | restore (`git checkout HEAD -- …`), `git diff HEAD` | empty; blob back to `63ab873…` | | rebuild, preflight (present) | **exit 0** | | re-run | **8 passed / 8** | **Leg B — the driver declaration** (same-package source resolution, no build in the path): | | reading | |:--|:--| | `turso-driver.ts` blob at HEAD | `bb55757e6025d55d58babbbe0c090eefa4afa001` | | blob after mutation (`transactionsUnsupported: false`) | `2bfb64a95287d3145dea4d36fd75cad23868f3e1` — reached disk; injected marker observed on disk | | declaration suite + capability census pin | **2 failed / 102** | | restore | blob identical to HEAD, `git diff HEAD` empty | | re-run | **102 passed / 102** | Predicted direction was RED, and RED is what both legs produced. Both scripts carried `trap … EXIT INT TERM` with absolute paths; both restores are proven by blob identity and an empty `git diff HEAD`, not by an exit code. ⚠️ One instrument error, reported rather than dropped: leg B's two `*_SRC_COUNT` echo lines were mis-quoted inside a quoted heredoc, so `grep` read the pattern's tail as extra filenames and printed a prefixed `0`. Those two lines are **void**, not readings. The on-disk proof does not rest on them — it rests on the anchor assertion (the pre-mutation text had to occur exactly once or the script aborted), the injected marker counted on disk, and the two blob hashes. ### Merge `origin/main` was merged after PR #18704 landed, through `scripts/pm/os-regen-merge.sh` — merge committed first, regeneration afterwards, never during (a `gen:schema` run in MERGE state rolls the authorable-surface anchor back to the old fork point). Three os-regen artifacts were regenerated from the merged tree. Asserted afterwards: **zero** lines present in `origin/main`'s `api-surface/data.json`, `authorable-surface/data.json` or `export-origins/data.json` are absent from the regenerated files, with the lit control firing (the single addition is `driverSupportsTransactions (function)`). Neither MIXED file — `dropped-refinements.baseline.json`, `packages/spec/src/migrations/registry.ts` — is touched by this branch at all. --- ## Acceptance notes Out-of-scope observations, noted and deliberately not acted on here: - `packages/spec/src/data/driver.zod.ts` still carries the retired `savepoints` and `isolationLevels` beside `transactions`; both remain correctly retired under this change and neither gained a reader. Noted, not filed. - `packages/objectql/src/engine.ts` has a third comment (near the `batchData` observability path) that cites `warnTransactionUnsupported` as its model; it is prose, still accurate, and left alone. Noted, not filed. - The `turso-driver-doors-declared-types.test.ts` header still quotes a TS2416 coordinate (`src/turso-driver.ts(1662,18)`) that has drifted by landings. The receipt's substance reproduces; only the coordinate is stale, and it is kept verbatim as the historical error text. Noted, not filed. --- _Generated by [Claude Code](https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho)_ --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 67624b7 commit 5ba2ec3

22 files changed

Lines changed: 835 additions & 107 deletions
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/core": minor
4+
"@objectstack/objectql": minor
5+
"@objectstack/driver-sql": minor
6+
"@objectstack/driver-turso": minor
7+
---
8+
9+
feat(spec,core,objectql,driver-sql,driver-turso): a transport can declare it has no transactions, and every transaction gate reads the declaration instead of method presence (#18063)
10+
11+
Maintainer ruling, decision batch #148 item 3, letter B, 「同意」 2026-09-17, verbatim and untranslated:
12+
13+
> `packages/spec`: the driver contract gains a way for a transport to **declare 「no transactions」** (the dev picks the smallest spelling the existing capability/contract surface already has — a capability bit is preferred over a new key), and the engine's transaction gating reads the declaration instead of method presence.
14+
15+
**`DriverCapabilities` gains one live bit, `transactionsUnsupported`.** A transport sets it to say that a handle it issued would be a FALSE SUCCESS rather than a missing feature: the caller gets a handle, the writes execute and are already durable, `rollback()` resolves and undoes nothing. Absence means `false`, exactly like `batchSchemaSync`, so a driver that declares nothing keeps the behaviour it has today.
16+
17+
**⛔ This is not `DriverCapabilities.transactions` un-retired, and the difference is not cosmetic.** That key was tombstoned in 17.0.0 under ADR-0049 enforce-or-remove and STAYS tombstoned — writing it is still a compile error and still a parse refusal carrying its prescription. It claimed "I support transactions" and nothing read it; this one declares "my transport cannot honour one" and the engine dispatches on it. Reviving the name would have inverted the record's own `absence = false` convention into a tri-state, turned a documented refusal into silent acceptance of a value whose meaning had changed underneath it, and made the tombstone's published text ("no code in any repository ever read it") false. A new key costs one bit; the name costs all of that.
18+
19+
**Adding a bit to a record enforce-or-remove has pruned SATISFIES that ADR rather than reversing it.** The audit removed thirty-one bits for one stated reason — no code anywhere read them — and kept the three where method presence provably cannot carry the signal. This change is the creation of the missing reader: `driverSupportsTransactions()` (exported from `@objectstack/spec`) is the one definition of the gate, and all FOUR places that used to spell `typeof driver.beginTransaction === 'function'` ask it — `ObjectQL.transaction()`, `ScopedContext.transaction`, the `ScopedContext` begin/commit/rollback trio, and `@objectstack/core`'s `engineCanRollBack`. The bit arrives WITH its reader, in the same change, which is the honest order the ADR asks for.
20+
21+
**Why method presence could not carry it.** `TursoDriver extends SqlDriver`, whose `beginTransaction()` opens a real knex transaction, so the inherited method reported the libSQL REMOTE transport as transactional. It is not — `RemoteTransport`'s data methods take no `options` argument at all, so a handle cannot reach the statement that would have to join it. A subclass cannot opt out of a door it did not open. This is the mirror of `batchSchemaSync`, which exists because a subclass can inherit `syncSchemasBatch` from a base whose transport batches while its own cannot.
22+
23+
**What changes for a caller.** On a datasource whose driver declares the bit, `engine.transaction()` now takes the DECLARED non-transactional path (ADR-0119 D1) instead of opening a transaction it cannot honour: the degrade warns once per datasource — naming the declaration, not a missing method — and `{ require: true }` throws `TransactionUnsupportedError` before the callback writes anything. `ScopedContext.transaction` and the discrete begin/commit/rollback trio read the same predicate; the trio's `begin` returns `null`. Both are the answers a driver with no `beginTransaction` already received.
24+
25+
**`driver-turso`.** The remote face declares `transactionsUnsupported: true`; local and embedded-replica inherit `false` from the base and are untouched. `TursoDriver.beginTransaction()` publishes the inherited declaration instead of `Promise<any>` — the annotation the earlier `any` was masking an LSP violation to avoid, dissolved rather than widened: the remote arm returns `never` (it refuses), so the only arm that still returns is the base's. `SqlDriver.beginTransaction()` keeps its narrow `Promise<Knex.Transaction>`; nothing in the base was widened.
26+
27+
**`@objectstack/core`.** `engineCanRollBack()` — the ADR-0119 D4 gate that `@objectstack/metadata-protocol` uses for `batchData` / `updateManyData` / `deleteManyData` under `options.atomic`, and that `runMigrationJournal()` uses to decide whether to start at all — reads the same predicate. It has to: it does not open the transaction itself, it vouches that `engine.transaction()` will, and on a driver that declares the bit the engine now takes its non-transactional path. A gate still reading method presence would vouch for a runtime that is about to run the callback with no transaction, so the atomic batch would answer `rollback` over writes that stayed on disk and the journal would write `chunk_done` rows its own contract says mean "committed". What a caller sees on such a datasource instead: `batchData({ atomic: true })` refuses with `501 NOT_IMPLEMENTED` — retry without `atomic`, or probe `capabilities.transactionalBatch` on `/discovery` first — and `runMigrationJournal()` refuses with `MigrationJournalRefusal('NOT_IMPLEMENTED')` before writing a single journal row. Both are the answers a driver with no `beginTransaction` already received.
28+
29+
**`RemoteTransport` loses `beginTransaction()`, `commit()` and `rollback()`.** They are a published surface, and this is **minor** rather than major on the ruling's own stated ground: that transport never honoured a transaction, so no working behaviour is withdrawn. They had already become unreachable from every caller in the repository when the driver started refusing them; they are now gone, and the declaration keeps them gone by design rather than by audit.

content/docs/references/data/driver-nosql.mdx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ const result = AggregationPipelineSchema.parse(data);
140140
| :--- | :--- | :--- | :--- |
141141
| **name** | `string` || Driver instance name |
142142
| **type** | `'nosql'` || Driver type must be "nosql" |
143-
| **capabilities** | `{ queryDateGranularity?: Record<string, boolean>; autonumber?: boolean; batchSchemaSync?: boolean }` || Driver capability flags |
143+
| **capabilities** | `{ queryDateGranularity?: Record<string, boolean>; autonumber?: boolean; batchSchemaSync?: boolean; transactionsUnsupported?: boolean }` || Driver capability flags |
144144
| **connectionString** | `string` | optional | Database connection string (driver-specific format) |
145145
| **poolConfig** | `{ min: number; max: number; idleTimeoutMillis: number; connectionTimeoutMillis: number }` | optional | Connection pool configuration |
146146
| **databaseType** | `Enum<'mongodb' \| 'couchdb' \| 'dynamodb' \| 'cassandra' \| 'redis' \| 'elasticsearch' \| 'neo4j' \| 'orientdb'>` || Specific NoSQL database type |
@@ -163,14 +163,15 @@ const result = AggregationPipelineSchema.parse(data);
163163
| **queryDateGranularity** | `Record<string, boolean>` | optional | Per-granularity native date bucketing (day/week/month/quarter/year). Missing keys fall back to in-memory bucketing. |
164164
| **autonumber** | `boolean` | optional | Driver natively generates persistent autonumber/sequence values |
165165
| **batchSchemaSync** | `boolean` | optional | Supports batched schema sync to reduce schema DDL round-trips (absence = false) |
166+
| **transactionsUnsupported** | `boolean` | optional | Transport cannot honour transactions even though `beginTransaction` is inherited (absence = false) |
166167
| **create** | `never` | optional | [REMOVED] `DriverCapabilities.create` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. CRUD is not optional for a driver: `create`/`find`/`findOne`/`update`/`delete` are REQUIRED `IDataDriver` methods and the engine calls them unconditionally. Delete the key. |
167168
| **read** | `never` | optional | [REMOVED] `DriverCapabilities.read` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. CRUD is not optional for a driver: reads go through the REQUIRED `find`/`findOne`/`count` methods, called unconditionally. Delete the key. |
168169
| **update** | `never` | optional | [REMOVED] `DriverCapabilities.update` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. CRUD is not optional for a driver: `update`/`upsert` are REQUIRED `IDataDriver` methods, called unconditionally. Delete the key. |
169170
| **delete** | `never` | optional | [REMOVED] `DriverCapabilities.delete` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. CRUD is not optional for a driver: `delete` is a REQUIRED `IDataDriver` method, called unconditionally. Delete the key. |
170171
| **bulkCreate** | `never` | optional | [REMOVED] `DriverCapabilities.bulkCreate` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. The bulk methods (`bulkCreate`/`bulkUpdate`/`bulkDelete`) are REQUIRED `IDataDriver` methods and the engine calls them directly; wire-level batch capability is advertised by REST discovery from the live composition (#3298), never from this record. Delete the key. |
171172
| **bulkUpdate** | `never` | optional | [REMOVED] `DriverCapabilities.bulkUpdate` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. The bulk methods are REQUIRED `IDataDriver` methods and the engine calls them directly; wire-level batch capability is advertised by REST discovery from the live composition (#3298), never from this record. Delete the key. |
172173
| **bulkDelete** | `never` | optional | [REMOVED] `DriverCapabilities.bulkDelete` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. The bulk methods are REQUIRED `IDataDriver` methods and the engine calls them directly; wire-level batch capability is advertised by REST discovery from the live composition (#3298), never from this record. Delete the key. |
173-
| **transactions** | `never` | optional | [REMOVED] `DriverCapabilities.transactions` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Transaction use is gated on METHOD PRESENCE — `driver.beginTransaction` (`engine.transaction()`, ADR-0034 ambient transactions): a driver without the method gets the non-transactional fallback, whatever this bit claimed. Discovery's `transactionalBatch` capability is likewise derived from `engine.transaction` plus the mounted batch route, never from this bit. Delete the key. |
174+
| **transactions** | `never` | optional | [REMOVED] `DriverCapabilities.transactions` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Transaction use is gated on the DRIVER'S DECLARATION, no longer on METHOD PRESENCE alone: `engine.transaction()` asks `driverSupportsTransactions(driver)` — `driver.beginTransaction` present AND `transactionsUnsupported` not set (ADR-0034 ambient transactions, ADR-0119 D1). A driver without the method — or a transport that declares that live bit — gets the non-transactional fallback, whatever this bit claimed. Discovery's `transactionalBatch` capability is likewise derived from `engine.transaction` plus the mounted batch route, never from this bit. The live `transactionsUnsupported` bit is NOT this key restored and is not its opposite spelled differently: this one CLAIMED support nothing checked, that one DENIES support the engine does check, and it is written only by a transport that inherits `beginTransaction` from a base class it cannot honour. A driver with real transactions declares nothing. Delete the key. |
174175
| **savepoints** | `never` | optional | [REMOVED] `DriverCapabilities.savepoints` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No savepoint code path exists in the engine — a capability bit for a feature the platform does not call is a false affordance, not documentation. Delete the key. |
175176
| **isolationLevels** | `never` | optional | [REMOVED] `DriverCapabilities.isolationLevels` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Isolation is requested per transaction via `beginTransaction({ isolationLevel })`; no planner ever consulted this list to decide anything. Delete the key. |
176177
| **queryFilters** | `never` | optional | [REMOVED] `DriverCapabilities.queryFilters` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. `find()` receives the full QueryAST (`where`/`orderBy`/`limit`/`offset`) and MUST execute all of it — the "ObjectQL will filter in memory" fallback this bit's description promised was never built. Delete the key. |

0 commit comments

Comments
 (0)