Repository navigation
fix(driver-memory): a composed ReadableIds leaf hashes its readable ids once per query, not once per row (ADR-0139 D3) - #23695
Conversation
…t once per row mingo's `$in` rebuilds a hash map of the whole list for every document it tests, so a read over R rows with an N-member list cost R x N inserts. The composed ReadableIds leaf (ADR-0139 D3) lowers into exactly such a list, as long as the readable parent set; the E3 conformance pins sat at 2-3 s per test against vitest's 5 s default and timed out under CI's per-core worker share. The driver now evaluates through its own mingo context whose `$in` is mingo's predicate with the list hashed when the query compiles. Claude-Session: https://claude.ai/code/session_01GP3comALHRcRBixS2TEnLF Co-authored-by: Claude <noreply@anthropic.com>
…adableIds leaf Every other `$in` stays mingo's own operator. `readableParentIds` marks the list it returns (object identity, a WeakSet), the `$in` arm lowers only a marked list to a private member-test operator that hashes it once when the query compiles, and the driver's `Query` adds that operator to mingo's default context. The operator name is refused by the filter shape gate, so it cannot be authored. The pins move to the leaf: hash-insert counts on find, count, aggregate, distinct and under $not/$or, the write doors' answers, the member test against mingo's `$in` over a case matrix, and the shape gate's refusal of the private name. Claude-Session: https://claude.ai/code/session_01GP3comALHRcRBixS2TEnLF Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check8 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 10 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 e206e6ce2766c38ceb19bf5215a27b79214ae8eb && git checkout e206e6ce2766c38ceb19bf5215a27b79214ae8eb
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin cbe771270680807ac7c1fb6ac78af20cfbb74a37 af4bdb733a5c3dcd85c7b3f3c5bf3a257793add6 && git checkout -B drift-repro cbe771270680807ac7c1fb6ac78af20cfbb74a37 && git merge --no-ff af4bdb733a5c3dcd85c7b3f3c5bf3a257793add6
node scripts/docs-audit/affected-docs.mjs --json cbe771270680807ac7c1fb6ac78af20cfbb74a37 |
席位核对 ·
|
| Item | Reading |
|---|---|
| Shape | Draft PR → main. The first line is Fixes #23679. A closing-keyword scan of the whole body finds one hit, line 1. #23685 is named only as "left open here". |
| Scope | 4 files (+354 / −3), all in packages/drivers/driver-memory plus one changeset: the claim's surface (6194432060). No content/docs/releases/ edit, no governed path, under the 3,000-line threshold. The branch merges clean with origin/main cbe7712706 (git merge-tree). |
| #5499 freeze | Held. The change sits inside ADR-0139 D3's row only: readableParentIds marks the list it returns, and only a marked list lowers to the private $readableIdsMember. Every other $in is still mingo's. The first commit's general $in override (9bf3629df) is undone by af4bdb733 before the squash, per the seat's scope note. The PR body quotes #5499's restore-invariant exception for a CI-green defect. |
Clause-② |
no, on the claim, the PR body and the changeset. Answers are unchanged, and the matrix pins equality with mingo's $in (13 lists × 13 values × 4 selector shapes). The private operator is not authorable: the shape gate refuses it with INVALID_FILTER / 400, which is pinned. index.ts exports nothing new. |
| Changeset, sentences read | One patch row for @objectstack/driver-memory. Its title line is "a composed ReadableIds leaf hashes its readable ids once per query, not once per row", with Before / After / Answers unchanged / Scope bullets that match the diff. |
| Seat's read of the diff | Query is now the driver's subclass at every call site in memory-driver.ts (find, count, distinct, the write doors, aggregate's where pre-filter, and readableParentIds). Aggregator keeps mingo's default context. A raw pipeline $match never carries the private operator, because only convertToMongoQuery lowers a marked list. A miss would fail loudly ("unknown query operator"), never answer silently. |
| Root cause | Posted on the card before any code change (6195291471): reproduced 2 of 2 at the shard's CPU share, and the mechanism was measured (mingo's $in re-hashes the list per row, R × N inserts). |
| Negative control | Ablation of the lowering branch turns red exactly the 6 cost pins (expected 200400 to be less than 20000), and the E3 pins go back to 1.7–2.5 s. Restore is clean. The pins count HashMap#set calls, with no wall clock. |
| Gates | The dev's derived union: 62 families, 61 exit 0, and 1 NOT MEASURED locally (check:dual-build-cjs-loads, which needs every package's dist; CI's Lint & Repo Gates runs it). The declared narrowing (package suite, one-core shape, gates; the farm to CI) was the seat's instruction. CI was running at this read; landing needs every check green or on the expected-skip roster. |
After landing. The seat reads the first hourly CI schedule run on main after the merge commit, including the Test Core shard that holds driver-memory. Then it settles #23685 (the same timeout) with a pointer to this PR.
Disposition:
out_of_scope_findings[0](any other large$in/$ninlist re-hashes per row too): Acceptance notes. No producer is measured, and [裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499 freezes the package, so there is zero pull.deviations: the first commit's wide override, narrowed before the squash, is accepted. The model-free commit trailers are accepted.
Generated by Claude Code
|
Landed ·
Status: #23679 is closed, and its
Generated by Claude Code |
Fixes #23679
Clause-②: no
main's hourly full run 38739484445 at8d1fb1fb2dwent red inTest Core (4/6). The red was in the whole-packages leg, in@objectstack/driver-memory'stest. This PR removes the cause, which is in the driver. It does not touch the test's timeout, the battery or the shard.#23685 is the same timeout, read from the other side. It is left open here; the seat settles it after this PR lands.
Root cause
The full reading is comment 6195291471 on the card. Measured at
8d1fb1fb2d:memory-readable-ids-conformance.test.ts›E3 pins:a deleted parent is excluded, on find, count and aggregate alikeanda reader with 2400 readable candidates gets every visible row, and count equals the rows served. Each fails withError: Test timed out in 5000ms., vitest's default.cache miss, executingeach time): 0 red.taskset -c 0 … vitest run --maxWorkers=3): 2 red. Both times it was exactly these two tests, at 6.5–10.7 s.convertToMongoQueryrewrites a composedReadableIdsleaf into the ids of every readable parent row (ADR-0139 D3, from feat(drivers): ADR-0139 E3 + E4 — the five drivers compile the composed ReadableIds leaf or refuse it with D3's envelope, with the conformance case-set #23656), and handed mingo that list as a plain$in.$inrunsintersection([value, list])for every document it tests, andintersectionbuilds a hash map of the whole list on each call. So one read costs R rows × N readable parents in hash inserts.The fix: inside D3's composed-leaf path only
readableParentIdsmarks the list it returns: object identity, held in aWeakSet.$inarm ofnormalizeFieldOperatorslowers a marked list to a private mingo operator,$readableIdsMember(memory-readable-ids-member.ts). It is mingo's$inpredicate with the list hashed once, when the query compiles. Nothing else changes.Queryknows the operator. It is a subclass of mingo'sQuerythat adds this one operator to mingo's default context. mingo passes the same options to the sub-queries it builds for$and,$orand$nor, so a leaf under any combinator compiles.$inis untouched. It is still mingo's own operator, andAggregatorand the analytics pipeline are unchanged. The private name cannot be authored: the filter shape gate refuses it (INVALID_FILTER/ 400, pinned).Landing point:
packages/drivers/driver-memory, as claimed. #5499 freezesdriver-memory. Its own exception covers this case, verbatim: "if the driver-memory defect affects CI green correctness (unit-test backend semantics causing false green/red), handle it as restore-invariant". A red hourly full run onmainis that case. The change stays inside ADR-0139 D3's row: the composed-leaf rewrite that #23656 added.Pins: deterministic, with no wall clock
memory-readable-ids-hashed-once.test.tshas 24 tests.HashMap#setcounts the inserts during a composed-leaf read. On every door the count must stay under a tenth of R × N (R = 500, N = 400):find,count,aggregate,distinct, under$notand under$or.$inover 13 lists × 13 values × 4 selector shapes. The shapes are a top-level field, a missing field, a dotted path, and a dotted path through an array of subdocuments.updateManyanddeleteManyanswer a composed leaf as before.$into answer.The E3 pins themselves are unchanged.
Verification
All on this branch's head
af4bdb733, in a shared 4-vCPU container. Heavy runs went through the shared verify lock.Tests (
driver-memory):pnpm --filter @objectstack/driver-memory test: 72 files, 1756/1756 passed.8d1fb1fb2dthey took 1.7–2.1 s and 2.7–3.2 s.memory-operator-key-clobberat 0.4 s.taskset -c 0 … vitest run --maxWorkers=3): 2 runs, 0 red, 1756/1756 each. The two pins took 174–261 ms and 73–110 ms. Before the fix this shape went 2 of 2 red, at 6.5–8.9 s and 10.4–10.7 s.Ablation. The lowering branch was mutated with
scripts/ablation-replace.mjs: the anchor went from 1 hit to 0 and the blob changed. The subject resolves throughsrc/, so no build was involved.expected 200400 to be less than 20000, which is R × N plus one insert per matching row. Answers, matrix and not-authorable stayed green.d6609e52f3e2), andgit diff HEADis empty.Typecheck and build:
pnpm --filter @objectstack/driver-memory typecheck: exit 0, after the^buildclosure was built. The maintsconfig.jsoncompiles all 72 test files, the new one included, counted with--listFiles.turbo run build --filter=@objectstack/driver-memory...: exit 0.dist/index.jsthroughrequire,dist/index.mjsthroughimport) and answer a$inread.Gates.
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 62 families. Reconciled with--ran: 61 run, all exit 0, and 1 NOT MEASURED.check:dual-build-cjs-loadsis NOT MEASURED: it exits 3 withPREREQUISITE NOT METbecause it needs every package'sdist. CI'sLint & Repo Gatesruns it.check:lean-entry-closurefirst exited 3 for lack ofobjectql'sdist. Afterturbo run build --filter=@objectstack/objectql(a full cache hit), it exited 0.check:driver-conformance:OK — 55 covered cell(s), 0 in the DEBT ledger, 0 exempt.check:query-options-erasure: test surface 236, at the ceiling, no files added.check:nul-bytes: OK.Declared narrowing. Local verification is this package's suite and typecheck, the shape that reproduced the red, and the derived gates. The rest of the battery is CI's to run.
index.tsexports nothing new. So no importer owes a re-run.mainhas moved 4 commits since the basec24598658. None of them touchesdriver-memory,core,spec,typesor the lockfile, so the branch is not merged withmain.For the seat
CIschedule run onmainafter this PR's merge commit, and in it theTest Coreshard whose list holds@objectstack/driver-memory(shard 4/6 at8d1fb1fb2d; the partitioner can move it).patchfor@objectstack/driver-memory, because a published file changes.Acceptance notes
$inand$ninlists. mingo re-hashes any other$inor$ninlist in this driver once per row too, including a list a caller writes. That is outside this PR by the seat's scope ruling: only D3's row rides in this frozen package. No producer of a large such list was measured. Not filed; carrier: none.9bf3629df) replaced$infor every query. The seat's scope ruling arrived before review, andaf4bdb733narrows it to the D3 leaf. The squash lands only the narrowed diff, and every reading above is onaf4bdb733.Generated by Claude Code