Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .changeset/17975-objectql-per-row-previous-docblock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
'@objectstack/objectql': patch
---

docs(objectql): the per-row `before*` docblock states the #16074 rule — a row-invariant-in-effect rewrite is ADMITTED (#17975)

`dispatchPerRowBeforeHooks`'s docblock (ADR-0058 Addendum II, clause D3) still
said per-row `previous` was supplied *"so a guard can REFUSE the write (throw),
not so a rewrite can be aimed"*, and a test comment in
`bulk-write-per-row-hooks.test.ts` said the same. Ruling #16074, landed in
`@objectstack/spec` by PR #17249, retired that: a per-row `previous`-conditioned
rewrite is admitted when its written KEY SET is the same on every matched row
and is assigned IN PLACE, kept safe by the engine's
`MULTI_UPDATE_HOOK_KEY_DIVERGENCE` refusal (#14099). Key-set divergence, a
per-row VALUE and a row-conditioned REPLACEMENT of `ctx.input.data` all stay
outside the contract.

This is published text, not an internal comment: JSDoc on a `private` member
survives `.d.ts` emit. Measured in the shipped `@objectstack/objectql@17.4.0`
tarball — the retired sentence is present in six published files, including
`dist/util-Dw5ZTIII.d.ts:3554`, on a member of the `ObjectQL` class that both
the `.` and `./core` entrypoints export. Every consumer's editor surfaces it on
hover, so as soon as spec's changeset is consumed the two packages would state
opposite contracts.

No behaviour change: the engine already follows the new rule, and the three
shipped provenance stamps (`email-template-provenance.ts`,
`sharing-rule-provenance.ts`, `webhook-provenance.ts`) all assign in place. The
admitted shape's coverage already exists in
`multi-update-hook-key-divergence.test.ts`; the test comment now points at it.

Graded `patch`: the act moves published PROSE. It adds no exported symbol, no
key and no accepted value — the accept set was widened by PR #17249 in
`@objectstack/spec`, not here — so this PR declares no clause ②.
8 changes: 6 additions & 2 deletions packages/objectql/src/bulk-write-per-row-hooks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -566,8 +566,12 @@ describe('[#5574 / D2] the per-row before context is the SINGLE-RECORD shape', (
});

it('a `previous`-reading GUARD can now refuse a bulk write per row', async () => {
// What per-row `previous` is FOR (D3 says so in as many words): so a guard
// can REFUSE, not so a rewrite can be aimed at one row.
// ONE of the two things per-row `previous` is FOR (D3 says so in as many
// words): so a guard can REFUSE. The other, ruled on #16074, is a
// ROW-INVARIANT-IN-EFFECT rewrite — the same written key set on every
// matched row, assigned in place — which
// `multi-update-hook-key-divergence.test.ts` covers. What stays outside
// the contract is a rewrite AIMED at one row.
const { engine } = await boot([hook('guard', 'beforeUpdate', (ctx) => {
if ((ctx.previous as any)?.status === 'locked') throw new Error('row is locked');
})]);
Expand Down
13 changes: 9 additions & 4 deletions packages/objectql/src/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3445,10 +3445,15 @@ export class ObjectQL implements IObjectQLEngine {
* its dispatch — that write-back is what makes "accumulate in dispatch order"
* true for both spellings rather than only the first.
*
* A rewrite CONDITIONED on the row (`ctx.previous`, `ctx.input.id`) is
* outside the contract: it does not scope itself to the row it was decided
* on, it widens to every matched row. Per-row `previous` is supplied so a
* guard can REFUSE the write (throw), not so a rewrite can be aimed.
* A rewrite CONDITIONED on the row (`ctx.previous`, `ctx.input.id`) cannot
* scope itself to the row it was decided on: it widens to every matched row.
* Per-row `previous` is supplied so a guard can REFUSE the write (throw),
* and — ruled on #16074 — so a hook can make a ROW-INVARIANT-IN-EFFECT
* rewrite: one whose written KEY SET is the same on every matched row AND is
* assigned IN PLACE (`ctx.input.data.x = 1`, never `ctx.input.data = {…}`,
* which the recording below cannot attribute). A rewrite AIMED at one row
* stays outside the contract, and what makes the admitted shape safe is that
* enforcement rather than the hook's good faith.
*
* ## D3, ENFORCED — divergent key sets refuse the batch [#14099]
*
Expand Down
Loading