Skip to content

driver-memory 的两个过滤面对**比较值的种类**给出相反答案:{f: /re/}(正则)与 {f: ['x']}(数组)各差一个方向 —— 实测 #5354

Description

@os-zhuang

#5324 / #5328 时给两个过滤面装上共用的形状门,顺手把一批比较值种类喂给两个面做对读,发现两处语义分叉(形状门不碰,也不该碰)。按 Prime Directive #10 单独记在这里,unassigned。

实测原文

单行数据 { id: '1', stage: 'won', score: 10 },InMemoryDriver.find vs memory-matcher.match,worktree 基于 c7406b0ec + #5324 的修复:

                     live(mingo)   ref(matcher)
{ stage: /WON/i }    ["1"]         []          >>> 分叉
{ stage: ['won'] }   []            ["1"]       >>> 分叉

两条方向相反,而且都不涉及 null / 缺字段(那一族是 #5299)。

机制

1. RegExp 比较值 —— mingo 当正则匹配,匹配器当结构相等

mingo 按 MongoDB 语义把 {field: /re/} 读作正则匹配。memory-matcher.checkCondition 的分派只豁免 DateArray:

if (typeof condition !== 'object' || condition === null ||
    condition instanceof Date || Array.isArray(condition)) {
    return value == condition;          // ← RegExp 不在这里
}
const keys = Object.keys(condition);    // RegExp 的自有可枚举键是 []
const isOperatorObject = keys.some(k => k.startsWith('$'));   // false
return JSON.stringify(value) === JSON.stringify(condition);   // 'won' vs '{}' → false

于是 RegExp 掉进结构相等分支,JSON.stringify(/WON/i){},永远不匹配

顺带记录:memory-driver.tsnormalizeFilterCondition 明确把 RegExp 排除在算子对象之外并原样传给 mingo(!(value instanceof RegExp)),所以实时路径这一侧是有意支持的。

2. 数组比较值 —— 匹配器靠 JS 松散相等意外匹配

Array.isArray(condition)  return value == condition;

'won' == ['won'] 在 JS 里是 true(数组被 toString()'won')。所以匹配器把 {stage: ['won']} 读成了「等于 'won'」。mingo 按 MongoDB 语义读作「字段等于该数组,或字段是包含该元素的数组」,标量字段 'won' 都不满足 → []

匹配器这一侧几乎肯定不是有意设计的 —— ['won','lost'] 会因为 'won' == ['won','lost']('won,lost')而不匹配,而 ['won'] 匹配;这是 toString() 的巧合,不是任何一种「数组比较值」语义。

为什么是 bug

  1. 同一个包的两个面,同一个 filter,两个相反答案。 { field: {} }(零个操作符的字段约束)在同仓有三个答案:driver-sql 组合子内 TRUE、顶层抛 INVALID_FILTER、formula/driver-memory FALSE #5240 的裁决理由原话:「a backend whose two halves disagree about what a filter MEANS is exactly the divergence the ruling closes」。driver-memory 的实时查询路径根本不支持 $not —— mingo 抛无 code 的 MingoError,CEL !expr 降下来的 RLS scope 在该驱动上直接 500 #5324 已经把形状统一到一个门里,但语义仍是两份实现。
  2. 匹配器是 FILTER_LOGIC_CASES held 的那一面,也是 formula 对照的那一面;实时路径是用户跑的那一面。哪一面「对」需要裁决,但两面同时对两个答案都言之凿凿是明确的缺陷。
  3. {f: ['x']} 这种写法在 AI 生成的元数据里很容易出现(作者想写 $in 却写成了裸数组)。今天它在两个后端上得到两个答案,且都不报错。

为什么 #5324 没有一并修

那一单加的是形状门(词汇表、$between 元数、组合子操作数、{field:{}}),对语义一律不碰 —— 它的实现注释里写明了「It decides SHAPE, never MEANING」。这两条是同一个比较值在两个求值引擎里的不同读法,修它要先裁决哪个读法是 canonical,那是一次协议裁决而不是 shape gate 能顺手带走的。

建议(不代裁决)

  • RegExp 比较值:FilterConditionSchema 没有声明它($regex 才是声明的写法)。可选:(a) 匹配器补上 condition instanceof RegExp 分支,与实时路径取齐;(b) 两个面都拒收RegExp,要求写 {$regex: …} —— 与 driver-memory 的实时查询路径根本不支持 $not —— mingo 抛无 code 的 MingoError,CEL !expr 降下来的 RLS scope 在该驱动上直接 500 #5324 「未声明的一律拒收」同向,但会打断今天在实时路径上能用的写法(需先清点是否有生产者)。
  • 数组比较值:匹配器的 value == condition 松散相等几乎肯定该改。可选:(a) 与 mingo 取齐(等于数组,或字段数组包含该元素);(b) 两个面都拒收裸数组、要求 $in(formulaevalField 已经是这个态度:if (Array.isArray(spec)) return false,即 fail-closed)。注意 formula 在这一条上是第三个答案。

无论走哪条,建议与 #5299 / #5298 / #5239 一起看 —— 它们是同一族「一个 filter,N 个后端 N 个答案」的裁决。

未验证的部分

我没有测 driver-sql / driver-sqlite-wasm / driver-mongodb 在这两个输入上的答案(driver-sql 对裸数组比较值会走 coerceFilterValue → knex 绑定,预期报错但没实测);也没有清点仓内是否真有生产者产出裸 RegExp 或裸数组比较值。严重度请 PM 按 triage 定,不代表我判断它低。

关联:#5324 / #5328(实测出这两条的那一单;形状门已统一,语义未动)、#5299(同包两面在「字段没有值」上的三处分叉)、#5298#5239(一致性表)、#5240(两面必须对 filter 的含义一致)。

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions