diff --git a/.changeset/18331-objectql-hook-wrappers-per-row-docblocks.md b/.changeset/18331-objectql-hook-wrappers-per-row-docblocks.md new file mode 100644 index 00000000000..a64f616518f --- /dev/null +++ b/.changeset/18331-objectql-hook-wrappers-per-row-docblocks.md @@ -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`). diff --git a/packages/objectql/src/hook-wrappers.ts b/packages/objectql/src/hook-wrappers.ts index 141c57b64f5..89d83464d48 100644 --- a/packages/objectql/src/hook-wrappers.ts +++ b/packages/objectql/src/hook-wrappers.ts @@ -1080,12 +1080,13 @@ function declaredFieldsFor(ctx: HookContext): Record | 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. @@ -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.