fix(driver-memory): analytics 面拒收编译不了的过滤器,而不是静默丢掉 (#5345) - #5375
Merged
Conversation
…pile instead of dropping it (#5345) `MemoryAnalyticsService` lowers `AnalyticsQuery.where` into a flat cube-style `{member, operator, values}` list, and answered everything that did not fit with `continue` — `$or` and `$not` wholesale, plus the five declared operators with no row in the mongo→cube table (`$between`, `$startsWith`, `$endsWith`, `$null`, `$regex`). A comment presented this as a feature: "ignore so a partial query still runs rather than failing entirely" — the identical reasoning ADR-0078 / #4286 judged wrong on `objectql`'s `having`. Deleted along with the behaviour it justified. The direction is what makes it a defect: a dropped predicate is FEWER constraints, therefore MORE rows. Measured on `FILTER_LOGIC_CASES`, 15 of its 17 cases returned a wider row set than the standard specifies, usually every row. `$not` makes it more than a wrong number — `cel-to-filter.ts` compiles a CEL `!expr` RLS read scope into `{$not: {…}}`, so the dropped branch was the read scope itself. Route 1 of the issue's two: refuse, do not backfill the cube pipeline — where #5366 and #5368 put the two neighbouring faces. Implementation reuses #5349's primitives rather than adding a second validator. `assertFilterConditionShape` now takes the calling face's `FilterFaceCapabilities` — what that face can COMPILE, distinct from what the protocol declares — and refuses the difference through `unsupportedFilterError` (INVALID_FILTER / 400). The query path and the matcher pass nothing and are byte-for-byte unaffected. The analytics face derives its capabilities from its own `MONGO_TO_CUBE_OPERATOR` table, so widening what it accepts and teaching it to compile the operator are now one edit. Two refusals distinct on purpose: an operator the protocol never declared is still `unknownFieldOperatorError` ("you have a typo"), one it declares and this face cannot lower is the new `uncompilableFieldOperatorError`. The gate runs in `normalizeFilters`, before any lowering, for the reason that module already documents — a refusal raised mid-lowering fires or not depending on key order. Both public entry points (`query()`, `generateSql()`) go through it. The two former `continue` sites now throw; the operator one is reachable via the nested-relation branch, which re-enters with a synthesised node the gate never walked. Tests: `FILTER_LOGIC_CASES` now covers this third face — the package had three filter surfaces and the shared table watched two. It cannot pass row-for-row (a cube pipeline has no `$or`), so it is held to the predicate that actually matters: agree with `find()`, or refuse with INVALID_FILTER, never a third quieter answer. Reverting only `memory-analytics.ts` fails 16 of the new assertions. Plus a dedicated suite asserting the envelope (code, status) and that each refusal names the offending operator or combinator. Out of scope, filed not fixed: #5373 (the cube comparand round-trip loses booleans and `null`), #5374 (`$notContains` lowers to a bare mingo `{$not: 'x'}` that constrains nothing). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pbu27iNUfQCHeuS551Rqo7
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 9 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
marked this pull request as ready for review
August 5, 2026 10:47
This was referenced Aug 5, 2026
This was referenced Aug 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #5345
走 issue 给的第一条路:拒收,不去补齐 cube 管线。这也是今晚另外两个面的落点(#5366 把 analytics normalizer 的拒收送到调用方,#5368 让四个 driver 在门口拒收越界输入),把
$or补进 cube 模型是大得多的改动,而且未必表达得出来。问题
memory-analytics.ts把AnalyticsQuery.where摊平成 cube 风格的{member, operator, values}列表,凡是塞不进这个形状的一律continue:$or/$not整条丢,以及 mongo→cube 映射表里没有的五个已声明算子($between、$startsWith、$endsWith、$null、$regex)一条条丢。注释还把它写成了一个特性 —— "ignore so a partial query still runs rather than failing entirely" —— 正是 ADR-0078 / #4286 在objectql的having上判定为错误的那条理由。这句注释和它辩护的行为一起删掉了。方向是放大,不是缩小。 少一个约束 = 少过滤 = 多返回行。在共享的
FILTER_LOGIC_CASES上实测,17 条用例里有 15 条返回了比标准更宽的行集,多数是全表。$not还不止是数字不准:cel-to-filter.ts把 CEL!expr的 RLS read scope 编译成的就是{$not: {…}},丢掉的那一支就是 read scope 本身 —— 聚合里混进了调用方无权读的行。改法
复用 #5349 的既有原语,没有新起一套。
assertFilterConditionShape现在接受调用面的FilterFaceCapabilities(这个面能编译什么,与协议声明了什么是两回事),差集经unsupportedFilterError拒收(INVALID_FILTER/ 400)。query path 和 reference matcher 不传这个参数,行为逐字节不变。两类拒收刻意分开:协议根本没声明的算子仍然是
unknownFieldOperatorError(「你拼错了」),协议声明了而本面编译不了的是新的uncompilableFieldOperatorError。把两者合并等于告诉仪表盘作者他的$between是个笔误。analytics 面的能力集从它自己的
MONGO_TO_CUBE_OPERATOR表派生,于是「放宽本面接受的输入」和「教会它编译这个算子」变成同一处修改 —— 手抄一份词表只在敲下去那天和编译器一致。门放在
normalizeFilters,在任何一个 key 被摊平之前,理由是那个模块自己已经写明的:摊平过程中途抛出的拒收,会因 key 顺序和先走了哪个兄弟分支而时灵时不灵。两个公开入口(query()/generateSql())都走这里。原来两处continue现在都改成 throw —— 其中算子那处是可达的:嵌套关系分支会用一个门没走过的合成节点{'a.b': spec}重新进入。测试
把 analytics 面纳入
FILTER_LOGIC_CASES的穿透范围 —— 这个包有三个过滤面,共享一致性表原先只盯住两个,而让这个缺陷活下来的正是一条缺失的断言。cube 管线里没有$or,所以这个面不可能逐行通过表;它被钉住的是真正要紧的那条谓词:这条谓词正是两个
continue违反的东西,而且将来 cube 管线真学会了$or,用例只是从「拒收」栏移到「一致」栏,本文件不用改。回归证据:只把
memory-analytics.ts还原成修复前,新断言失败 16 条。顺带看到一个值得记下的巧合 ——a $or nested under a top-level $and修复前也是「对」的,因为它被丢掉的$or恰好被一个幸存的兄弟 key 覆盖了。数字看着对,从来不是证据。另有一个专门的 suite 断言拒收信封(
code/status)本身,以及每条拒收都点名了是哪个算子/组合子被拒 —— 仪表盘作者要能看出是哪条谓词出了问题。Changeset
minor,不是 patch。这是已发布面上可观察的行为变更:一个where里带$between或$or的部件,以前静默返回被放大的聚合值,现在报错。changeset 正文把每种形状的改写方式($between→ 两个边界、$null→$exists: false、$or/$not→ 走find())都写全了,因为这段文本会作为CHANGELOG.md随包发出去。与 #5368 用 patch 的区别在于:那一单拒收的是协议从未声明的形状(执行既有契约),这一单拒收的是协议声明了、
find()今天就在跑的形状,是本面契约的收窄。范围外 —— 已单独立单,未在本 PR 修
逐个实测「本面声明支持的 11 个算子各自编译成什么」时撞到的,都不属于 #5345 的范围面(它裁的是没有映射的算子):
{is_active: true}取到 0 行),null比较数被整条丢掉(取到全表) #5373 —— cube 比较数往返有损:stringifyForCube(true)→'1'→coerceFilterValue('1')→ 数字1,于是{is_active: true}(规格 docstring 自己的示例)取到 0 行;{field: null}被raw == null整条丢掉,取到全表。$notContains编译成裸 mingo{$not: 'x'},该谓词不约束任何行 —— 结果被放大到全表 #5374 ——$notContains映射到裸 mingo{$not: 'x'},该谓词不构成约束,取到全表。两条都在 #5345 之后依然被
ANALYTICS_FILTER_CAPABILITIES声明为「本面支持」,所以在MONGO_TO_CUBE_OPERATOR的 TSDoc 上留了一句指向它们的话:表里有一行只代表这个面会去尝试该算子,不代表它编译出的谓词是对的,别把这 11 个读成「11 个已知可用」。另按范围围栏未触碰:
service-analytics的对应面(#5325 在飞)、$exists三答案分叉(#5369)、cloud 的RemoteTransport(cloud#1077 / cloud#1116)。本 PR 的 diff 只落在driver-memory,packages/spec一个字节未动。(构建
packages/spec时gen:schema会把authorable-surface.base.json的baseRev重锚到当前分支的 merge base —— 这是它在任何分支上的正常副产物,不属于本 PR,已还原。)🤖 Generated with Claude Code
https://claude.ai/code/session_01Pbu27iNUfQCHeuS551Rqo7
Generated by Claude Code