Repository navigation
fix(driver-memory): the cube face lowers the FilterArray spelling of where instead of answering every row - #20944
Conversation
An array `where` at MemoryAnalyticsService answers every row and the generateSql echo carries no WHERE, while the object spelling of the same filter answers its rows. These pins hold the lowering the next commit adds: both spellings answer the same rows and echo, `[]` stays no filter, and an array the face cannot lower is refused INVALID_FILTER / 400 on both exits. Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude <noreply@anthropic.com>
…`where`
MemoryAnalyticsService.normalizeFilters read only a non-array object `where`,
so `[['d', '=', 'v1']]` skipped the comparand doors, the shared lowering, the
vocabulary gate and the flatten: query() aggregated every row and the
generateSql() echo carried no WHERE, while `{ d: 'v1' }` answered its row.
The array is now lowered at the method's entry by the spec's own pair, the one
the analytics `where` door and the engine call: isFilterAST gates the shape and
parseFilterAST lowers it, so the array reaches the doors as the
FilterCondition it spells. `[]` stays "no filter"; an array with no lowering
(the infix join, an unknown operator, a list of scalars, a cube-style entry
list) is refused INVALID_FILTER / 400 on both exits instead of answering every
row.
Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY
Co-authored-by: Claude <noreply@anthropic.com>
… `where` Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude <noreply@anthropic.com>
…be-filter-array-lowered
📓 Docs Drift Check5 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 — 9 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 eb65f705ae7da7b9de4649e9931b515ab32645b5 && git checkout eb65f705ae7da7b9de4649e9931b515ab32645b5
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3fbf3ca6173e80dc515448fd0df497dfb3aed29c 8d585eadd1eeae6355cd641d08d54418b6e21576 && git checkout -B drift-repro 3fbf3ca6173e80dc515448fd0df497dfb3aed29c && git merge --no-ff 8d585eadd1eeae6355cd641d08d54418b6e21576
node scripts/docs-audit/affected-docs.mjs --json 3fbf3ca6173e80dc515448fd0df497dfb3aed29c |
…wing A `where` array isFilterAST rejects used to answer every row on query() and generateSql() and now answers INVALID_FILTER / 400, so the driver-memory changeset moves to minor with a BREAKING paragraph naming the refused shapes, the `Clause-②: no (narrowing)` line, and one ADR-0087 disposition (not-required, no-migration-prescription). No code or pin changes. Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs: card #20859 (body and its six comments: triage 5912813886, release 5913765727, claim 5918346956, Check-runs on the head, read once at 2026-09-30T21:53Z and not polled: 40 runs, none failed. In progress at that moment: ① Derived judgmentsRead against the spec pair (
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #20859
Clause-②: no (narrowing)
What changed
MemoryAnalyticsService.normalizeFilters(packages/drivers/driver-memory/src/memory-analytics.ts) read only a non-array objectwhere. AFilterArraywheretherefore skipped the shared comparand doors, the shared lowering, the vocabulary gate and the flatten, so its predicate vanished.The array is now lowered at the method's entry by a module-private
lowerWhereFilterArray, which calls the spec's own pair:isFilterASTgates the shape andparseFilterASTlowers it. That is the pair the analyticswheredoor (lowerAnalyticsWhere, see #5334) and the engine'slowerWhereFilterArraycall. The face adds no parser of its own. The lowered condition then enters the step-3 door (#20857) exactly as an authored object does. So both spellings meet the same comparand doors, the samelowerFilterConditionand the same gate, and both exits (query()andgenerateSql()) read the same normalized list.The three arrival answers are the analytics
wheredoor's:[]isFilterASTacceptsparseFilterAST, then the door, the lowering and the gate, like the object spellingINVALID_FILTER/ 400 on both exitsMeasured
On the fixture from the card (
dis'v1','v2',null, absent):whered78a0bda07)ca8448c005){ d: 'v1' }d = 'v1'd = 'v1'(control, unchanged)[['d', '=', 'v1']]d = 'v1'[][['d', '=', 'v1'], 'or', ['d', '=', 'v2']](infix)INVALID_FILTER/ 400[['d', 'in', 'v1']](scalarin)INVALID_FILTER/ 400, as{ d: { $in: 'v1' } }isTests
New pin file
packages/drivers/driver-memory/src/memory-analytics-filter-array-where.test.ts, 16 cases:FilterConditionit lowers to (a comparison, a flat list, anandgroup, anorgroup, a negative-polarity leaf,is_null,is_not_null);[]control;Refusals assert the envelope (
code+status), not the prose.62eff2a8f9(pins only, source at the base), the file gave 15 failed, 1 passed. The one pass is the[]control. The card's row read rows['1','2','3','4']and echoundefined.f2be8432cd.scripts/ablation-replace.mjsreplacedconst where = lowerWhereFilterArray((query as { where?: unknown }).where);with the pre-fix read. The anchor went from 1 to 0, and the blob movedcc0aaa764022to3445aece6953. Result: 15 failed, 1 passed (the[]control). The restore was proven: the blob equals HEADcc0aaa764022, andgit diff HEADis empty. The pins import./memory-analytics.jsby relative path, and the package's vitest config aliases@objectstack/*to source, so no build sat between the mutation and the reading.ca8448c005, afterpnpm --filter '@objectstack/driver-memory...' build, thedriver-memorysuite gave 66 files and 1429 tests passed.typecheckexited 0, andtsc --noEmit --listFilesincludes the new test file.ca8448c005:dispatch-gates --commandsderived 60 families, and all 60 were run.check:dual-build-cjs-loads,check:lean-entry-closureandcheck:type-check-debt. After a fullturbo run build(72 tasks) they were re-run, and all three passed.dispatch-gates --ranreported 60 derived, 60 run, 0 NOT-MEASURED, 0 UNRUN.check-changeset-fixed,authz-resolver,error-code-casing,filter-alias-parity,object-def-param-keys,tenant-chokepoint.eslint.config.mjs's**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}block.eslint --no-inline-config --format jsonover the two changed TypeScript files: 2 files, 0 errors, 0 warnings, atca8448c005.Acceptance notes
isFilterASTrefuses (the infix join, an unknown operator, a list of scalars, a cube-style entry list) used to answer every row and now answersINVALID_FILTER/ 400. None of those shapes is admitted byAnalyticsQuerySchema.where(FilterConditionSchema) or byFilterArraySchema, and the triage direction rules out dropping. Answered by the seat (5920030329, B, open to veto): the face's accept set narrows, as it did on PR feat(service-analytics,driver-memory): the shared filter lowering at the analytics seams and the cube face's new door (#5930 step 3) #20857 ([finding] 仓内存在 5 个独立的过滤器→谓词编译器,每次语义裁决成本 ×5 —— 值得立「谓词编译收敛」调查程序(#5298 成本清单副产品) #5930 step 3) on this same face, so this PR takesClause-②: no (narrowing). The changeset isminorwith a BREAKING paragraph and one ADR-0087 marker; the claim's "nothing is newly refused" was the seat's error.parseFilterAST, then at the door. Both are idempotent (an assertion, and a copy-on-write narrowing), so the verdict is the same one the first pass gives.normalizeFilters' object branch still carries its!Array.isArray(where)test. After the lowering it is always true for anything that reaches it, and it is kept as the object branch's own statement of what it reads.wheredoor's and the engine's non-lowerable-array refusals render the value with a bareJSON.stringify. Abigintinside such an array would make that call throw a rawTypeErrorinstead of the envelope. This face's message renders through a non-throwing preview. No JSON wire can carry abigint.Generated by Claude Code