Skip to content

分析查询的 {field: {$eq: null}} / {$ne: null} 编译成 col = '' / col != '',与同文件里 {field: null}IS NULL 自相矛盾 #5332

Description

@os-zhuang

#5325(filter-normalizer.buildNode$not NULL 安全 + 布尔常量)时,为了写 $not
逐算子极性表必须逐个核对本文件每个算子实际编译成什么,在这一步发现的分叉。
不属于 #5325 的范围面(那一单是 $not / 布尔单位元三条),按 Prime Directive #10
单独记在这里,unassigned。

位置

packages/services/service-analytics/src/strategies/filter-normalizer.tsfieldLeaves

  • stringifyForCube

现象(实测)

stringifyForCubenull 返回空字符串 '':

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}fieldLeavesraw === null 分支 → notSet
    (IS NULL),而 {field: {$eq: null}} 走算子映射 → = ''。两种写法在
    filter.zod.ts 里是同一个意思。
  • 与其他后端分叉:read-scope-sql.tscompileOperator(同包)对 $eq: null 明确
    IS NULL$ne: nullIS NOT NULL;driver-sqlnullValueSatisfiesOperator
    也是按「$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 里注明
'' 比较数本身是另一个缺陷,单独记录、本单不裁定」。本单就是那条记录。

改法一旦落地,#5325nullValueSatisfiesOperator / 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: nullleaf('notSet', [])
$ne: nullleaf('set', []),与 raw === null 分支和 read-scope-sql 对齐。
stringifyForCubev == null → '' 保持不动(它还服务于别的调用点),或改成让
null 无法进入值数组。

严重度请 PM 按 triage 定 —— 我只测了它编译成什么和取到什么行,没有统计现网有多少
{$eq: null} 形状的 widget。

关联:#5325(同函数、同一轮核对里发现)、#5297 / read-scope-sql.ts(同包里正确的那一半)、
#5146 / #5296($not 的 NULL 语义)、#4128(同文件上一轮「算子静默丢失」)。

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions