修 #5325(filter-normalizer.buildNode 的 $not NULL 安全 + 布尔常量)时,为了写 $not 的
逐算子极性表必须逐个核对本文件每个算子实际编译成什么,在这一步发现的分叉。
不属于 #5325 的范围面(那一单是 $not / 布尔单位元三条),按 Prime Directive #10
单独记在这里,unassigned。
位置
packages/services/service-analytics/src/strategies/filter-normalizer.ts 的 fieldLeaves
现象(实测)
stringifyForCube 对 null 返回空字符串 '':
function stringifyForCube(v: unknown): string {
if (v == null) return '';
…
}
于是一个 null 比较数经算子映射后变成了一次普通值比较。同一个「这个字段是空」的意思,
在同一个文件里有两个答案。经 NativeSQLStrategy.generateSql 实测(cube deals,列 stage):
where |
生成的 WHERE |
绑定值 |
应为 |
{ stage: null } |
stage IS NULL |
[] |
✅ |
{ stage: { $eq: null } } |
stage = $1 |
[''] |
stage IS NULL |
{ stage: { $ne: null } } |
stage != $1 |
[''] |
stage IS NOT NULL |
{ stage: { $null: true } } |
stage IS NULL |
[] |
✅ |
两条 objectql 路径同样:renderFilterNodeSql 回显 stage = $1 / [''],
filterNodeToCondition 交给引擎的是 {stage: ''}(经 coerceFilterValueForObjectQL('') →
''),即拿空串去和存储里的 null 比 —— 永不匹配。
为什么是 bug
- 同文件自相矛盾:
{field: null} 走 fieldLeaves 的 raw === null 分支 → notSet
(IS NULL),而 {field: {$eq: null}} 走算子映射 → = ''。两种写法在
filter.zod.ts 里是同一个意思。
- 与其他后端分叉:
read-scope-sql.ts 的 compileOperator(同包)对 $eq: null 明确
给 IS NULL、$ne: null 给 IS NOT NULL;driver-sql 的 nullValueSatisfiesOperator
也是按「$eq: null 就是 null 谓词」写的。这个包的另一半和它们一致,这一半不一致。
- 后果是静默错行,不是报错:一条「阶段为空」的 widget filter 取到零行(除非真有列
存了空串),作者看到的是「没有数据」而不是任何错误。
- 对文本列尤其危险:SQLite / MySQL 里
'' 是一个真实的值,所以
{stage: {$ne: null}} 会把 stage = '' 的行排掉,而它本意是「非空」。
与 #5325 的关系(为什么那一单没顺手改)
#5325 的守卫表必须「跟随本文件的 emitter」而不是跟随 read-scope-sql 的 —— 这是
sql-driver.ts 早就写明的不变量(每个 guard 匹配自己的 emitter)。所以那一单里
$eq / $ne 的 null 比较数被当作普通值比较来定极性,并在 TSDoc 里注明
「'' 比较数本身是另一个缺陷,单独记录、本单不裁定」。本单就是那条记录。
改法一旦落地,#5325 的 nullValueSatisfiesOperator / operatorIsNullTotal 需要同步补回
read-scope-sql 里那两条 value === null 的 arm($eq: null / $ne: null 变成 null-total
→ guard 'none'),两处一起改才自洽。filter-normalizer-not-null-safe.test.ts 里没有
钉住 {$not: {stage: {$eq: null}}} 的行集,正是为了不把当前这个错误答案钉死。
建议
fieldLeaves 在算子循环里先判 null 比较数:$eq: null → leaf('notSet', [])、
$ne: null → leaf('set', []),与 raw === null 分支和 read-scope-sql 对齐。
stringifyForCube 的 v == null → '' 保持不动(它还服务于别的调用点),或改成让
null 无法进入值数组。
严重度请 PM 按 triage 定 —— 我只测了它编译成什么和取到什么行,没有统计现网有多少
{$eq: null} 形状的 widget。
关联:#5325(同函数、同一轮核对里发现)、#5297 / read-scope-sql.ts(同包里正确的那一半)、
#5146 / #5296($not 的 NULL 语义)、#4128(同文件上一轮「算子静默丢失」)。
修 #5325(
filter-normalizer.buildNode的$notNULL 安全 + 布尔常量)时,为了写$not的逐算子极性表必须逐个核对本文件每个算子实际编译成什么,在这一步发现的分叉。
不属于 #5325 的范围面(那一单是
$not/ 布尔单位元三条),按 Prime Directive #10单独记在这里,unassigned。
位置
packages/services/service-analytics/src/strategies/filter-normalizer.ts的fieldLeavesstringifyForCube。现象(实测)
stringifyForCube对null返回空字符串'':于是一个
null比较数经算子映射后变成了一次普通值比较。同一个「这个字段是空」的意思,在同一个文件里有两个答案。经
NativeSQLStrategy.generateSql实测(cubedeals,列stage):where{ stage: null }stage IS NULL[]{ stage: { $eq: null } }stage = $1['']stage IS NULL{ stage: { $ne: null } }stage != $1['']stage IS NOT NULL{ stage: { $null: true } }stage IS NULL[]两条 objectql 路径同样:
renderFilterNodeSql回显stage = $1/[''],filterNodeToCondition交给引擎的是{stage: ''}(经coerceFilterValueForObjectQL('')→''),即拿空串去和存储里的null比 —— 永不匹配。为什么是 bug
{field: null}走fieldLeaves的raw === null分支 →notSet(
IS NULL),而{field: {$eq: null}}走算子映射 →= ''。两种写法在filter.zod.ts里是同一个意思。read-scope-sql.ts的compileOperator(同包)对$eq: null明确给
IS NULL、$ne: null给IS NOT NULL;driver-sql的nullValueSatisfiesOperator也是按「
$eq: null就是 null 谓词」写的。这个包的另一半和它们一致,这一半不一致。存了空串),作者看到的是「没有数据」而不是任何错误。
''是一个真实的值,所以{stage: {$ne: null}}会把stage = ''的行排掉,而它本意是「非空」。与 #5325 的关系(为什么那一单没顺手改)
#5325 的守卫表必须「跟随本文件的 emitter」而不是跟随
read-scope-sql的 —— 这是sql-driver.ts早就写明的不变量(每个 guard 匹配自己的 emitter)。所以那一单里$eq/$ne的 null 比较数被当作普通值比较来定极性,并在 TSDoc 里注明「
''比较数本身是另一个缺陷,单独记录、本单不裁定」。本单就是那条记录。改法一旦落地,#5325 的
nullValueSatisfiesOperator/operatorIsNullTotal需要同步补回read-scope-sql里那两条value === null的 arm($eq: null/$ne: null变成 null-total→ guard
'none'),两处一起改才自洽。filter-normalizer-not-null-safe.test.ts里没有钉住
{$not: {stage: {$eq: null}}}的行集,正是为了不把当前这个错误答案钉死。建议
fieldLeaves在算子循环里先判null比较数:$eq: null→leaf('notSet', [])、$ne: null→leaf('set', []),与raw === null分支和read-scope-sql对齐。stringifyForCube的v == null → ''保持不动(它还服务于别的调用点),或改成让null无法进入值数组。严重度请 PM 按 triage 定 —— 我只测了它编译成什么和取到什么行,没有统计现网有多少
{$eq: null}形状的 widget。关联:#5325(同函数、同一轮核对里发现)、#5297 /
read-scope-sql.ts(同包里正确的那一半)、#5146 / #5296(
$not的 NULL 语义)、#4128(同文件上一轮「算子静默丢失」)。