Repository navigation
fix(objectql): a predicate delete walks the rows it matches as one set, one probe per relation per 500 rows (#27004) - #27073
Conversation
… per batch (red first) Claude-Session: https://claude.ai/code/session_01FEkQ5ktxGoJfsedLmkFjdk Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5ktxGoJfsedLmkFjdk Co-authored-by: Claude <noreply@anthropic.com>
…lete's matched rows; changeset Claude-Session: https://claude.ai/code/session_01FEkQ5ktxGoJfsedLmkFjdk Co-authored-by: Claude <noreply@anthropic.com>
…lection now sends per depth Claude-Session: https://claude.ai/code/session_01FEkQ5ktxGoJfsedLmkFjdk Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5ktxGoJfsedLmkFjdk Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 19 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 233a3cce0f447fab15f899aa3588adfbf74fb5a1 && git checkout 233a3cce0f447fab15f899aa3588adfbf74fb5a1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f708cf224a9c70447fd2030ad10596b28cc0edec cec640f0946b7fdc64ce4fbb9b3b3b1079bb56e3 && git checkout -B drift-repro f708cf224a9c70447fd2030ad10596b28cc0edec && git merge --no-ff cec640f0946b7fdc64ce4fbb9b3b3b1079bb56e3
node scripts/docs-audit/affected-docs.mjs --json f708cf224a9c70447fd2030ad10596b28cc0edec
|
席位核对ACCEPT · The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.
Generated by Claude Code |
Triage: p0 fix ready, not yet enqueued · triage seat ·
|
Seat note: already in the merge queue, at its head ·
|
Contract reviewServed-tier: Inputs: card #27004 (body and all 7 comments), the PR body and file list, the net diff of ① Derived judgmentsThe engine change is right. Each item names one change to the accept set or the public surface.
② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL Generated by Claude Code |
Requested by Leehom · project thread
Fixes #27004
Clause-②: yes (widening)
Metadata-protocol: none
维护者速读
multi: true加过滤条件,流程里的批量delete_record)不再对每条命中记录各跑一遍引用关系检查,而是把命中的记录当成一个集合,每个引用关系每 500 条只查一次。按 id 单条删除是“一条记录的集合”,走同一条路径。src/sql-driver.ts(22,096 lines, +2,533 in 30 days) along itsSqlDriverseams, one move-only slice per PR #25521 cut 19/20。现在同样的删除只发 12 次查询。[]、必填多值查找按行升级拒绝,这些结果逐条不变。唯一变化:命中的记录之间互相引用时(例如经理和下属都被这次删除命中,下属以 restrict 引用经理),现在视为同一个删除集合,不再拒绝;集合外的引用照旧拒绝。回滚 = revert 本 PR。packages/objectql/src/engine.ts的walkReferencingRelations(按集合探查后逐条判定,cleared记录已移除的多值成员)、probeReferencingRowsOfSet(分批探查与按 id 归属)、collectCascadeDeleteSet(按层批量收集)。pnpm --filter @objectstack/service-automation exec vitest run --maxWorkers=2 src/builtin/delete-record-predicate-walk-batched.integration.test.ts,预期 4 个测试全绿,第一个钉住依赖查询次数为 detail 6、link 3、tags 3。What changes
cascadeDeleteRelationstakes a ROOT SET. The by-id branch ofdelete()passes[id]; the predicate arm passes the ids it matched, once, with no per-row loop left. There is one walk; no new option onengine.delete.collectCascadeDeleteSetcollects breadth-first one depth at a time: the records found at one depth are probed together, per object and per cascading relation. Members and edges are the ones the record-by-record collection found, andrefuseRestrictsInsideOwnCascadereads its members in the same discovery order from the edges.probeReferencingRowsOfSet(new, private) is the dependents probe for a set: one read per 500 ids (REFERENCE_PROBE_BATCH, now also the predicate arm's$indelete batch),$inon a single-valued reference and an OR of each id's own$containslimbs on a multiple lookup (the spelling the nested-relation lowering already sends). Each row goes to the ids it references, by the 【缺陷】被 multiple:true lookup 指向的对象 REST DELETE 全 400——cascadeDeleteRelations 依赖探针用裸等值查 JSON 列 #9362 element reading; a batch of one sends exactly the by-id filter. A single-valued row whose value names none of the batch makes that batch fall back to one probe per id.walkReferencingRelationsprobes each relation once for the set, then judges and writes per record, in the set's order, with every rule as before (objectql: cascade delete trips a restrict on a record the same cascade was about to delete (registry-order, depth-first walk) #22305 set exemption, cascadeDeleteRelations refuses a required multi-value lookup delete even when member removal would leave the set non-empty #9688 per-row escalation with its count, engine: the one-definition-of-multi-valued invariant reaches packages/objectql and driver-turso — the domain:engine half of #18199's ruling #18408 member removal writing[], the 删除记录时「引用清理」用操作人身份查询引用表,读权不足即整体 403(应以系统身份执行) #12166 elevation records one per record per referenced object, the [Decision] Deleting an organization once organization_id is NOT NULL: refuse while it still owns records, or delete them with it #24948 organization check per record). NewCascadeDeleteSet.clearedrecords, per relation, the members whose walk has finished it, so a multiple-lookup row read once for the whole set has the earlier members left out of the remainder it is judged on and written with (remainderAfterMemberRemoval(stored, id, cleared)).refuseRestrictsInsideOwnCascadebatches its probes per relation over the members asking it, then judges in the same member and relation order as before.Tests
packages/services/service-automation/src/builtin/delete-record-predicate-walk-batched.integration.test.ts(new; realSqlDriveron better-sqlite3, the realdelete_recordexecutor), 4 cases:acc9c57dthe driver saw 2,002 / 1,001 / 1,001 dependents probes; on this head 6 / 3 / 3, and 3deleteManycalls.[].DELETE_RESTRICTED,dependentCount: 1, naming the record whose removal empties it (the same record the row-by-row walk named); nothing is deleted or cleared.restrictself-lookup: the delete goes through (count 2); a holder outside the matched rows still refuses.acc9c57dtoo (they pin unchanged outcomes); case 1 fails there; case 4 fails there (it pins the widening below).packages/objectql/src/engine-inspect-cascade-delete.test.ts: its test double now answers$in, because the collection probes the records of one depth together. No assertion changed.Acceptance notes
Clause-②: yes (widening)). One walk of one set makes the matched rows members of it. A matched row that only other matched rows hold (through arestrict, or a required lookup's escalatedset_null) refuses nothing, and a matched row another matched row'sset_nullpoints at is not cleared before it is deleted: the objectql: cascade delete trips a restrict on a record the same cascade was about to delete (registry-order, depth-first walk) #22305 rule a cascade already applies to the rows it deletes. The row-by-row walk refused the first case whenever it met the held row first, and cleared the second with anupdate(hooks and all) on a row about to go. A holder outside the matched rows still refuses, unchanged. The objectql: a predicate delete (multi: true with a filter) skips the referential walk, so a flow's bulk delete_record bypasses restrict, master_detail cascade and set_null #26871 changeset (still unreleased) said only that arestricton a matched row refuses.delete(), one per row, with their hooks and their own walk, as before; only the probes are batched.packages/objectql/src/engine*.ts(read beforepr_create); feat(spec): a hook declares its refusal catalogue, and its body refuses by code, answered in the caller's language #26831 merged and is in this head.Verification
Final head:
cec640f0(merge oforigin/mainatcb8988ee).acc9c57d2,002 / 1,001 / 1,001 dependents probes on the three relations; this head 6 / 3 / 3. On the base dist the pin file readsTests 2 failed | 2 passed (4)(the count and the set-exemption case); on this head4 passed.Test Files 476 passed (476),Tests 8888 passed (8888). Every existing walk pin (engine-cascade-delete-atomic,engine-cascade-delete*,engine-cascade-delete-sibling-restrict,summary-rollup,engine-nested-relation-lowering) is green with no assertion changed.delete-record-predicate-referential-walkand the fix(objectql): a predicate update or delete recomputes the parent roll-up summaries its matched rows name (#26950) #27000predicate-write-summary-recompute:Tests 12 passed (12).ef03ae64, built closures): the 40 test files in 13 packages that delete through a relation: metadata 22, metadata-protocol 246, plugin-audit 138, plugin-auth 47, plugin-hono-server 49, plugin-security 433, plugin-sharing 9, rest 221 (+12 skipped live cells), runtime 9, service-automation 69, service-messaging 20, service-settings 8, service-storage 141: all exit 0.pnpm --filter @objectstack/objectql typecheckandpnpm --filter @objectstack/service-automation typecheckexit 0; eslint on the three touched files exit 0.dispatch-gates.mjs --commandsderives, run on this head, every one exit 0;--ranreconciliation:65 derived famil(ies) accounted for — 65 run, 0 NOT-MEASURED.check-plugin-teardown-shape --self-testneeded the documented shallow-clone fetch first.The rest cap test (data-nested-object-door.test.ts "the cap: …"), alone, --maxWorkers=2, base and head objectql dist alternated, 5 runs each (shared box, gate loop running beside it)
Base 2,451 to 2,893 ms, head 306 to 419 ms: about 7.5× less. Before #26930 the test took about 0.1 s; what is left is the elevation ledger's 1,002 log lines and the matched-id read.
Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_01FEkQ5ktxGoJfsedLmkFjdk