修 #5146 时实测到的邻接分叉,不在该单范围内(维护者的拍板明确只覆盖 $not)。
这里按 Prime Directive #10 记录。
事实(实测,main@88b9b2d5c + PR #5296 的分支;这几条路径 PR #5296 未改动)
fixture:1: stage='won'、2: stage='lost'、3: stage=NULL、4: stage=NULL。
| filter |
driver-sql 编译 |
driver-sql 返回 |
driver-memory |
formula matchesFilterCondition |
{ stage: { $ne: 'won' } } |
stage <> 'won' |
2 |
2,3,4 |
2,3,4 |
{ stage: { $nin: ['won'] } } |
stage not in ('won') |
2 |
2,3,4 |
2,3,4 |
{ stage: { $notContains: 'w' } } |
stage NOT LIKE '%w%' |
2 |
2(另一处分叉,另单) |
2,3,4 |
成因与 #5146 同源:SQL 里 NULL <> 'won' 是 UNKNOWN 而不是 TRUE,WHERE 只留 TRUE;
JS 家族用两值逻辑求值,undefined !== 'won' 直接为真。区别只在于 #5146 的宿主是
$not,这里是算子本身。
为什么单独记
PR #5296 只把 $not 内部的叶子编译成全域谓词(这是 #5146 拍板的范围),$not
之外的比较逐字符未变 —— 刻意的:改动非否定路径会影响每一条普通查询的 SQL 形状与
索引利用,属于另一个量级的决定,需要单独拍板。所以今天的状态是:
{ $not: { stage: 'won' } } → 三家一致(NULL 行返回);
{ stage: { $ne: 'won' } } → 仍然分叉(SQL 少三行)。
对使用者而言,这个不一致是可见的:同一条「不是 won」的列表过滤,SQL 数据源上看不到
stage 为空的记录,内存数据源上看得到;RLS 写侧 check(formula)与读侧 SQL 也会对
同一条规则给出不同答案。
需要拍板的
$ne / $nin / $notContains 是否也采用 NULL-safe 语义(col IS NULL OR col <> v)?
建议与 #5146 的 spec 半边、#5239 的 FILTER_LOGIC_CASES 扩表一并考虑:无论选哪个,
跨驱动 case 都应该把 null 行的去留写进那张表(它今天刻意不含 null 处理,所以这类
分叉现在没有任何门禁能发现)。
关联
#5146($not 的同源分叉,已拍板 NULL-safe)、PR #5296(实现,含逐算子极性表可直接
复用)、#5239(conformance 表)、#5240({ field: {} } 的三个答案)、cloud#1076。
修 #5146 时实测到的邻接分叉,不在该单范围内(维护者的拍板明确只覆盖
$not)。这里按 Prime Directive #10 记录。
事实(实测,
main@88b9b2d5c+ PR #5296 的分支;这几条路径 PR #5296 未改动)fixture:
1: stage='won'、2: stage='lost'、3: stage=NULL、4: stage=NULL。matchesFilterCondition{ stage: { $ne: 'won' } }stage <> 'won'22,3,42,3,4{ stage: { $nin: ['won'] } }stage not in ('won')22,3,42,3,4{ stage: { $notContains: 'w' } }stage NOT LIKE '%w%'22(另一处分叉,另单)2,3,4成因与 #5146 同源:SQL 里
NULL <> 'won'是 UNKNOWN 而不是 TRUE,WHERE只留 TRUE;JS 家族用两值逻辑求值,
undefined !== 'won'直接为真。区别只在于 #5146 的宿主是$not,这里是算子本身。为什么单独记
PR #5296 只把
$not内部的叶子编译成全域谓词(这是 #5146 拍板的范围),$not之外的比较逐字符未变 —— 刻意的:改动非否定路径会影响每一条普通查询的 SQL 形状与
索引利用,属于另一个量级的决定,需要单独拍板。所以今天的状态是:
{ $not: { stage: 'won' } }→ 三家一致(NULL 行返回);{ stage: { $ne: 'won' } }→ 仍然分叉(SQL 少三行)。对使用者而言,这个不一致是可见的:同一条「不是 won」的列表过滤,SQL 数据源上看不到
stage 为空的记录,内存数据源上看得到;RLS 写侧
check(formula)与读侧 SQL 也会对同一条规则给出不同答案。
需要拍板的
$ne/$nin/$notContains是否也采用 NULL-safe 语义(col IS NULL OR col <> v)?$not的语义在 driver-sql 与 driver-memory / formula 之间分叉:NULL 行的去留相反,$not: {}一个是 TRUE 一个是 FALSE #5146 的裁定同向、与 JS 家族一致、declared = enforced 只剩一种答案;代价是所有
$ne/$nin谓词的 SQL 形状改变(可能影响索引利用),且这是每一条普通查询都会走的路径,不像
$not那样局限在一个本就不可 sargable 的形状里。且 CEL / 过滤器作者写
stage != 'won'时的意图会系统性落空 —— 与$not的语义在 driver-sql 与 driver-memory / formula 之间分叉:NULL 行的去留相反,$not: {}一个是 TRUE 一个是 FALSE #5146 拍板时给出的理由相反。
{ $or: [{ stage: { $null: true } }, { stage: { $ne: 'won' } }] }),即维持现状但在 spec 契约里把「
$ne不含 NULL 行」写明,由文档而非实现消除歧义。建议与 #5146 的 spec 半边、#5239 的
FILTER_LOGIC_CASES扩表一并考虑:无论选哪个,跨驱动 case 都应该把 null 行的去留写进那张表(它今天刻意不含 null 处理,所以这类
分叉现在没有任何门禁能发现)。
关联
#5146(
$not的同源分叉,已拍板 NULL-safe)、PR #5296(实现,含逐算子极性表可直接复用)、#5239(conformance 表)、#5240(
{ field: {} }的三个答案)、cloud#1076。