修 #5240 (四后端对 { field: {} } 拒收)时,为了确认 driver-memory 那一行的现场而实测了驱动的真实 查询路径,发现这一处不属于 #5240 范围的缺陷。按 Prime Directive #10 单独记在这里,unassigned。
现象
InMemoryDriver.find 并不走 memory-matcher.ts 的 match() —— 它只从那个文件 import 了 getValueByPath,过滤实际由 convertToMongoQuery() → mingo 完成。而 mingo 是 MongoDB 语义的实现,MongoDB 没有文档级 $not ,于是:
WHERE {"$not":{"stage":"won"}} => THROW MingoError | code= undefined | status= undefined | unknown top level operator: $not
WHERE {"$or":[{"$not":{"stage":"won"}}]} => THROW MingoError | code= undefined | status= undefined | unknown top level operator: $not
WHERE {"$and":[{"$not":{"stage":"won"}}]} => THROW MingoError | code= undefined | status= undefined | unknown top level operator: $not
(实测原文,InMemoryDriver + syncSchema('deal') + 两行数据,worktree 基于 26e1029f5。)
normalizeFilterCondition 对 $not 只是递归归一化后原样透传 (result[key] = ...),没有任何 De Morgan 改写,所以 $not 在任何位置 都失败,不只是顶层。
为什么是 bug
$not 是已声明的协议算子。 spec LOGICAL_OPERATORS 声明它;driver-sql 实现它([driver-sql] 未知查询 operator(is/is_null)静默透传返整表,应报错;缺 $null 处理 #2704 加的分支,$not 的语义在 driver-sql 与 driver-memory / formula 之间分叉:NULL 行的去留相反,$not: {} 一个是 TRUE 一个是 FALSE #5146 又把它改成 NULL-safe);driver-mongodb 专门有一层翻译(mongodb-filter-logic-translation.test.ts)正是为了绕开「MongoDB 没有文档级 $not」;memory-matcher.match 自己也实现了它。四个后端里只有 driver-memory 的实时路径 没有。
它落在权限路径上。 cel-to-filter.ts 把 CEL !expr 编译成 { $not: {...} },这是 RLS read scope 的常规产物。driver-memory 是默认的开发/测试驱动 —— 一条带否定的 scope 在它上面不是「多返回/少返回几行」,而是整个查询抛异常。
异常逃出了 ADR-0112 信封。 MingoError 没有 code 也没有 status,于是 mapDataError 走 default 分支,客户端拿到的是 500 形状的裸 { error } —— data: unsupported-filter-operator refusal ships without error.code and leaks the [sql-driver] prefix #4436 为这个驱动建立的 INVALID_FILTER / 400 envelope(见 memory-filter-refusal-envelope.test.ts)对这一类完全没覆盖到。
为什么至今没被测出来
FILTER_LOGIC_CASES 对 driver-memory 只经由 memory-matcher (memory-matcher-or-semantics.test.ts)跑,那是参考实现 ,不是驱动的实时路径;而 driver-sql / driver-sqlite-wasm / driver-mongodb 三家的同名 conformance 都是穿过真驱动 跑的。表里第 $not ANDs with its sibling keys inside a branch 条(filter-logic-conformance.ts:172)因此从未在 InMemoryDriver.find 上执行过一次。
即:一致性表覆盖了这个后端的一半,而没覆盖的那一半正好是用户真正用到的那一半。
建议(不代裁决)
两条路都要维护者/spec 车道拍板方向,故只列不选:
另外无论走哪条,建议补一条 FILTER_LOGIC_CASES 穿过 InMemoryDriver.find 的 conformance 套件 —— 覆盖缺口本身比这个 $not 更值钱(可与 #5239 同批考虑)。
未验证的部分:我没有测 driver-mongodb 的翻译层是否已经覆盖全部 $not 形状,也没有量化今天有多少 RLS 规则真的编译出 $not。严重度请 PM 按 triage 定,不代表我判断它低。
关联:#5240 (实测出这一条的那一单)、#5146 (driver-sql 的 NULL-safe $not)、#5297 (service-analytics 的 $not 分叉)、#5239 (一致性表)、#4436 (driver-memory 的 refusal envelope)。
修 #5240(四后端对
{ field: {} }拒收)时,为了确认 driver-memory 那一行的现场而实测了驱动的真实查询路径,发现这一处不属于 #5240 范围的缺陷。按 Prime Directive #10 单独记在这里,unassigned。现象
InMemoryDriver.find并不走memory-matcher.ts的match()—— 它只从那个文件 import 了getValueByPath,过滤实际由convertToMongoQuery()→ mingo 完成。而 mingo 是 MongoDB 语义的实现,MongoDB 没有文档级$not,于是:(实测原文,
InMemoryDriver+syncSchema('deal')+ 两行数据,worktree 基于26e1029f5。)normalizeFilterCondition对$not只是递归归一化后原样透传 (result[key] = ...),没有任何 De Morgan 改写,所以$not在任何位置都失败,不只是顶层。为什么是 bug
$not是已声明的协议算子。 specLOGICAL_OPERATORS声明它;driver-sql 实现它([driver-sql] 未知查询 operator(is/is_null)静默透传返整表,应报错;缺 $null 处理 #2704 加的分支,$not的语义在 driver-sql 与 driver-memory / formula 之间分叉:NULL 行的去留相反,$not: {}一个是 TRUE 一个是 FALSE #5146 又把它改成 NULL-safe);driver-mongodb 专门有一层翻译(mongodb-filter-logic-translation.test.ts)正是为了绕开「MongoDB 没有文档级$not」;memory-matcher.match自己也实现了它。四个后端里只有 driver-memory 的实时路径没有。cel-to-filter.ts把 CEL!expr编译成{ $not: {...} },这是 RLS read scope 的常规产物。driver-memory 是默认的开发/测试驱动 —— 一条带否定的 scope 在它上面不是「多返回/少返回几行」,而是整个查询抛异常。MingoError没有code也没有status,于是mapDataError走 default 分支,客户端拿到的是 500 形状的裸{ error }—— data: unsupported-filter-operator refusal ships without error.code and leaks the [sql-driver] prefix #4436 为这个驱动建立的INVALID_FILTER/ 400 envelope(见memory-filter-refusal-envelope.test.ts)对这一类完全没覆盖到。为什么至今没被测出来
FILTER_LOGIC_CASES对 driver-memory 只经由memory-matcher(memory-matcher-or-semantics.test.ts)跑,那是参考实现,不是驱动的实时路径;而 driver-sql / driver-sqlite-wasm / driver-mongodb 三家的同名 conformance 都是穿过真驱动跑的。表里第$not ANDs with its sibling keys inside a branch条(filter-logic-conformance.ts:172)因此从未在InMemoryDriver.find上执行过一次。即:一致性表覆盖了这个后端的一半,而没覆盖的那一半正好是用户真正用到的那一半。
建议(不代裁决)
两条路都要维护者/spec 车道拍板方向,故只列不选:
normalizeFilterCondition里对$not做 De Morgan 下推(driver-mongodb 已有先例可抄),顺带把$not的语义在 driver-sql 与 driver-memory / formula 之间分叉:NULL 行的去留相反,$not: {}一个是 TRUE 一个是 FALSE #5146 的 NULL-safe 极性一并对齐(否则会新增一处与 driver-sql 的分叉,参见 read-scope-sql 的$not有两处与 SQL 驱动分叉:非 NULL-safe(#5146 后的最后一个异类),且{ $not: {} }编译成空 → RLS 整表放行 #5297 在 service-analytics 上的同款)。INVALID_FILTER/ 400,至少把 500 形状变成 catalogued 400;但这等于宣布 driver-memory 不支持一个已声明算子,与「参考实现」的定位冲突。另外无论走哪条,建议补一条
FILTER_LOGIC_CASES穿过InMemoryDriver.find的 conformance 套件 —— 覆盖缺口本身比这个$not更值钱(可与 #5239 同批考虑)。未验证的部分:我没有测
driver-mongodb的翻译层是否已经覆盖全部$not形状,也没有量化今天有多少 RLS 规则真的编译出$not。严重度请 PM 按 triage 定,不代表我判断它低。关联:#5240(实测出这一条的那一单)、#5146(driver-sql 的 NULL-safe
$not)、#5297(service-analytics 的$not分叉)、#5239(一致性表)、#4436(driver-memory 的 refusal envelope)。