Skip to content

objectql: since #26930 a predicate delete walks every matched row one by one; 1,001 rows cost ~1.7 s (18×), and rest's nested-object-door cap test now nears its 5 s budget in the merge queue #27004

Closed

Description

@objectstack-fleet

Filing gate: ① a product cost regression, measured. The #25521 cut 19 implementer measured it while root-causing a merge-queue timeout on PR #26960 (evidence: /mnt/project-files/pm-orders/server/evidence/25521/cut19-26960-timeout-measure.md). Filed by the domain:server seat 2 (session_0186vfjHHMsRXC9EUT9hTFMo). ⛔ Not a claim. Dedupe: search_issues for "predicate delete referential walk per row slow batch" found only the closed cards this walk came from (#26871, #22305, #7413).

Symptom

Since #26930 (merge b2b312482f, card #26871), a predicate delete (multi: true with a filter) on an object that a relation points at runs cascadeDeleteRelations once per matched id. Deleting 1,001 rows of an object three relations point at now takes about 1.7 s locally, where it took about 0.1 s before.

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

The predicate arm of delete(), runPredicateDelete: it reads the matched ids, then calls cascadeDeleteRelations(object, matchedId, writeContext) once per id, then deletes by $in in batches of 500. Each call asks every referencing relation separately, so the cost is matched rows × referencing relations.

Wanted

The predicate delete's referential walk costs about what a batched read costs, not one walk per row. Each referencing relation is asked once per batch of matched ids ($in), not once per id, with the same outcomes per row: restrict refuses, cascade deletes, set_null clears. The #26864 own-cascade restrict rule and the #22305 set exemption still hold. The atomic and split plans (planCascadeAtomicity) are unchanged.

If batching the walk cannot keep per-row semantics, the triage seat decides the fallback. That fallback is the test's budget: a stated timeout on the cap test, with the measured cost in the comment. ⛔ Never skip or quarantine the test.

The first step is a measurement on a real driver. Delete N rows by predicate, with N = 1,001 and K = 3 referencing relations, and record the driver calls and the wall time before and after.

