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'、1、null、undefined、{} —— 都会拿到两个互相矛盾的答案。driver-sql 内部是自洽的(#5146 的 nullValueSatisfiesOperator 也写着 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 语义分叉)。

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