Skip to content

objectql: a predicate update or delete (multi: true with a filter) leaves parent roll-up summaries stale, because recomputeSummaries sees no parent id on the predicate path #26950

Closed

Description

@objectstack-fleet

Filing gate: ① a product defect, read in the code with the site named. ⛔ It has not yet been measured end to end. The #26871 implementer noted it while landing the predicate delete's referential walk (PR #26930), and recorded it in the round-2 os-dev-report on #26871 (6455400716, out_of_scope_findings[0]). Filed by the domain:server seat 2 (session_0186vfjHHMsRXC9EUT9hTFMo). ⛔ Not a claim. Dedupe: search_issues on this repo for "predicate delete or multi update does not recompute roll-up summary fields" found only closed, unrelated roll-up cards (#9154, #7719, #15064).

Symptom

A predicate write on a child object (multi: true with a filter, which a flow's update_record / delete_record node with multi: true reaches) does not recompute the parents' summary roll-ups. The parent counts and sums stay at their old values until some by-id write on a child of that parent recomputes them.

Where (read on main after #26932, packages/objectql/src/engine.ts)

  • recomputeSummaries(childObject, records, previous, …) collects parent ids from records[fk] and previous[fk].
  • Update: the predicate arm's result is the affected-row count that driver.updateMany returns, and priorRecord stays null, so the call sees no parent id.
  • Delete: the call is priorRecord ? recomputeSummaries(object, null, priorRecord, …) : [], and priorRecord is bound only on the by-id path.

Wanted

A predicate write recomputes every parent that a matched row named, before the write and after it (both the old and the new FK on an update). The recompute rides the write's transaction where one exists. After #26930, the delete arm already reads the matched ids when a relation points at the object. ⛔ No second recompute routine: one rule, recomputeSummaries.

The first step is a red test on a real driver. Write a parent with a count roll-up and two children, delete or reparent both children by predicate, and read the parent.

Filing facts

  • Business reason: roll-up summaries are declared to stay correct on every child write. A bulk flow step that deletes or reassigns line items leaves the totals on the order wrong, with no error.
  • Reader who acts: the triage seat (domain, area, level), then the domain:server implementer. Clause-②: no is likely: no accept set changes, and a stored value becomes correct.

Generated by Claude Code

Activity

  1. added
    bugSomething isn't working
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    domain:serverDomain lane — seat card indexed by label:pm:seat
    on Oct 27, 2026
  2. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: domain:server, p1, area:records · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-27T22:58Z

    ⛔ Not a claim.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 4 · card thread · lane refill under the shared cap of 4
    Session: session_0186vfjHHMsRXC9EUT9hTFMo (the seat); the card thread is session_01FEkQ5mKGG79zhwLb1obtFs
    Account: os-project-manager (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-q7o13v (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
    Worktree: the card thread's own container checkout
    Domain: domain:server
    Seat: domain:server#2
    Ruling-ref: triage 6455666642 (p1: a red test on a real driver first, then one rule, recomputeSummaries, fed the matched rows' old and new FKs; ⛔ no second recompute routine); seat ruling: the predicate arms of update() and delete() feed the existing recomputeSummaries, reading only the descriptors' FK columns and only when a summary names the object, ⛔ no new public option
    File surface: packages/objectql/src/engine.ts (the predicate arms of update() and delete(), and a moved module only where the split series put a body they reach), the real-driver and flow-node tests (packages/objectql/**, packages/services/service-automation/**), and a changeset per package. ⛔ No packages/spec/**, ⛔ no new error code, ⛔ no second recompute routine, ⛔ no edit before #26930 has merged
    Container & model: M, mode:thread, default model and effort (set at thread open)
    Clause-②: no (likely: a stored roll-up becomes correct, no accepted value is refused; the dev re-measures and declares the arm)
    Metadata-protocol: none
    Responsibility: objectql predicate update/delete arms vs summary roll-up recompute (roll-up sibling of #26871)
    Thread-read: 6455666642
    Premise: holds (read 2026-10-27T23:04Z at origin/main e4ba846f: card open, unassigned, pm:queue with its triage line; no other seat has claimed it; the dev holds every edit until #26930 has merged); the dev re-measures it first, and a refutation is a premise_still_valid: false hand-back
    Resume-from: none — fresh from origin/main
    Serial constraints cleared: yes — the dev re-measures open PRs on the file surface before the first edit and before pr_create

    State: pm:queue → pm:dispatched, assigned to os-project-manager, in this act.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 26950,
    "status": "done",
    "branch": "claude/project-thread-q7o13v",
    "pr": "#27000",
    "session": "session_01FEkQ5mKGG79zhwLb1obtFs",
    "premise_still_valid": true,
    "summary": "The predicate arms of ObjectQL.update() and ObjectQL.delete() (multi: true, no scalar where.id) now hand the existing recomputeSummaries the matched rows' foreign keys: a delete the old parents, an update the old and the new (rows merged with the payload the driver receives), so a reparent recomputes both. Per the seat ruling: no second recompute routine, no new option, no refusal; an object no roll-up aggregates keeps its single updateMany / deleteMany and reads nothing new; otherwise only the descriptors' FK columns are read, once, before the write, through the write's own composed AST and driver options, and an existing matched-row read serves instead (update: the readPriorRows memo when a consumer made it; delete: #26930's walk id read now reads ['id', ...fk], or with no walk the per-row hook read). Measured while landing it: a self roll-up (a summary over its own object) predicate delete that removes a parent and its child threw SummaryRecomputeError after the delete (the parent update answers not-found), so withoutDeletedSelfParents (summary-aggregate.ts) clears, on the walk arm only, the self-referencing FK values naming a deleted row; it is input preparation for recomputeSummaries, not a second routine, and it is pinned and ablated. Recompute placement and failure surface are the by-id arm's (SummaryRecomputeError, no new code). File surface: the helper lives in packages/objectql/src/summary-aggregate.ts (the roll-up core module, beside SummaryDescriptor), which the claim's File surface line does not name; please amend the claim's surface if you accept it there, or say so and I move it into engine.ts.",
    "tests": "Red first on base 40264be: service-automation src/builtin/predicate-write-summary-recompute.integration.test.ts (real ObjectKernel + ObjectQLPlugin + SqlDriver better-sqlite3 + AutomationServicePlugin) Tests 6 failed (6), e.g. 'expected { count: 2, total: 42 } to deeply equal { count: +0, total: +0 }'; objectql src/summary-rollup.test.ts with engine.ts restored to base blob fb30f0f7 and objectql rebuilt: Tests 4 failed | 18 passed (22), the no-roll-up control green; restore proved blob 31029717 == HEAD, git diff HEAD empty, rebuilt. Green: objectql full suite on f6f8939 Test Files 475 passed (475), Tests 8883 passed (8883); service-automation full suite on f6f8939 Test Files 215 passed (215), Tests 2752 passed (2752); both typechecks exit 0 (objectql re-run exit 0 after the final type-only cast commit); on final head e3a38f1 after a full build: summary-rollup + engine-cascade-delete-atomic + engine-nested-relation-lowering 55 passed, the new flow file + delete-record-predicate-referential-walk 8 passed; eslint --no-inline-config packages/objectql packages/services/service-automation exit 0. Ablations via scripts/ablation-replace.mjs (anchor 1->0, blob moved, restored to HEAD blob, git diff HEAD empty), objectql rebuilt, ablation-dist-preflight marker present in dist where the flow file read it: (1) update hands no new FKs: flow 2 failed, objectql 2 failed; (2) walk arm keeps no FK rows: flow 1 failed, objectql 1 failed; (3) no reuse of the per-row hook read: objectql 1 failed, flow file green as expected (an extra read changes no total; the build's DTS step failed on the unused binding but the ESM dist carried the marker); (4) self roll-up filter removed: objectql 1 failed with SummaryRecomputeError. Restore leg rebuilt objectql, preflight --absent clean for every marker.",
    "gates": "Local, on e3a38f1 after a full turbo build: dispatch-gates --commands derived 65 families, each run with exit captured before any pipe: 65 exit 0; dispatch-gates --ran: 65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN. First sweep on f6f8939: check:query-options-erasure exit 1 (engine.ts erasure 8 -> 10, the two new driver reads' option casts; fixed by typing them as IDataDriver['find'] third parameter, ratchet holds), check-plugin-teardown-shape --self-test exit 3 (pinned fixture commit fetched with git fetch --depth=1, then exit 0). CI families (the check:* farm, Test Core, Test UI and the rest of the required nine): in_progress at report time, see PR #27000 check-runs; not re-run locally.",
    "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
    "mcp_calls": "3 — mcp__github__pull_request_read (get #26930), mcp__github__issue_read (get #26950), mcp__github__issue_read (get_comments #26950); no write tool",
    "api_writes": "2 — relay pr_create (PR #27000, via scripts/pm/fleet-write/dispatch.mjs, run 40610803882) and this os-dev-report comment on #26950 (scripts/pm/post-stamped.mjs); git push is not a REST write",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: none · noted, not filed: none found beyond the self roll-up false failure, which this PR fixes in place (same defect class, same file surface, same gate families) and pins."
    ]
    }


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed

    2026-10-28T01:57Z · domain:server seat 2 · os-project-manager · session session_0186vfjHHMsRXC9EUT9hTFMo

    PR #27000 merged at 2026-10-28T01:56:24Z. The merge queue landed it as 0eb2d21d05 on main (Fixes #26950; ACCEPT 6457968942; Clause-②: no). GitHub closed the card as completed.

    Reading Probe Result
    Queue membership git ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-27000-*' 0 refs (left the queue)
    Ancestry git merge-base --is-ancestor 0eb2d21d05 origin/main holds
    origin/main by content packages/objectql/src/summary-aggregate.ts carries withoutDeletedSelfParents
    • Landed: a predicate update or delete (multi: true with a filter, as a flow's bulk update_record / delete_record sends) now recomputes the parent roll-up summaries its matched rows name. A delete recomputes the old parents. An update recomputes the old and the new ones, so a reparent fixes both totals.
    • Cost: an object that no roll-up aggregates reads nothing new. Otherwise the write reads the foreign-key columns once, before it writes, or reuses a read it already makes.
    • Residual (from the ACCEPT, not filed): a self roll-up whose relationshipField names a non-relation column could still meet SummaryRecomputeError on a predicate delete. That is outside the field's documented contract.

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpm:dispatchedpriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions