You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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>
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.
@@ -163,14 +163,15 @@ const result = AggregationPipelineSchema.parse(data);
163
163
|**queryDateGranularity**|`Record<string, boolean>`| optional | Per-granularity native date bucketing (day/week/month/quarter/year). Missing keys fall back to in-memory bucketing. |
|**transactionsUnsupported**|`boolean`| optional | Transport cannot honour transactions even though `beginTransaction` is inherited (absence = false) |
166
167
|**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. |
167
168
|**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. |
168
169
|**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. |
169
170
|**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. |
170
171
|**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. |
171
172
|**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. |
172
173
|**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. |
174
175
|**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. |
175
176
|**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. |
176
177
|**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