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
35 changes: 35 additions & 0 deletions .changeset/18331-objectql-hook-wrappers-per-row-docblocks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
'@objectstack/objectql': patch
---

docs(objectql): the hook-wrapper docblocks state the per-row `before*` contract (#18331)

Two docblocks in `hook-wrappers.ts` stated the RETIRED batch model in the
present tense: `pickRecordPayload`'s said a predicate (`multi: true`) bulk
update's `before*` dispatch "still fires once for the batch with no prior row",
and `pickPreviousPayload`'s "when `previous` is ABSENT" list named that same
dispatch as an absence case because "it fires ONCE for N matched rows".

Ruling #16074 / ADR-0058 Addendum II (clauses D1/D2) retired that model, and the
engine already implements the replacement: `dispatchPerRowBeforeHooks` dispatches
`before*` once per matched row on the single-record shape and binds that row's
pre-image (`previous: coerceBooleanFields(schema, row)`). So both phases of a
predicate write now merge, materialise and bind `previous` exactly as a
single-record write does; what remains unbound is any update-shaped context
whose prior row is not in hand, which is what the second docblock now says.

This is published text, not an internal comment. Measured against the shipped
`@objectstack/objectql@17.4.0` tarball: the first docblock is emitted verbatim
onto the exported `hookRecordState` declaration (`dist/util-Dw5ZTIII.d.ts:8039`,
and the matching `.d.mts`), reachable from both the `.` and `./core`
entrypoints, so every consumer's editor surfaces the retired sentence on hover.
The second docblock does NOT ship — `pickPreviousPayload` is module-private and
appears in `dist/` only as an `{@link}` reference — but it is the source a
maintainer reads, and two docblocks one screen apart stating opposite contracts
is the drift this repairs.

No behaviour change and no assertion change: prose only.

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 ② (`Clause-②: no`).
26 changes: 14 additions & 12 deletions packages/objectql/src/hook-wrappers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1080,12 +1080,13 @@ function declaredFieldsFor(ctx: HookContext): Record<string, unknown> | undefine
* Materialisation is applied only when the record's persisted state is in hand
* — an insert (nothing to know) or an update whose prior row was fetched.
*
* Since #5038 a predicate bulk update's AFTER dispatch is per row and DOES
* carry the row's prior state, so it merges and materialises like any
* single-record write — which is exactly what "`record` is the row's real state,
* not the bare payload" means (#4862). Its `before*` dispatch still fires once
* for the batch with no prior row, so that payload is left exactly as it is
* rather than gaining `null`s that contradict N stored rows.
* A predicate bulk update's AFTER dispatch is per row since #5038, and its
* `before*` dispatch since #5574 (ADR-0058 Addendum II, D1/D2): both carry the
* row's prior state, so both merge and materialise like any single-record
* write — which is exactly what "`record` is the row's real state, not the
* bare payload" means (#4862). A context whose prior row is not in hand keeps
* its payload exactly as it is rather than gaining `null`s that contradict
* stored state.
*
* Copies, never mutates: `ctx.previous` and `ctx.input.data` are the engine's
* own objects, observed by the handlers that run after this gate.
Expand Down Expand Up @@ -1174,12 +1175,13 @@ function pickRecordPayload(ctx: HookContext): any {
* identifier from the CEL scope. Same here:
* - **insert** — there is no prior state, so `previous` is unbound and any
* reference to it is an author error, reported as such;
* - **the `before*` dispatch of a predicate (`multi: true`) bulk write** —
* it fires ONCE for N matched rows, so there is no single prior record to
* bind; `previous` stays unbound rather than being invented. The `after*`
* dispatch of that same write is per row since #5038 and binds the row's
* own pre-image, so a transition condition there reads exactly as it does
* on a single-record write.
* - **any update-shaped context whose prior row is not in hand** — with no
* row there is no single prior record to bind, so `previous` stays unbound
* rather than being invented. A predicate (`multi: true`) bulk write is no
* longer one of these: since #5574 (ADR-0058 Addendum II, D1/D2) it
* dispatches BOTH phases per matched row — `after*` since #5038 — each
* context binding that row's own pre-image, so a transition condition
* reads exactly as it does on a single-record write.
* Binding `null`/`{}` instead would make `previous.x == null` answer "yes"
* for a record whose prior state is simply unknown — a fabricated fact, the
* one thing materialisation is careful never to do.
Expand Down
Loading