|
| 1 | +--- |
| 2 | +"@objectstack/spec": minor |
| 3 | +"@objectstack/metadata-protocol": minor |
| 4 | +--- |
| 5 | + |
| 6 | +feat(spec,metadata-protocol): `IObjectQLEngine.transaction` joins the slot contract, and `batchData`'s `atomic` flag becomes real — rollback or refusal, never silent best-effort (ADR-0118 D1/D4, #4612) |
| 7 | + |
| 8 | +**D1 — the contract fix.** `ObjectQL.transaction()` — ADR-0034's ambient |
| 9 | +transaction, shipped since v8.0.0 — was reachable from plugin space only |
| 10 | +through `as unknown as` casts: the metadata protocol's atomic publish and its |
| 11 | +`transactionalBatch` discovery probe, and the sys-metadata repository's |
| 12 | +`withTxn`, each declared a private structural slice of an engine none of them |
| 13 | +import. It is now declared on `IObjectQLEngine`, required per that contract's |
| 14 | +own rule, with its caveats written into the TSDoc as part of the declared |
| 15 | +meaning rather than left to be discovered: it covers the **default driver |
| 16 | +only**, and when that driver has no `beginTransaction` the callback runs with |
| 17 | +no transaction and no rollback. `MetadataHostEngine` and the sys-metadata |
| 18 | +repository's engine surface now type their optional member as |
| 19 | +`IObjectQLEngine['transaction']`, so a narrow host surface can no longer drift |
| 20 | +from the real signature. Runtime `typeof === 'function'` probes stay — that is |
| 21 | +test-double defence the type system does not replace. |
| 22 | + |
| 23 | +**D4 — the honesty fix.** `batchData`'s `options.atomic` promised "rollback |
| 24 | +entire batch on any failure (transaction mode)" and delivered a `break` |
| 25 | +statement. Every write before the failure stayed committed, and — the part that |
| 26 | +did the real damage — the response reported those rows `success: true` under |
| 27 | +the one flag whose job is to guarantee they were undone. |
| 28 | + |
| 29 | +Now an explicitly atomic batch runs inside ONE `engine.transaction()`: the |
| 30 | +first failure rolls back every prior write, and the response says so |
| 31 | +(`succeeded: 0`, with rows marked `ROLLED_BACK:` / the causal error / |
| 32 | +`NOT_ATTEMPTED:`, and no row reporting success). On a runtime that cannot roll |
| 33 | +back — no `transaction()`, or a default driver without `beginTransaction` — an |
| 34 | +atomic request is **refused** with `501 NOT_IMPLEMENTED` rather than silently |
| 35 | +degrading, matching the cross-object `/batch` route. `atomic` takes precedence |
| 36 | +over `continueOnError`, whose own description already scoped it to |
| 37 | +`atomic=false`. In atomic mode the upsert path no longer falls back to an |
| 38 | +insert when its update throws: inside an aborted transaction that fallback can |
| 39 | +only fail with a secondary error that buries the real cause. |
| 40 | + |
| 41 | +**Aligned declaration.** `BatchOptionsSchema.atomic` declared `.default(true)` |
| 42 | +while no enforcement site delivered atomicity — and the REST route forwards the |
| 43 | +original request body rather than the parsed output, so the declared default |
| 44 | +never reached the loop at all. The default is now `false`: the declaration is |
| 45 | +aligned down to what every site already does, rather than up to what none of |
| 46 | +them did. Honouring the old `true` would have silently flipped the failure |
| 47 | +semantics of every existing batch caller and hard-failed ordinary batches on |
| 48 | +any driver that cannot transact. Callers who were explicitly sending |
| 49 | +`atomic: true` now get what they always asked for; callers sending nothing keep |
| 50 | +today's behaviour exactly. |
| 51 | + |
| 52 | +If you were passing `atomic: true` and relying on partial results surviving a |
| 53 | +failure, that was the bug — switch to `atomic: false` (or omit it) for |
| 54 | +best-effort semantics. |
| 55 | + |
| 56 | +ADR-0118 also rules on two items landing separately: D2 specifies a |
| 57 | +framework-owned migration-journal runner for multi-step migrations too large |
| 58 | +for one transaction, and D3 retires the declared-but-unimplemented |
| 59 | +`IDataEngine.batch?`. |
0 commit comments