Skip to content

$null 的比较值不是布尔时,driver-sql 与 driver-memory 给出**完全相反**的答案(一个 IS NULL,一个 IS NOT NULL)—— 实测 #5347

Description

@os-zhuang

#5324 / #5328 时按 #5328 的建议「顺带查同一 switch 里其他有条件写入的 arm 是否同款($null、regex 族)」清点,发现 $null 这一条不是「静默丢弃」而是静默取反。按 Prime Directive #10 单独记在这里,unassigned。

实测原文

同一个过滤器 { stage: { $null: 'yes' } },同一份数据(单行 { id: '1', stage: 'won' }):

driver-sql     (better-sqlite3 :memory:)   =>  RESOLVED []        // 编译成 stage IS NULL
driver-memory  (InMemoryDriver.find)       =>  RESOLVED ["1"]     // 编译成 stage IS NOT NULL
driver-memory  (memory-matcher.match)      =>  ["1"]              // 与 find 一致

对照组,证明分叉只出现在非布尔值上:

{ $null: true }   =>  sql []      memory []       ✅ 一致
{ $null: false }  =>  (未测 sql)  memory ["1"]

机制:两边的默认分支挂在相反的一侧

driver-sql(sql-driver.ts :6363)按 === false 判定,即「不是 false 就当 true」:

case '$null':
  (builder as any)[opValue === false ? whereNotNull : whereNull](field);

driver-memory(memory-driver.ts normalizeFieldOperators)按 === true 判定,即「不是 true 就当 false」:

case '$null':
  if (val === true) { result.$eq = null; } else { result.$ne = null; }

两个默认分支方向相反,于是每一个既不是 true 也不是 false 的比较值 —— 'yes'1nullundefined{} —— 都会拿到两个互相矛盾的答案driver-sql 内部是自洽的(#5146nullValueSatisfiesOperator 也写着 case '$null': return value !== false),driver-memory 内部也是自洽的(两个过滤面一致);分叉严格发生在两个后端之间。

为什么是 bug

  1. 不是「丢一条约束」,是「答反了」。 driver-memory 对形状错误的 $between 静默不发谓词(匹配 0 行),driver-sql 同一过滤器抛错 —— 一个过滤器两个答案 #5328 那类静默是结果集变空或变全,还看得出不对;这一条返回的是一个语义上恰好相反的行结果集,两边都言之凿凿。
  2. spec 声明的是布尔。 FieldOperatorsSchema$null: z.boolean().optional(),所以非布尔值本就是越界输入 —— 但 where 走到驱动时没有任何一层按 FieldOperatorsSchema 校验过,所以越界输入是真的能到这里的。AI 生成的元数据把 $null: "true"(字符串)写出来是完全可能的,而字符串 "false" 是 truthy,于是 SQL 说 IS NULL、memory 说 IS NOT NULL。
  3. 它落在权限面上的概率与 $not 同级:$null 是 RLS scope 里判断「字段没值」的常规写法。

建议(不代裁决)—— 这需要一次裁决,不能各改各的

三条路,方向不同,必须先定哪个是 canonical,否则改一边只是把分叉换个位置:

我个人倾向 A:它是唯一一条不需要在两个都很难自圆其说的巧合之间挑一个的路,也是唯一一条让「AI 写错的元数据在写入时就被拒」而不是「在两个后端上各跑出一个答案」的路。但这条会改变今天不报错的输入的行为,所以留给维护者拍。

顺带记录:同一轮清点里,regex 族($contains / $startsWith / $endsWith / $notContains)的比较值不是字符串时,两边都做 String(val) 字符串化([object Object]),方向一致、都是 fail-closed,不构成分叉 —— driver-sql 的 #5041 注释也明确把这一族排除在守卫外。这条不需要动。

未验证的部分

我没有实测 driver-mongodb / driver-sqlite-wasm 在同一输入下的行为(sqlite-wasm 继承 SqlDriver,预期与 SQL 一致但没实测;mongodb 的 $null arm 写法与 memory 相同,预期与 memory 一致但没实测),也没有量化真实元数据里出现非布尔 $null 的频率。严重度请 PM 按 triage 定,不代表我判断它低。

关联:#5324 / #5328(实测出这一条的那一单)、#5146($not 的 NULL 语义裁决,同款「先定 canonical 再改」)、#5240(一个条件一种措辞)、#5298(非否定路径上的 NULL 语义分叉)。

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions