Repository navigation
No plugin-reachable transactional write primitive — every migration-class tool rebuilds journal + compensation #4612
Description
Activity
Claiming this issue.
- Session: https://claude.ai/code/session_01NKcGqCYCCpMkB5UW8jNPXx
- Branch:
claude/focused-mendel-5akrna— harness-fixed name, so it deviates from theclaude/issue-<n>-<slug>convention. Recording it here explicitly so the claim is still discoverable by branch.
Scoping note — two premises in the issue need correcting
1. Option 1's hard part is already paid. ADR-0034 ("Robust multi-write transactions (ambient transaction context)") is Accepted — implemented (v8.0.0).
ObjectQL.transaction()exists atpackages/objectql/src/engine.ts:4934-4973with an ambientAsyncLocalStoragehandle lifted into every driver call atbuildDriverOptions(engine.ts:1257-1275) — so hooks, validation predicates and internal reads already join the open transaction, which is exactly the "middleware/hook semantics inside a transaction" design project the issue prices in. Nested calls JOIN rather than nest (ADR-0067 D2).What is actually missing is contract reachability, not the mechanism:
IObjectQLEngine(packages/spec/src/contracts/objectql-engine.ts) never declaredtransaction, so a plugin typed to the slot cannot see it. The in-repo callers reach it through structural casts —packages/metadata-protocol/src/protocol.ts:2289(thetransactionalBatchdiscovery probe) and:7455-7458(inTxninpublishPackageDrafts), plus a hand-declared local member insys-metadata-repository.ts. That contract's own evidence bar (L29-37: a member joins when a cross-package consumer already calls it through the slot) is met three times over; the evidence just never got recorded.2. driver-turso is not in this repo.
turso-driver.ts:764-776/remote-transport.ts:430-443don't resolve here — the only Turso artifact isdocs/design/driver-turso.md(Status: Proposal). The in-repo drivers already implementbeginTransaction/commit/rollbackas required members ofIDataDriver(packages/spec/src/contracts/data-driver.ts:221-235): driver-sqlsql-driver.ts:2214+, driver-memorymemory-driver.ts:595+, driver-mongodbmongodb-driver.ts:545+.The queued-consumer pain is real regardless. Engine transactions alone cannot serve the four tools: a million-row backfill cannot hold one SQLite write-lock, driver-memory's
beginTransactiondeep-clones the entire database,transaction()binds only the default driver (engine.ts:4950) so multi-datasource writes silently commit outside it, and no in-process rollback survives a process crash. So journal + compensation + resume is needed on top of transactions, not instead of them.Decision (maintainer-confirmed) — a hybrid, recorded as ADR-0118
- D1 — declare
transaction()onIObjectQLEngine; delete the casts. This is option 1 reduced to what it actually costs given ADR-0034. - D2 — bless a framework-owned migration-journal runner (option 2): preflight dry-run → persistent journal in a
sys_migration_journalplatform object → chunked writes, each chunk insideengine.transaction()with itschunk_donemarker written inside that transaction (sodone ⇔ committed) → LIFO compensation → re-entrant forward recovery after a crash → at-least-once withattempt-keyed idempotency, per the discipline already documented inpackages/core/src/utils/bulk-write.ts. ADR-0118 specifies it in implementable detail; the runner itself lands in a follow-up PR. - D3 — retire
IDataEngine.batch?(packages/spec/src/contracts/data-engine.ts:97-100). Declared with a three-word doc comment, implemented by no engine, called by nobody, and itsDataEngineRequestunion even nests batches recursively. Enforce-or-remove (ADR-0049); D1 + D4 cover its stated purpose. - D4 — a bug found while scoping, fixed here:
batchData'satomicflag opens no transaction. It only breaks the loop on first failure (packages/metadata-protocol/src/protocol.ts:5302-5305) — every op before the failure stays committed while the response is titled "atomic". Same class as [P1]filterfolds towhereinengine.findonly —findOne/count/update/deletesilently match EVERY row, and the hook docs teach the broken call #4346: a write-path guarantee that is declared but not enforced. It becomes real (oneengine.transaction()around the batch, rollback-all, response reporting zero successes) or refused with501 NOT_IMPLEMENTEDon a runtime that cannot roll back — never silent best-effort. The Zodatomicdefault (packages/spec/src/api/batch.zod.ts:62) is also declaredtruewhile no enforcement site delivers it; it gets aligned to the enforced value rather than the reverse, so no existing caller's failure semantics change without opting in.
This PR: ADR-0118 + D1 + D4 + tests + changeset. Follow-ups to be filed: the D2 runner implementation, the D3 mechanical removal,
engine.transactioncaveat hardening (silent degrade / default-driver-only), and the sibling fake-atomic indeleteManyData/updateManyData.D13 can proceed on the hand-rolled pattern and collapse onto the runner when D2 lands, as the issue anticipated.
Generated by Claude Code
- added a commit that references this issue
on Aug 2, 2026 - added 3 commits that reference this issue
on Aug 3, 2026
Found while scoping the D13 promotion tool (cloud#874). Not a blocker for any single tool — the point is that four migration-class consumers are now queued behind the same missing primitive, and each is being forced into the same hand-rolled shape.
Current state
batch?(requests, { transaction?: boolean })(packages/spec/src/contracts/data-engine.ts:70-91) — no engine implementation, no callers.beginTransaction/commit/rollback,turso-driver.ts:764-776;remote-transport.ts:430-443) — not surfaced through ObjectQL.multi: truepredicate updates). A failure mid-sequence strands partial state; a process crash (as opposed to a thrown error) defeats in-process compensation entirely.The queued consumers
sys_organization,sys_business_unit(_member),sys_member,sys_user_position, N business objectsowning_business_unit_idEach independently converges on: dry-run preflight → undo journal → LIFO compensation → re-entrant forward recovery (precedent: cloud
organization-lifecycle.tsslug rename). One copy of that machinery is an engineering answer; four copies is a platform debt signal.Decision needed (one of three)
No urgency ranking implied between 1 and 2; what needs deciding is which debt we are choosing. D13 proceeds on the hand-rolled pattern regardless and simplifies later if 1/2 lands.
Refs: #4346 (the
multi: trueno-predicate footgun — evidence this write path is sharp), ADR-0110, ADR-0117, cloud#874, cloud#1006.