修 #5324 (driver-memory 的未知 $op 透传)时,把同一批输入喂给 driver-sql 做信封对照,发现 SQL 侧在文档级 位置有同一条缝,且症状比 memory 侧更隐蔽。按 Prime Directive #10 单独记在这里,unassigned。
实测原文
SqlDriver + better-sqlite3 :memory:,单行 { id: '1', stage: 'won', score: 10 }:
WHERE {"$where":"return true"} => RESOLVED [] // 期望:INVALID_FILTER / 400
WHERE {"$nor":[{"stage":"won"}]} => RESOLVED [] // 期望:INVALID_FILTER / 400
对照组 —— 字段级 同类输入在 SQL 侧是正确的,一直都是:
WHERE {"stage":{"$sounds_like":"won"}} => THROW code=INVALID_FILTER status=400
Unsupported filter operator "$sounds_like" on field "stage". Supported operators: …
WHERE {"score":{"$between":5}} => THROW code=INVALID_FILTER status=400
Operator "$between" on field "score" requires a [min, max] value array.
机制
FilterConditionSchema 在节点 位置只声明三个 $ 键(LOGICAL_OPERATORS:$and / $or / $not),其余键都是字段名。applyFilterCondition 的分支结构正是按这个假设写的,但没有任何一处检查 这个假设:
结果不是报错,而是一个匹配不到任何行的谓词 —— 静默的空结果集 ,和 #5328 在 driver-memory 上的 $between 是同一个症状。
{$nor:[…]} 更值得看一眼:它的值是数组,连 typeof value === 'object' && !Array.isArray(value) 这一支都进不去,同样落到 else,同样静默。
为什么是 bug
同一个驱动,两个位置两种答案。 字段级未知算子响亮拒收(A filter with an operator outside VALID_AST_OPERATORS is silently dropped, not rejected — single-condition views return unfiltered results #3948 / data: unsupported-filter-operator refusal ships without error.code and leaks the [sql-driver] prefix #4436 的成果),文档级未知算子静默返回空 —— 而调用方分辨不出「真的没有匹配」和「过滤器根本没编译」。这与 [spec] $field 跨字段比较:spec 声明 + cel-to-filter 产出,但无任何 SQL 执行层实现 —— enforce-or-remove 裁决位 #5041 记录的形状完全一致:那一条也是「查询编译了、跑了、返回零行」,并且当时的结论是「A silent wrong answer is what A filter with an operator outside VALID_AST_OPERATORS is silently dropped, not rejected — single-condition views return unfiltered results #3948 / fix(data): a filter the server cannot apply is rejected, not silently ignored (#4181) #4209 settled is strictly worse than an error on a permission-scoped read」。
$where 这个名字不是随便举的例子。 driver-mongodb 的 default 分支注释专门点名 $where / $function / $expr 是 P0 —— 一个未声明的 $op 被后端当真求值会绕过查询意图。SQL 侧目前不会求值它(它变成了列名),但「未声明的键悄悄改变了 WHERE 的含义」这件事本身就该拒收。
四家后端现在只剩它。 driver-memory 在本轮(driver-memory 的实时查询路径根本不支持 $not —— mingo 抛无 code 的 MingoError,CEL !expr 降下来的 RLS scope 在该驱动上直接 500 #5324 )已经对文档级未声明 $op 抛 INVALID_FILTER / 400;driver-mongodb 的 translateCondition 至少不会把它编成列(落到 default 的字段分支);SQL 是唯一把它编成列名并静默的。
建议(不代裁决)
在 reduceFilterKey 里(而不是发射器里)补一条:节点位置上以 $ 开头且不在 LOGICAL_OPERATORS 中的键,抛 unsupportedFilterError。理由与 #5240 把 {field:{}} 的拒收放在这条验证走查上完全相同 —— 这条走查是穷尽的、不短路的,放在发射器里会让「refused or ignored depending on its SIBLINGS」(该函数自己的注释)重演。
driver-memory 侧的实现可以逐字参照:packages/plugins/driver-memory/src/filter-refusal.ts 的 unknownLogicalOperatorError + assertFilterConditionShape。
未验证的部分
我只实测了 better-sqlite3 一个方言(其他方言把 "$where" 当列名的行为可能是报错 而不是返回空 —— 那样症状不同但病因相同)。也没有清点仓内是否真有产出文档级未声明 $op 的生产者;这更像是 AI 生成的元数据或手写 scope 的失误路径。严重度请 PM 按 triage 定,不代表我判断它低。
关联:#5324 (driver-memory 的同一条缝,文档级 + 字段级都已修)、#5328 、#5134 (reduceFilterNode 形状门)、#5240 (为什么拒收要放在走查上)、#5041 (「编译了、跑了、返回零行」的先例)、#4436 (信封)。
修 #5324(driver-memory 的未知
$op透传)时,把同一批输入喂给 driver-sql 做信封对照,发现 SQL 侧在文档级位置有同一条缝,且症状比 memory 侧更隐蔽。按 Prime Directive #10 单独记在这里,unassigned。实测原文
SqlDriver+ better-sqlite3:memory:,单行{ id: '1', stage: 'won', score: 10 }:对照组 —— 字段级同类输入在 SQL 侧是正确的,一直都是:
机制
FilterConditionSchema在节点位置只声明三个$键(LOGICAL_OPERATORS:$and/$or/$not),其余键都是字段名。applyFilterCondition的分支结构正是按这个假设写的,但没有任何一处检查这个假设:reduceFilterKey(SqlDriver.applyFilterCondition 丢弃编译成空的 $and/$or 子过滤器,而不是套用布尔单位元 —— 与同仓 matchesFilterCondition / driver-memory 相反 #5134 的形状门)对一个不认识的$键直接return 'clause'—— 注释写的是「A field key always contributes a predicate」,即它把$where当作了字段键;remoteColumn(table, '$where', …),把$where当列名编译。结果不是报错,而是一个匹配不到任何行的谓词 —— 静默的空结果集,和 #5328 在 driver-memory 上的
$between是同一个症状。{$nor:[…]}更值得看一眼:它的值是数组,连typeof value === 'object' && !Array.isArray(value)这一支都进不去,同样落到 else,同样静默。为什么是 bug
$field跨字段比较:spec 声明 + cel-to-filter 产出,但无任何 SQL 执行层实现 —— enforce-or-remove 裁决位 #5041 记录的形状完全一致:那一条也是「查询编译了、跑了、返回零行」,并且当时的结论是「A silent wrong answer is what A filter with an operator outside VALID_AST_OPERATORS is silently dropped, not rejected — single-condition views return unfiltered results #3948 / fix(data): a filter the server cannot apply is rejected, not silently ignored (#4181) #4209 settled is strictly worse than an error on a permission-scoped read」。$where这个名字不是随便举的例子。 driver-mongodb 的default分支注释专门点名$where/$function/$expr是 P0 —— 一个未声明的$op被后端当真求值会绕过查询意图。SQL 侧目前不会求值它(它变成了列名),但「未声明的键悄悄改变了 WHERE 的含义」这件事本身就该拒收。$not—— mingo 抛无 code 的 MingoError,CEL!expr降下来的 RLS scope 在该驱动上直接 500 #5324)已经对文档级未声明$op抛INVALID_FILTER/ 400;driver-mongodb 的translateCondition至少不会把它编成列(落到default的字段分支);SQL 是唯一把它编成列名并静默的。建议(不代裁决)
在
reduceFilterKey里(而不是发射器里)补一条:节点位置上以$开头且不在LOGICAL_OPERATORS中的键,抛unsupportedFilterError。理由与 #5240 把{field:{}}的拒收放在这条验证走查上完全相同 —— 这条走查是穷尽的、不短路的,放在发射器里会让「refused or ignored depending on its SIBLINGS」(该函数自己的注释)重演。driver-memory 侧的实现可以逐字参照:
packages/plugins/driver-memory/src/filter-refusal.ts的unknownLogicalOperatorError+assertFilterConditionShape。未验证的部分
我只实测了 better-sqlite3 一个方言(其他方言把
"$where"当列名的行为可能是报错而不是返回空 —— 那样症状不同但病因相同)。也没有清点仓内是否真有产出文档级未声明$op的生产者;这更像是 AI 生成的元数据或手写 scope 的失误路径。严重度请 PM 按 triage 定,不代表我判断它低。关联:#5324(driver-memory 的同一条缝,文档级 + 字段级都已修)、#5328、#5134(
reduceFilterNode形状门)、#5240(为什么拒收要放在走查上)、#5041(「编译了、跑了、返回零行」的先例)、#4436(信封)。