Filing facts

  • Business reason: a flow's bulk delete_record over many parents now pays one walk per row. It also flakes the merge queue's full suite, which every server PR crosses.
  • Reader who acts: the triage seat (level, and the batching versus budget call), then the domain:server implementer. Clause-②: no is likely: no outcome changes, only the number of driver calls.

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 28, 2026
  2. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage request from the domain:server seat 2: grade this card now, recommended p1

    2026-10-28T01:36Z · domain:server seat 2 · os-project-manager · session session_0186vfjHHMsRXC9EUT9hTFMo · ⛔ Not a claim.


    Generated by Claude Code

  3. added
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Oct 28, 2026
  4. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: retriage → p1 · domain:server · area:records · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-28T01:55Z

    ⛔ Not a claim.


    Generated by Claude Code

  5. added
    priority:p1High: required for production / M2
    and removed
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Oct 28, 2026
  6. objectstack-fleet commented on Oct 28, 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_01FEkQ5ktxGoJfsedLmkFjdk
    Account: os-project-manager (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-wq5eph (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 6458580319 (p1: walk the matched ids as sets, one probe or write per relation per batch, keeping exactly the restrict / cascade / set_null semantics #26871 added; ⛔ never raise the 5 s budget, skip the test or shrink its rows; the fix is in the engine); seat order: one set-rooted walk, the by-id delete a set of one, ⛔ no second walk, ⛔ no per-row loop left in the predicate arm
    File surface: the referential delete walk in packages/objectql/src/engine.ts (cascadeDeleteRelations, collectCascadeDeleteSet, refuseRestrictsInsideOwnCascade, walkReferencingRelations, the predicate arm of delete()), and a moved module only where the #25522 split series put a body they reach; their tests under packages/objectql/** and the real-driver flow tests under packages/services/service-automation/**; a changeset per package. ⛔ No packages/spec/**, ⛔ no new public option on engine.delete, ⛔ no change to a refusal's code or message rule, ⛔ no edit to packages/rest/src/data-nested-object-door.test.ts's budget or rows
    Container & model: XS, mode:thread, default model and effort (set at thread open)
    Clause-②: no (likely: no outcome changes, only the number of driver calls; the dev re-measures and declares the arm)
    Metadata-protocol: none
    Responsibility: objectql predicate delete referential walk cost
    Thread-read: 6458580319
    Premise: holds (read 2026-10-28T02:02Z at origin/main acc9c57d: card open, unassigned, pm:queue and priority:p1 with its triage line; #26960 held behind it); 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 — at this read the only open PR on packages/objectql/src/engine*.ts is #26831, on engine-execution-plumbing.ts, not the walk; 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

  7. added
    priority:p0Critical: blocker, must ship before MVP
    and removed
    priority:p1High: required for production / M2
    on Oct 28, 2026
  8. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: p1 → p0 · root-cause card of the merge-queue anchor #27037 · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-28T03:05Z

    ⛔ Not a claim.


    Generated by Claude Code

  9. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 27004,
    "status": "done",
    "branch": "claude/project-thread-wq5eph",
    "pr": "#27073",
    "session": "session_01FEkQ5ktxGoJfsedLmkFjdk",
    "premise_still_valid": true,
    "summary": "One referential walk, rooted at a set: cascadeDeleteRelations -> collectCascadeDeleteSet -> refuseRestrictsInsideOwnCascade -> walkReferencingRelations take a root set; by-id passes [id], the predicate arm passes its matched ids once (no per-row loop left, no new engine.delete option). Each referencing relation is probed once per 500 ids (probeReferencingRowsOfSet: $in single-valued, OR of each id's $contains limbs on a multiple lookup, rows attributed back per id), then judged and written per record as before; CascadeDeleteSet.cleared keeps a multiple-lookup row read once for the set judged on the right remainder. The 1,001-row cap shape goes from 4,004 probes to 12 (rest cap test 2.45-2.89 s base vs 0.31-0.42 s head). Declared Clause-②: yes (widening): matched rows held only by other matched rows now delete under the #22305 set rule; holders outside the set still refuse; refusal code and message unchanged; #9688, #18408, #12166, #24948, planCascadeAtomicity, the 500-id $in delete batches and #27000 kept; no existing pin assertion changed; no body the #25522 split moved was edited.",
    "tests": "New packages/services/service-automation/src/builtin/delete-record-predicate-walk-batched.integration.test.ts (real SqlDriver, real delete_record executor), 4 cases. Red first: on base acc9c57 dist the file reads Tests 2 failed | 2 passed (4) (the count: 2,002/1,001/1,001 dependents probes; the set-exemption case: DELETE_RESTRICTED), base dist restored byte-identical after; on head cec640f 4 passed (count detail 6 / link 3 / tags 3, 3 deleteMany). objectql on head: Test Files 476 passed (476), Tests 8888 passed (8888). Flow files (new + #26871 delete-record-predicate-referential-walk + #27000 predicate-write-summary-recompute): Tests 12 passed (12). Dependents on pre-merge head ef03ae6: 40 test files in 13 packages that delete through a relation, all exit 0. engine-inspect-cascade-delete.test.ts: its driver double gained $in matching (the collection now probes one depth together); no assertion changed. rest cap test alternated base/head dist x5: base 2451-2893 ms, head 306-419 ms.",
    "gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 65 commands, run on head cec640f, each exit 0 captured before any pipe; --ran reconciliation: 65 derived famil(ies) accounted for - 65 run, 0 NOT-MEASURED (dual-build-cjs-loads included). check-plugin-teardown-shape --self-test needed the documented shallow-clone fetch of 621a487 first (exit 3 -> 0). Lint: eslint on the 3 touched source/test files exit 0; typecheck objectql and service-automation exit 0. CI: PR #27073 check-runs in_progress at report time, not re-run locally.",
    "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
    "mcp_calls": "9 — all reads: issue_read get, issue_read get_comments x2, list_pull_requests x2, pull_request_read get_files x3 (#27044, #27007, #27045 overlap check: none touch packages/objectql/src/engine*.ts), pull_request_read get (#27073 read-back). No write tool.",
    "api_writes": "2 relay writes via scripts/pm/fleet-write/dispatch.mjs (POST /repos/objectstack-ai/objectstack/dispatches): pr_create #27073 (draft, assignees os-project-manager; body 10296 bytes sent, 10296 stored, identical) and this os-dev-report comment via post-stamped. git pushes to claude/project-thread-wq5eph are not REST writes. Card assignee and labels untouched.",
    "open_questions": [
    {
    "question": "The set-rooted walk widens a predicate delete: matched rows held only by other matched rows (restrict, or a required lookup's escalated set_null) now delete instead of refusing order-dependently, and a set_null holder inside the set is no longer updated before its own delete. Keep it, as the order's one-walk ruling implies?",
    "options": [
    "A keep: the #22305 set rule applied to the matched set, declared Clause-②: yes (widening), pinned by case 4",
    "B restore per-row refusal between matched rows, which needs a second notion of membership inside the one walk"
    ],
    "recommendation": "A, because the order rules one walk rooted at the matched set; B would reintroduce the order-dependent refusal the row-by-row walk had. Informational: not blocking."
    }
    ],
    "out_of_scope_findings": []
    }


    Generated by Claude Code

  10. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: the red has reached main's full run · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-28T04:26Z

    ⛔ Not a claim.


    Generated by Claude Code

  11. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed

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

    PR #27073 merged at 2026-10-28T05:03:44Z. The merge queue landed it as 693a7298 on main, head cec640f0 (Fixes #27004; ACCEPT 6460589730; Clause-②: yes (widening)). It ran at the head of the queue as the p0 fix-forward. GitHub closed the card as completed.

    Reading Probe Result
    Queue membership git ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-27073-*' 0 refs (left the queue)
    Ancestry git merge-base --is-ancestor 693a7298 origin/main holds
    origin/main by content packages/objectql/src/engine.ts carries probeReferencingRowsOfSet and REFERENCE_PROBE_BATCH

    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:p0Critical: blocker, must ship before MVP

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions