做 #5374 时,为了确认新的谓词构造表在「比较数是数组」时的行为,逐个实测了标量算子收到数组比较数会怎样。这一条不属于 #5374 的范围面 —— #5374 裁的是算子映射到了错的目标,这一条在更上游的**降级(lowering)**这一层,#5374 修好之后依然存在。按 Prime Directive #10 单独记在这里,unassigned。
位置
packages/plugins/driver-memory/src/memory-analytics.ts 的 flattenFilterCondition(约 :595):
for ( const opKey of opEntries ) {
const cubeOp = this . mongoOperatorToCubeOperator ( opKey , key , `${ here } .${ opKey } ` ) ;
const v = wrapper [ opKey ] ;
out . push ( { member : key , operator : cubeOp , values : Array . isArray ( v ) ? [ ...v ] : [ v ] } ) ;
}
Array.isArray(v) ? [...v] : [v] 是无差别 的:它对 $in / $nin 是对的(那两个算子的比较数本来就是列表),对 $eq / $ne / $gt / $contains 等标量算子则把「一个数组值」摊成了「列表里的第一个元素」。下游取 values[0],数组性就此丢失。
现象(实测,在 #5374 修复之后)
3 行数据,qty 分别为 100 / 200 / 100(id 1/2/3):
where
find()
analytics
说明
{qty: {$eq: [100]}}
[]
[1,3]
数组比较数被摊成标量 100,于是匹配上了
{qty: {$eq: []}}
[]
[]
巧合一致:摊成 undefined 后同样取不到行
find() 的答案是对的:存的是数字 100,[100] 是一个数组,mingo 跨类型比较永不相等,所以 0 行。analytics 面把作者写的数组 当成了标量 ,回答了一个作者没有问的问题。
方向这次是收窄→放大都可能 :上表是放大(0 → 2 行),但 {qty: {$eq: [100, 200]}} 会静默丢掉 200,那是收窄。两个方向都是静默的。
为什么是 bug
它把一个「什么都匹配不上」的查询变成了「匹配上了」。 作者写 $eq: [100] 大概率是笔误(想写 $in),而正确的行为是照实回答 0 行 —— 让笔误可见。摊平之后笔误被修正成了另一个查询 ,作者永远看不到自己写错了。这是 Prime Directive Add comprehensive test suite for Zod schema validation #12 点名的「消费端宽容」形状:在消费端替生产者猜意图,把错误藏起来。
live query path 不这么做。 normalizeFieldOperators 的 case '$eq' 是 result[op] = store(val),数组原样进 mingo。所以又是本包两面对同一个 where 给出不同行集({ field: {} }(零个操作符的字段约束)在同仓有三个答案:driver-sql 组合子内 TRUE、顶层抛 INVALID_FILTER、formula/driver-memory FALSE #5240 )。
spec 侧允许这个输入。 FilterCondition 的 $eq 是宽类型,所以这个形状能通过 assertFilterConditionShape,不会在门口被拦下。
建议(不代裁决)
两条路,请裁:
倾向 B,因为「0 行」和「作者写错了」在图表上同样看不出区别,而 #5347 / #5328 已经为这一类建立了拒收的惯例;但 B 需要先确认 spec 是否有意允许 $eq: [...] 表达「等于这个数组值」(schemaless 存储里数组是合法列值),若有意,则只能走 A。
未验证的部分
没有测 $contains / $gt 等其余标量算子收到数组时的行为,只测了 $eq / $ne。机制是共用的一行代码,推断其余相同,但未实测。
没有确认是否存在真实调用方依赖当前的摊平行为(例如某个把 $in 写成 $eq 的 widget 元数据)。改之前值得 grep 一遍。
关联:#5374 (同文件、同一轮核对里发现;它裁的是算子→谓词层,本条在降级层)、#5347 / #5328 (同类「形状不对就拒收」的先例)、#5240 (本包两面不得分叉)、#5373 。
做 #5374 时,为了确认新的谓词构造表在「比较数是数组」时的行为,逐个实测了标量算子收到数组比较数会怎样。这一条不属于 #5374 的范围面 —— #5374 裁的是算子映射到了错的目标,这一条在更上游的**降级(lowering)**这一层,#5374 修好之后依然存在。按 Prime Directive #10 单独记在这里,unassigned。
位置
packages/plugins/driver-memory/src/memory-analytics.ts的flattenFilterCondition(约 :595):Array.isArray(v) ? [...v] : [v]是无差别的:它对$in/$nin是对的(那两个算子的比较数本来就是列表),对$eq/$ne/$gt/$contains等标量算子则把「一个数组值」摊成了「列表里的第一个元素」。下游取values[0],数组性就此丢失。现象(实测,在 #5374 修复之后)
3 行数据,
qty分别为100/200/100(id1/2/3):wherefind(){qty: {$eq: [100]}}[][1,3]100,于是匹配上了{qty: {$eq: []}}[][]undefined后同样取不到行find()的答案是对的:存的是数字100,[100]是一个数组,mingo 跨类型比较永不相等,所以 0 行。analytics 面把作者写的数组当成了标量,回答了一个作者没有问的问题。方向这次是收窄→放大都可能:上表是放大(0 → 2 行),但
{qty: {$eq: [100, 200]}}会静默丢掉200,那是收窄。两个方向都是静默的。为什么是 bug
$eq: [100]大概率是笔误(想写$in),而正确的行为是照实回答 0 行 —— 让笔误可见。摊平之后笔误被修正成了另一个查询,作者永远看不到自己写错了。这是 Prime Directive Add comprehensive test suite for Zod schema validation #12 点名的「消费端宽容」形状:在消费端替生产者猜意图,把错误藏起来。normalizeFieldOperators的case '$eq'是result[op] = store(val),数组原样进 mingo。所以又是本包两面对同一个where给出不同行集({ field: {} }(零个操作符的字段约束)在同仓有三个答案:driver-sql 组合子内 TRUE、顶层抛 INVALID_FILTER、formula/driver-memory FALSE #5240)。FilterCondition的$eq是宽类型,所以这个形状能通过assertFilterConditionShape,不会在门口被拦下。建议(不代裁决)
两条路,请裁:
$in/$nin(以及隐式相等的数组写法)接受列表,其余算子原样保留数组比较数。与 live path 一致,改动小。assertFilterConditionShape对标量算子收到数组时报INVALID_FILTER,与$null的比较值不是布尔时,driver-sql 与 driver-memory 给出**完全相反**的答案(一个 IS NULL,一个 IS NOT NULL)—— 实测 #5347($null收到非布尔)、driver-memory 对形状错误的$between静默不发谓词(匹配 0 行),driver-sql 同一过滤器抛错 —— 一个过滤器两个答案 #5328($between形状不对)同款 —— 那两条都是这么裁的,先例更强,而且笔误会得到一个说得出话的 400 而不是 0 行。倾向 B,因为「0 行」和「作者写错了」在图表上同样看不出区别,而 #5347 / #5328 已经为这一类建立了拒收的惯例;但 B 需要先确认 spec 是否有意允许
$eq: [...]表达「等于这个数组值」(schemaless 存储里数组是合法列值),若有意,则只能走 A。未验证的部分
$contains/$gt等其余标量算子收到数组时的行为,只测了$eq/$ne。机制是共用的一行代码,推断其余相同,但未实测。$in写成$eq的 widget 元数据)。改之前值得 grep 一遍。关联:#5374(同文件、同一轮核对里发现;它裁的是算子→谓词层,本条在降级层)、#5347 / #5328(同类「形状不对就拒收」的先例)、#5240(本包两面不得分叉)、#5373。