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 表)。

Activity

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions