做 #5374 ($notContains 编译成裸 {$not: 'x'})时,为了确认修复没有把 $match 的组装弄坏,逐个实测了「一个 where 里同一字段带多个算子」会编译成什么。这一条不属于 #5374 的范围面 —— #5374 裁的是算子 → 谓词 这一层(映射指向了错的目标),这一条在谓词 → $match 的组装 这一层,#5374 修好之后依然完整存在。按 Prime Directive #10 单独记在这里,unassigned。
位置
packages/plugins/driver-memory/src/memory-analytics.ts,query() Stage 1 的 $match 组装循环(约 :180):
for ( const filter of normalizedFilters ) {
const fieldPath = this . resolveFieldPath ( cube , filter . member ) ;
matchStage [ fieldPath ] = /* 该条目的谓词 */ ; // ← 直接赋值,不合并
}
flattenFilterCondition 对 {field: {$op1: a, $op2: b}} 会 push 两条 NormalizedCubeFilter,member 都是 field。两条都以 matchStage[fieldPath] = … 落盘,于是后一条覆盖前一条 ,前一个算子彻底消失。
现象(实测)
3 行数据,qty 分别为 100 / 200 / 300(id 分别为 1/2/3)。左列是 find(),右列是 analytics 面 —— 两列都是在 #5374 修复之后 测的,即这条与那条无关:
where
find()
analytics
实际编译出的 $match
{qty: {$gte: 150, $lte: 250}}
[2]
[1,2]
{qty: {$lt: 250}}
{qty: {$ne: 300, $gt: 150}}
[2]
[2,3]
{qty: {$gt: 150}}
方向是放大 :丢掉的永远是一个约束,所以行只会变多,不会变少。这与 #5345 / #5373 / #5374 是同一个方向、同一类后果 —— 一个看起来正常的图表统计了它本该排除的行 —— 只是机制换成了「谓词被生成了,但被下一个谓词覆盖掉」。
为什么是 bug
{$gte, $lte} 是区间的规范写法。 AnalyticsQuery.where 是 FilterCondition,而 dashboard widget 元数据直接用这个形状表达「金额在某区间」「日期在某窗口」。这恰恰是最常见的多算子写法,不是边角。
declared ≠ enforced。 ANALYTICS_FILTER_CAPABILITIES 声明本面支持这 11 个算子,assertFilterConditionShape 也放行了 {$gte, $lte} 这个形状 —— 声明能算,实际只算了一半。
另一半是对的。 live query path 的 normalizeFieldOperators 把同字段的多个算子合并 进一个对象,多个 regex 型算子还会通过 _multiRegex 提升成 $and。所以这是同一个 driver 的两面对同一个 where 给出不同行集 —— { field: {} }(零个操作符的字段约束)在同仓有三个答案:driver-sql 组合子内 TRUE、顶层抛 INVALID_FILTER、formula/driver-memory FALSE #5240 为本包立下的不变量所禁止的形状。
建议(不代裁决)
不是一行改。合并本身容易(matchStage[fieldPath] = {...existing, ...predicate}),但有两处必须一起想清楚,否则会把一个覆盖换成另一个覆盖:
另一条路是拒收 同字段多算子(与 #5345 同款),但那会拒掉本面 find() 明明能算、且是 widget 常用形状的查询,恐怕不是想要的答案。
未验证的部分
关联:#5374 (同文件、同一轮核对里发现;它裁的是算子→谓词层,本条是谓词→$match 组装层)、#5345 、#5373 、#5240 (本包两面不得分叉)、#3948 (no-silent-drop)。
做 #5374(
$notContains编译成裸{$not: 'x'})时,为了确认修复没有把$match的组装弄坏,逐个实测了「一个where里同一字段带多个算子」会编译成什么。这一条不属于 #5374 的范围面 —— #5374 裁的是算子 → 谓词这一层(映射指向了错的目标),这一条在谓词 →$match的组装这一层,#5374 修好之后依然完整存在。按 Prime Directive #10 单独记在这里,unassigned。位置
packages/plugins/driver-memory/src/memory-analytics.ts,query()Stage 1 的$match组装循环(约 :180):flattenFilterCondition对{field: {$op1: a, $op2: b}}会 push 两条NormalizedCubeFilter,member 都是field。两条都以matchStage[fieldPath] = …落盘,于是后一条覆盖前一条,前一个算子彻底消失。现象(实测)
3 行数据,
qty分别为100/200/300(id分别为 1/2/3)。左列是find(),右列是 analytics 面 —— 两列都是在 #5374 修复之后测的,即这条与那条无关:wherefind()$match{qty: {$gte: 150, $lte: 250}}[2][1,2]{qty: {$lt: 250}}{qty: {$ne: 300, $gt: 150}}[2][2,3]{qty: {$gt: 150}}方向是放大:丢掉的永远是一个约束,所以行只会变多,不会变少。这与 #5345 / #5373 / #5374 是同一个方向、同一类后果 —— 一个看起来正常的图表统计了它本该排除的行 —— 只是机制换成了「谓词被生成了,但被下一个谓词覆盖掉」。
为什么是 bug
{$gte, $lte}是区间的规范写法。AnalyticsQuery.where是FilterCondition,而 dashboard widget 元数据直接用这个形状表达「金额在某区间」「日期在某窗口」。这恰恰是最常见的多算子写法,不是边角。ANALYTICS_FILTER_CAPABILITIES声明本面支持这 11 个算子,assertFilterConditionShape也放行了{$gte, $lte}这个形状 —— 声明能算,实际只算了一半。normalizeFieldOperators把同字段的多个算子合并进一个对象,多个 regex 型算子还会通过_multiRegex提升成$and。所以这是同一个 driver 的两面对同一个where给出不同行集 ——{ field: {} }(零个操作符的字段约束)在同仓有三个答案:driver-sql 组合子内 TRUE、顶层抛 INVALID_FILTER、formula/driver-memory FALSE #5240 为本包立下的不变量所禁止的形状。建议(不代裁决)
不是一行改。合并本身容易(
matchStage[fieldPath] = {...existing, ...predicate}),但有两处必须一起想清楚,否则会把一个覆盖换成另一个覆盖:$contains+$contains,或$contains+$notContains各自带$regex)。live path 为此有_multiRegex→ 顶层$and的提升,cube 管线需要一份等价物。$lte的 bare-day 半开区间规则(driver-memory / driver-mongodb:裸日期$lte上界在 datetime 值上同样丢当天数据(#3777 的非 SQL 驱动对齐) #4042)会把$lte写成$lt,与显式的$lt键相撞;合并时谁赢需要明确。另一条路是拒收同字段多算子(与 #5345 同款),但那会拒掉本面
find()明明能算、且是 widget 常用形状的查询,恐怕不是想要的答案。未验证的部分
qty上的数值算子组合。没有测 regex 型算子的组合({name: {$contains: 'a', $notContains: 'b'}}),因为那正是上面第一个待裁决点,测出来的结果取决于怎么裁。generateSql那一半在同样输入下是否也丢约束(那个出口的算子层缺陷另有 driver-memory analytics 面的generateSql对in/notIn/set/notSet输出错误 SQL:回退成=且只取第一个值($in: ['100','200']→WHERE code = '100') #5433)。关联:#5374(同文件、同一轮核对里发现;它裁的是算子→谓词层,本条是谓词→
$match组装层)、#5345、#5373、#5240(本包两面不得分叉)、#3948(no-silent-drop)。