修 #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.ts 的 checkCondition:
- 早退守卫
if (value === undefined && op !== '$exists' && op !== '$ne' && op !== '$null') return false;
—— 豁免名单里没有 $nin,所以缺键的记录直接判 $nin 不通过,而同一记录若把字段
写成 null 则通过。formula 的 evalOp 两种情况都判「不在集合里」→ 通过。
case '$notContains': if (typeof value !== 'string' || value.includes(target)) return false;
—— 值不是字符串(null / undefined)一律判否;formula 的
!(typeof actual === 'string' && actual.includes(v)) 判是。
$exists 两边读的是不同的东西:driver-memory 读「有没有值」
(value !== undefined && value !== null),formula 读「键在不在」
(actual !== undefined)。对「键在、值为 null」这一格必然相反。
为什么这不是学术问题
matchesFilterCondition 是 RLS 写侧 check 的求值器(insert/update 的后像),
driver-memory 的 match 是内存驱动的读过滤。同一条策略里写
{ 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 顶出来。
需要拍板的
$notContains 对「没有值」的字段是否成立?(SQL 侧天然是「不成立」,formula 说
「成立」,driver-memory 说「不成立」。)
$exists 的语义是「键存在」还是「有值」?两者在 SQL 上不可区分,所以选「有值」能让
三家统一,选「键存在」则 SQL 永远无法兑现 —— 按 declared = enforced,倾向前者。
$nin 对缺键记录:driver-memory 的早退守卫应把 $nin(以及同族的
$notContains)一并豁免,还是维持?
建议与 #5146 的 spec 半边、#5239 的 FILTER_LOGIC_CASES 扩表一并裁定,并把这几格写进
那张跨后端表 —— 它今天刻意不含 null 处理,所以这三处分叉现在没有任何门禁能发现。
关联
#5146 / PR #5296($not 的 NULL 语义,已拍板并落地 engine 半边)、#5298(非否定路径上
$ne/$nin/$notContains 的 SQL vs JS 分叉)、#5240({ field: {} } 的三个答案)、
#5239(conformance 表)。
修 #5146 时,为了给 SQL 侧找一个「JS 家族的正确答案」而把两个 JS 后端逐算子跑了一遍,
顺带测出它们彼此在三处不一致。不在 #5146 范围内(那单裁定的是
$not在 SQL 上的 NULL 语义),按 Prime Directive #10 记录。
事实(实测)
两组 fixture,区分「键在、值为 null」与「键不存在」:
NULLED:3: { stage: null }、4: { stage: null }MISSING:3: {}(无 stage 键)、4: {}match(值为 null){ stage: { $notContains: 'w' } }{ stage: { $exists: true } }{ stage: { $nin: ['won'] } }成因都在
packages/plugins/driver-memory/src/memory-matcher.ts的checkCondition:if (value === undefined && op !== '$exists' && op !== '$ne' && op !== '$null') return false;—— 豁免名单里没有
$nin,所以缺键的记录直接判$nin不通过,而同一记录若把字段写成
null则通过。formula 的evalOp两种情况都判「不在集合里」→ 通过。case '$notContains': if (typeof value !== 'string' || value.includes(target)) return false;—— 值不是字符串(null / undefined)一律判否;formula 的
!(typeof actual === 'string' && actual.includes(v))判是。$exists两边读的是不同的东西:driver-memory 读「有没有值」(
value !== undefined && value !== null),formula 读「键在不在」(
actual !== undefined)。对「键在、值为 null」这一格必然相反。为什么这不是学术问题
matchesFilterCondition是 RLS 写侧check的求值器(insert/update 的后像),driver-memory的match是内存驱动的读过滤。同一条策略里写{ 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 顶出来。
需要拍板的
$notContains对「没有值」的字段是否成立?(SQL 侧天然是「不成立」,formula 说「成立」,driver-memory 说「不成立」。)
$exists的语义是「键存在」还是「有值」?两者在 SQL 上不可区分,所以选「有值」能让三家统一,选「键存在」则 SQL 永远无法兑现 —— 按 declared = enforced,倾向前者。
$nin对缺键记录:driver-memory 的早退守卫应把$nin(以及同族的$notContains)一并豁免,还是维持?建议与 #5146 的 spec 半边、#5239 的
FILTER_LOGIC_CASES扩表一并裁定,并把这几格写进那张跨后端表 —— 它今天刻意不含 null 处理,所以这三处分叉现在没有任何门禁能发现。
关联
#5146 / PR #5296(
$not的 NULL 语义,已拍板并落地 engine 半边)、#5298(非否定路径上$ne/$nin/$notContains的 SQL vs JS 分叉)、#5240({ field: {} }的三个答案)、#5239(conformance 表)。