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
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
// - updateManyData / batchData → per-row `droppedFields` on each result row;
// - insertManyData → per-row `droppedFields` on each outcome;
// - createManyData → aggregated top-level `droppedFields` (its
// response has no per-row slot; the insert strip is schema-uniform).
// response has no per-row slot, so a union is the only view it can
// represent; read a name there as "at least one row dropped this field",
// never "every row dropped the same set" — ruling C (#14147) exempts keys a
// `beforeInsert` hook assigned, recorded per row, so rows CAN differ).

import { describe, it, expect, vi } from 'vitest';
import { assertEngineUpdateDispatch, assertEngineFindOnePredicate } from '@objectstack/metadata-core';
Expand Down Expand Up @@ -87,7 +90,7 @@ describe('createManyData — aggregated top-level droppedFields (#3455)', () =>
return { p: new ObjectStackProtocolImplementation(engine as any), engine };
}

it('aggregates the schema-uniform create strip across rows into one event', async () => {
it('two rows forging the same readonly key surface ONE aggregated top-level event', async () => {
const { p } = makeProtocol();
const res: any = await p.createManyData({
object: 'approval_case',
Expand Down
27 changes: 20 additions & 7 deletions packages/metadata-protocol/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1873,14 +1873,27 @@ const CLONE_STRIP_FIELDS: readonly string[] = [
* `(object, reason)` with the UNION of dropped field names.
*
* Used by the bulk-create surface (`createManyData`), whose `{ object, records,
* count }` response has no per-row slot to hang a `droppedFields` on. The
* create-side static-`readonly` strip is schema-uniform — every row drops the
* same set — which makes an aggregated view faithful rather than lossy. (Since
* #14147 that strip is the ENGINE's, which reports one event per CALL for it,
* so the aggregation is over the runtime-owned per-row events.) Returns `[]` when nothing was dropped so callers can spread
* count }` response has no per-row slot to hang a `droppedFields` on — a union
* is the only view that response can represent, which is the whole reason this
* collapse exists. (Since #14147 that strip is the ENGINE's, which reports one
* event per CALL for it, so the aggregation is over the runtime-owned per-row
* events.)
*
* ⚠️ So read a name in a merged event as "AT LEAST ONE row dropped this field",
* never "every row dropped the same set". Maintainer ruling C (#14147) put the
* static-`readonly` strip INSIDE `engine.insert`, AFTER the `beforeInsert`
* hooks, where it exempts keys a hook itself assigned — recorded PER ROW and
* indexed per row at the call: `packages/objectql/src/engine.ts` hands
* `stripReadonlyFields` the option `hookWrittenKeys: rowHookWrittenKeys[i]`,
* and that option's only power is to turn a STRIP into a KEEP. A hook that
* stamps a protected key on some rows and not others therefore makes those rows
* drop DIFFERENT sets, so the union is faithful to the BATCH without being
* faithful to any one row.
*
* Returns `[]` when nothing was dropped so callers can spread
* `...(x.length ? { droppedFields: x } : {})` and keep the omit-when-empty shape.
* The per-row `insertMany`/`batch` paths keep row precision instead (they have a
* per-row result to carry it).
* The per-row `insertMany`/`batch` paths carry their own per-row `droppedFields`
* instead — they have a per-row result to hang one on.
*/
function mergeDroppedFieldEvents(events: DroppedFieldsEvent[]): DroppedFieldsEvent[] {
if (events.length === 0) return [];
Expand Down
Loading