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(同文件上一轮「算子静默丢失」)。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions