Skip to content

{ field: {} }(零个操作符的字段约束)在同仓有三个答案:driver-sql 组合子内 TRUE、顶层抛 INVALID_FILTER、formula/driver-memory FALSE #5240

Description

@os-zhuang

#5134(driver-sql 的空 $and/$or/$not 单位元)时按纪律核对「编译成空」的全部成因,发现这一处不属于本单范围的分叉。按 Prime Directive #10 单独记在这里,unassigned。

现象

{ a: {} } —— 一个字段,后面跟零个操作符。FilterConditionSchema 的非递归半边是 z.record(z.string(), z.unknown()),所以这个形状是声明合法的。同一个 filter,同仓四条路径给三个答案:

路径 代码位置 { a: {} } 的答案
driver-sql,顶层 sql-driver.ts applyFilters 的 plain-map 分支 → assertCompilableComparand INVALID_FILTER / 400(#5041 加的闸门)
driver-sql,组合子内($or/$and/$not 的分支里) sql-driver.ts applyFilterConditiontypeof value === 'object' 分支 遍历零个操作符 → 不产出任何 SQL → 实际语义 TRUE
formula matches-filter.ts evalField:if (keys.length === 0 ¦¦ …) return false FALSE(fail closed,显式写死)
driver-memory memory-matcher.ts checkCondition:落到 JSON.stringify(value) === JSON.stringify(condition) FALSE(结构相等,顺带为假)

driver-sql 自己内部就不自洽:同一个 { a: {} },写在顶层被响亮拒收,包进一层 $or 就变成静默的 TRUE。

为什么这是个 bug 而不是洁癖

$or 里,一个「什么都不产出」的分支会被 knex 连同分组一起丢掉,于是

{ $or: [ { a: {} }, { b: 2 } ] }

编译成 (b = 2) —— 而按 {} 是 TRUE 析取项的算法(#5134 刚刚为空节点确立的),它应当匹配全部行;按 formula/driver-memory 的 FALSE 判定,它应当只匹配 b = 2。两种读法结论不同,而当前 SQL 的结果是碰巧和后者一致、却是经由「丢弃子句」而非任何一种语义得到的 —— 和 #5134 修掉的那个成因完全同源。

为什么 #5134 没顺手修

因为两个候选答案各有依据、且互相矛盾,这是语义定调不是实现细节:

#5134 的实现刻意不碰它:归约函数把带字段键的节点一律判为 'clause'(而非 'true'),所以这次的单位元修复既没有把 { a: {} } 悄悄升格为「匹配所有行」,也没有替它做任何裁决 —— 行为与修复前逐字节相同。代码里留了注释指向本单。

建议

三选一,需维护者/spec 车道拍板;拍板后四个后端一起对齐,并把该 case 补进 FILTER_LOGIC_CASES(见 #5239,那一单已经在扩这张表)。个人倾向 拒收:它让 AI 生成的 metadata 在编写期就炸,而不是在某个后端上安静地多返回或少返回几行 —— 与 #5041 已经在 driver-sql 顶层建立的先例一致,只是把同一条闸门补到组合子内部。若嫌收紧契约代价大,次选 FALSE(跟随两个参照实现,且方向 fail-closed)。

未验证的部分:我没有找到今天真的会产出 { field: {} } 的 producer(UI 筛选器里「选了字段还没选操作符」是最可能的来源,未实测)。严重度请 PM 按 triage 定,不代表我判断它低。

关联:#5134(同函数、同「编译成空」家族,已修空组合子)、#5041(顶层那道闸门的由来)、#5239(一致性表扩条)、#3774

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions