Skip to content

driver-memory 与 formula 对「字段没有值」给出三处不同答案:$notContains(null 值)、$exists(键在值为 null)、$nin(缺键) #5299

Description

@os-zhuang

#5146 时,为了给 SQL 侧找一个「JS 家族的正确答案」而把两个 JS 后端逐算子跑了一遍,
顺带测出它们彼此在三处不一致。不在 #5146 范围内(那单裁定的是 $not 在 SQL 上
的 NULL 语义),按 Prime Directive #10 记录。

事实(实测)

两组 fixture,区分「键在、值为 null」与「键不存在」:

  • NULLED:3: { stage: null }4: { stage: null }
  • MISSING:3: {}(无 stage 键)、4: {}
filter driver-memory match(值为 null) driver-memory(缺键) formula(值为 null) formula(缺键)
{ stage: { $notContains: 'w' } } 不匹配 不匹配 匹配 匹配
{ stage: { $exists: true } } 不算存在 不算存在 算存在 不算存在
{ stage: { $nin: ['won'] } } 匹配 不匹配 匹配 匹配

成因都在 packages/plugins/driver-memory/src/memory-matcher.tscheckCondition:

  1. 早退守卫 if (value === undefined && op !== '$exists' && op !== '$ne' && op !== '$null') return false;
    —— 豁免名单里没有 $nin,所以缺键的记录直接判 $nin 不通过,而同一记录若把字段
    写成 null 则通过。formula 的 evalOp 两种情况都判「不在集合里」→ 通过。
  2. case '$notContains': if (typeof value !== 'string' || value.includes(target)) return false;
    —— 值不是字符串(null / undefined)一律判否;formula 的
    !(typeof actual === 'string' && actual.includes(v)) 判是。
  3. $exists 两边读的是不同的东西:driver-memory 读「有没有值」
    (value !== undefined && value !== null),formula 读「键在不在」
    (actual !== undefined)。对「键在、值为 null」这一格必然相反。

为什么这不是学术问题

matchesFilterCondition 是 RLS 写侧 check 的求值器(insert/update 的后像),
driver-memorymatch 是内存驱动的读过滤。同一条策略里写
{ notes: { $notContains: 'secret' } },一条 notes 为 null 的记录在写侧被判「满足」
而在内存读侧被判「不满足」 —— 而 SQL 侧又是第三个答案(NOT LIKE 对 NULL 是 UNKNOWN
→ 不返回)。#5146 只把 $not 这一格统一了,这三格没人裁定过。

顺带说明 PR #5296 在这三格上没有顺手做决定:driver-sql 的 $not 改写对
$notContains 跟随 formula(理由:那正是该驱动今天已有的答案,不借改写引入一个没人
裁定的语义),$exists 保持 IS NOT NULL(SQL 分不出「键不在」和「值为 null」)。
三个后端的实测答案都已按现状钉在测试里:

  • packages/plugins/driver-memory/src/memory-matcher-not-null-safe.test.ts
    的 “known disagreements with formula.matchesFilterCondition” 一节
  • packages/formula/src/matches-filter-not-null-safe.test.ts
    的 “known disagreement with driver-memory” 一节

所以裁定之后,要改的地方是明确的、且改动会立刻被这两组 pin 顶出来。

需要拍板的

  1. $notContains 对「没有值」的字段是否成立?(SQL 侧天然是「不成立」,formula 说
    「成立」,driver-memory 说「不成立」。)
  2. $exists 的语义是「键存在」还是「有值」?两者在 SQL 上不可区分,所以选「有值」能让
    三家统一,选「键存在」则 SQL 永远无法兑现 —— 按 declared = enforced,倾向前者。
  3. $nin 对缺键记录:driver-memory 的早退守卫应把 $nin(以及同族的
    $notContains)一并豁免,还是维持?

建议与 #5146 的 spec 半边、#5239FILTER_LOGIC_CASES 扩表一并裁定,并把这几格写进
那张跨后端表 —— 它今天刻意不含 null 处理,所以这三处分叉现在没有任何门禁能发现。

关联

#5146 / PR #5296($not 的 NULL 语义,已拍板并落地 engine 半边)、#5298(非否定路径上
$ne/$nin/$notContains 的 SQL vs JS 分叉)、#5240({ field: {} } 的三个答案)、
#5239(conformance 表)。

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions