Repository navigation
$exists 的比较值不是布尔时,三个后端面给出三个答案,driver-memory 自己的两个面在 'yes' 上就已分叉 —— 实测,$null(#5347)的同族另一轴 #5369
Description
修 #5347 / #5348(PR #5368)时,按 PM 要求核对 driver-sql 的守卫表 nullValueSatisfiesOperator 能否随 $null 的拒收一并收紧,清点到紧邻的 $exists arm 带着同款的 === false 二分。实测后确认它也分叉,而且比 $null 更散。不在 #5347 范围内(PM 裁定只覆盖 $null),按 Prime Directive #10 单独记在这里,unassigned。
实测原文
fixture 两行 —— {id:'1', stage:'won'}、{id:'2', stage:null}(注意第二行:键在,值为 null)。三个求值面:
driver-sql driver-memory find driver-memory match
(better-sqlite3) (mingo) (参考匹配器)
$exists: 0 ["1"] [] ["2"]
$exists: 'yes' ["1"] ["1","2"] ["1"]
$exists: '' ["1"] [] ["2"]
对照组(布尔值,方向一致):
$exists: true ["1"] ["1","2"] ["1"]
$exists: false ["2"] [] ["2"]
机制:三处按三种规则读同一个比较值
driver-sql(sql-driver.ts,applyFilterCondition 的发射器 switch,$null arm 下一条):
case '$exists':
(builder as any)[opValue === false ? whereNull : whereNotNull](field);—— === false 恒等。除字面 false 外一律 IS NOT NULL,所以 0 和 '' 都读作「存在」。
driver-memory 的 mingo 面(memory-driver.ts normalizeFieldOperators):
case '$exists': case '$regex': case '$options':
result[op] = val;—— 原样交给 mingo,mingo 按 truthy 读。所以 0 / '' 读作「不存在」,'yes' 读作「存在」。
driver-memory 的参考匹配器(memory-matcher.ts):
case '$exists':
const exists = value !== undefined && value !== null;
if (exists !== !!target) return false;—— 也按 truthy(!!target),但它的 exists 定义是「有值」(!== undefined && !== null),而 mingo 的是「键存在」。所以在 stage: null 那一行上两者必然相反,$exists: 'yes' 就已经分开:mingo 说 ["1","2"],matcher 说 ["1"]。
三个规则:=== false 恒等 / truthy + 键存在 / truthy + 有值。
为什么这是独立的一条,而不是 #5299 或 #5347 的一部分
- 与 driver-memory 与 formula 对「字段没有值」给出三处不同答案:
$notContains(null 值)、$exists(键在值为 null)、$nin(缺键) #5299 不同轴。 driver-memory 与 formula 对「字段没有值」给出三处不同答案:$notContains(null 值)、$exists(键在值为 null)、$nin(缺键) #5299 问的是「$exists的语义是『键存在』还是『有值』」—— 那是布尔比较值下的语义分歧,需要一次语义裁决。本条问的是「比较值根本不是布尔时怎么办」,是形状问题,答案不依赖 driver-memory 与 formula 对「字段没有值」给出三处不同答案:$notContains(null 值)、$exists(键在值为 null)、$nin(缺键) #5299 怎么裁:无论$exists最终定义成哪一种,$exists: 0都没有一个不靠猜的读法。两条可以独立修,也可以一起修。 - 与
$null的比较值不是布尔时,driver-sql 与 driver-memory 给出**完全相反**的答案(一个 IS NULL,一个 IS NOT NULL)—— 实测 #5347 同族但被明确排除。$null的比较值不是布尔时,driver-sql 与 driver-memory 给出**完全相反**的答案(一个 IS NULL,一个 IS NOT NULL)—— 实测 #5347 的 PM 裁定只覆盖$null;PR fix(driver-sql,driver-memory,driver-mongodb): 越界过滤输入在门口拒收,而不是每个后端各答一个 (#5347, #5348) #5368 因此刻意没有收紧$exists,并在nullValueSatisfiesOperator里留了注释说明理由:该守卫表的case '$exists': return value === false必须继续与发射器的opValue === false读法一致,单独收紧守卫表而不加配套的比较值闸门,是制造分叉而不是消除分叉。PR 里也有一条用例把今天的$exists行为钉住,证明它没被顺手改动。
为什么值得修
$null的理由逐字适用。 spec 的FieldOperatorsSchema声明$exists: z.boolean().optional()(与$null同一处),非布尔是越界输入;而where走到驱动时没有任何一层按FieldOperatorsSchema校验过。AI 生成的元数据把$exists: "true"/$exists: 1写出来完全可能,字符串"false"是 truthy —— 与$null的比较值不是布尔时,driver-sql 与 driver-memory 给出**完全相反**的答案(一个 IS NULL,一个 IS NOT NULL)—— 实测 #5347 记录的陷阱一模一样。$exists落在权限面上。 与$null同级:RLS scope 里判断「字段有没有值」是常规写法。- 今天没有任何门禁能发现它。 共享表
FILTER_LOGIC_CASES刻意不含 null 处理(driver-memory 与 formula 对「字段没有值」给出三处不同答案:$notContains(null 值)、$exists(键在值为 null)、$nin(缺键) #5299 已记录这一点),所以三个面的分歧不会被任何 conformance 顶出来。
建议(不代裁决)
与 #5347 取同一条路 —— 非布尔比较值一律拒收(INVALID_FILTER / 400),落点与 #5368 的 $null 闸门完全对称:
- driver-sql:
reduceFilterKey的校验遍历上(不是发射器 —— 发射器会被布尔单位元短路,fix(driver-sql,driver-memory,formula)!:{ field: {} }四个后端一律拒收 —— 零个操作符的字段约束不再有三个答案 (#5240) #5327 / fix(driver-sql,driver-memory,driver-mongodb): 越界过滤输入在门口拒收,而不是每个后端各答一个 (#5347, #5348) #5368 的理由);拒收之后nullValueSatisfiesOperator的case '$exists': return value === false可随之收紧为value === true(方向注意:$exists与$null相反 —— 一个 NULL 列满足$exists当且仅当调用方问的是false)。 - driver-memory:
filter-refusal.ts的assertFieldConstraintShape,与$null那条并排一行。两个过滤面自动同时生效。 - driver-mongodb:
translateFieldOperators的$existsarm,与$null那条并排。 - cloud
RemoteTransport:同款=== false二分($nullarm 紧邻),归 cloud#1116 那条的配套,建议同时做。
代价与 #5347 相同:这是收紧,今天靠 truthy/falsy 巧合工作的调用方会拿到 400。#5347 的裁定已经为这一类代价定了调,所以本条大概率是同一句话的重复适用,而不是一次新裁决 —— 但仍请 PM 明确裁一次,不要由实现方代拍。
注意先后:若 #5299 先裁定了 $exists 的语义(键存在 vs 有值),本条的修复会顺带把两个 JS 面的实现对齐;若本条先修,#5299 的裁决面会缩小到只剩布尔输入。两条不冲突,任意顺序都可以。
未验证的部分
- driver-sqlite-wasm / cloud TursoDriver(local):未单独实测,但两者都继承
SqlDriver的同一段发射器代码,预期与 driver-sql 一致(fix(driver-sql,driver-memory,driver-mongodb): 越界过滤输入在门口拒收,而不是每个后端各答一个 (#5347, #5348) #5368 已对$null/ 节点位置越界$op两条实测确认过这条继承链确实传递行为)。 - cloud RemoteTransport:代码对读确认带同款
=== false二分,未对$exists单独跑数。 @objectstack/formula的matchesFilterCondition:未测。driver-memory 与 formula 对「字段没有值」给出三处不同答案:$notContains(null 值)、$exists(键在值为 null)、$nin(缺键) #5299 已记录它与 driver-memory 在布尔$exists上就不一致,非布尔下大概率是第四个答案。- 没有量化真实元数据里出现非布尔
$exists的频率。
严重度请按 triage 定,不代表我判断它低 —— 我只是按范围没有在 #5368 里顺手改。
关联
#5347 / PR #5368($null 的同款,已裁定 A 并落地四家后端;本条被明确排除在外)、#5299($exists 在布尔比较值下的语义分歧,另一轴,未决)、#5240({field:{}} 四后端拒收)、#5324 / #5328(拒收你无法求值的东西)、#5296(nullValueSatisfiesOperator 守卫表)、objectstack-ai/cloud#1116(remote Turso 的 $null,同族)。
Generated by Claude Code
Activity
os-zhuang commentedon Aug 5, 2026
分诊 + 范围注记(2026-08-05,锚点 #5499):挂
finding持有。理由:本单是跨后端语义分歧($exists非布尔比较值),与 #5298 / #5299 同族——需要一次语义裁决才有可派发的验收判据;且 driver-memory 投入已冻结(维护者 2026-08-05 裁决),其两个面的分叉随之冻结,driver-sql / formula 侧的答案分歧仍是活问题。下次发现分诊轮与 #5298 / #5299 并案处理(同一次裁决覆盖$ne/$nin/$notContains/$exists/$null一族最经济)。不构成认领。会话:session_01N3uGFF8teXbpgtbEJ1aYXuGenerated by Claude Code
os-zhuang commentedon Aug 5, 2026
发现分诊轮判级:持有(
finding留),已并案入 #5298(该单已转needs-user-decision,一次裁决覆盖$ne/$nin/$notContains/$exists/$null一族)。本单在裁决书里的份额:$exists非布尔比较值是否照 #5347 裁定 A 一律拒收(预期为同一句话的重复适用)。裁决后:driver-sql 半边可先行入队;driver-memory / driver-mongodb 半边按 #5499 冻结;cloudRemoteTransport半边归 cloud#1116 配套。本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
claude commentedon Aug 6, 2026
NULL/无值语义族裁决已落在 #5298(2026-08-06,方案 A:NULL-safe)。本单(
$exists非布尔比较值)按预期一句话套用 #5347-A 口径,随 #5298 的交付批次处置,详见该单裁决评论。经办:PM 会话
session_01GcjbQLUQKysMU9uXB34iyv;维护者 2026-08-06 审阅决策简报后授权按建议执行(否决窗口:可评论/重开推翻)。Generated by Claude Code
os-zhuang commentedon Aug 6, 2026
按 #5298 裁决闭环关闭(completed):
$ne/$nin/$notContains:driver-sql 排除 NULL 行,driver-memory / formula 返回它们(#5146 只裁定了$not) #5298「四项确认」③):$exists取「有值」,与$null严格互补——本单实测的「三个后端三个答案」(实为四个,非否定路径上的$ne/$nin/$notContains:driver-sql 排除 NULL 行,driver-memory / formula 返回它们(#5146 只裁定了$not) #5298 成本清单补测 formula 第四答案)收敛为一个;$exists非布尔比较值拒收闸照$null的比较值不是布尔时,driver-sql 与 driver-memory 给出**完全相反**的答案(一个 IS NULL,一个 IS NOT NULL)—— 实测 #5347-A 落地;formula 语义一行收敛;nullValueSatisfiesOperator收紧为value === true」经实测方向写反,未采纳(论证见 非否定路径上的$ne/$nin/$notContains:driver-sql 排除 NULL 行,driver-memory / formula 返回它们(#5146 只裁定了$not) #5298 成本清单 §7);$notNULL-safe —— 同一个 TursoDriver,local 返回 NULL 行、remote 不返回(实测,origin/main) #5903,driver-memory / mongodb 闸随 [裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499 解冻批次(其行为在冻结期不变)。Generated by Claude Code
fix(analytics): read scope 的 `$null` / `$exists` 非布尔比较数改为拒收,不再按真值性编成相…
fix(service-analytics)!: the analytics where door runs the shared com…
fix(service-analytics)!: the analytics where door refuses a non-boole…
fix(driver-memory,driver-mongodb): refuse a non-boolean $exists compa…
fix(service-analytics): runtime strings state each decision in words …