实现 #5239 的 spec 半边(FILTER_LOGIC_CASES 扩四条布尔单位元)时实测发现,不在该单范围内,按 Prime Directive #10 单独记在这里,unassigned。
#5239 的前提是「四条只差 driver-mongodb 一家,补上归约即可全绿」。实测这条前提不成立:一致性表在册的另外两个后端对同样四条给出的不是「错的答案」,而是相反的立场,并且写在代码注释里、还有 pin 测试钉着。
实测矩阵(origin/main @ 175d789,四行 fixture)
| 后端 |
{$and:[]} |
{$or:[]} |
{$or:[{a:'x'},{}]} |
{$not:{}} |
| 期望(#5134 / #5239 裁定) |
全部行 |
零行 |
全部行 |
零行 |
formula matchesFilterCondition |
全部行 |
零行 |
全部行 |
零行 |
driver-memory |
全部行 |
零行 |
全部行 |
零行 |
driver-sql(#5134 / PR #5243) |
全部行 |
零行 |
全部行 |
零行 |
driver-sqlite-wasm |
全部行 |
零行 |
全部行 |
零行 |
driver-mongodb(#5239 本次) |
全部行 |
零行 |
全部行 |
零行 |
read-scope-sql |
抛错 |
抛错 |
行 1,2 |
整表 |
analytics filter-normalizer |
抛错 |
抛错 |
行 1,2 |
整表 |
两处落点:
packages/services/service-analytics/src/read-scope-sql.ts 的 compileNode(第 53-56 行)
packages/services/service-analytics/src/strategies/filter-normalizer.ts 的 buildNode(第 249-255 行)
两者都被一致性表在册:read-scope-sql-conformance.test.ts 与 native-sql-filter-logic-conformance.test.ts 各自遍历全部 FILTER_LOGIC_CASES。所以四条一进表,这两个 suite 立刻各红四格。
为什么这是「要拍板」而不是「有人没跟上」
对立是成文的,不是遗漏。filter-normalizer.ts 的错误消息逐字写着:
"$and" requires a non-empty array. An empty combinator has no defensible reading — dropping it widens the query, and treating it as "match nothing" silently empties a chart.
「treating it as match nothing」正是 #5134 为 $or: [] 定下的答案。而 read-scope-sql.test.ts:86 用 expect(...).toThrowError(/non-empty array/) 把这个抛错钉住了。所以这不是「照抄 #5296 即可」的那类跟进(#5297 的 $not 就是那类),而是两条已落地的立场必须有一条让步。
顺带注意:「响亮拒收」本身是有先例的答案 —— #5240 对 { field: {} } 取的正是拒收,理由是「让 AI 生成的元数据在编写期就炸」。同一条理由套在空组合子上同样成立:一个 disjunct 列表循环出零项,几乎必然是 producer 的 bug,而不是作者想表达「零行」。
两轴分析
A. 项目长远合理性
B. 让 AI 写的元数据难写错
推荐:取单位元,analytics 两处对齐 —— 理由是 A 轴的组合性(拒收无法回答嵌套),加上 B 轴上 $or: [] = 零行本身已经是 fail-closed。若维护者更看重编写期信号,可以两者兼取:归约照做,同时在 publish/lint 面对字面量空组合子响亮拒收 —— 运行期语义单一,作者错误仍在编写期爆炸,这是 PD #12「在生产端拒绝、不要在消费端容忍」的标准形状,也是唯一不需要任何一边让步的读法。
连带
关联
#5239(一致性表扩条,被本单阻塞)、#5134 / PR #5243(driver-sql 单位元)、#5297(同两处的 $not 分叉)、#5240({field:{}} 取拒收的先例)、cloud#1073、#3774。
实现 #5239 的 spec 半边(FILTER_LOGIC_CASES 扩四条布尔单位元)时实测发现,不在该单范围内,按 Prime Directive #10 单独记在这里,unassigned。
#5239 的前提是「四条只差 driver-mongodb 一家,补上归约即可全绿」。实测这条前提不成立:一致性表在册的另外两个后端对同样四条给出的不是「错的答案」,而是相反的立场,并且写在代码注释里、还有 pin 测试钉着。
实测矩阵(
origin/main@175d789,四行 fixture){$and:[]}{$or:[]}{$or:[{a:'x'},{}]}{$not:{}}formulamatchesFilterConditiondriver-memorydriver-sql(#5134 / PR #5243)driver-sqlite-wasmdriver-mongodb(#5239 本次)read-scope-sqlfilter-normalizer两处落点:
packages/services/service-analytics/src/read-scope-sql.ts的compileNode(第 53-56 行)packages/services/service-analytics/src/strategies/filter-normalizer.ts的buildNode(第 249-255 行)两者都被一致性表在册:
read-scope-sql-conformance.test.ts与native-sql-filter-logic-conformance.test.ts各自遍历全部FILTER_LOGIC_CASES。所以四条一进表,这两个 suite 立刻各红四格。为什么这是「要拍板」而不是「有人没跟上」
对立是成文的,不是遗漏。
filter-normalizer.ts的错误消息逐字写着:「treating it as match nothing」正是 #5134 为
$or: []定下的答案。而read-scope-sql.test.ts:86用expect(...).toThrowError(/non-empty array/)把这个抛错钉住了。所以这不是「照抄 #5296 即可」的那类跟进(#5297 的$not就是那类),而是两条已落地的立场必须有一条让步。顺带注意:「响亮拒收」本身是有先例的答案 —— #5240 对
{ field: {} }取的正是拒收,理由是「让 AI 生成的元数据在编写期就炸」。同一条理由套在空组合子上同样成立:一个 disjunct 列表循环出零项,几乎必然是 producer 的 bug,而不是作者想表达「零行」。两轴分析
A. 项目长远合理性
{}/{$and:[]}/{$or:[]}/{$not:{}}会在嵌套里互相组合($notof$or:[]、$and里含$or:[]),只有三值归约能把整棵树算完。拒收方案必须回答「嵌在$or第三个分支里的$and: []算不算错」—— 拒收要么过度(合法的空合取也炸),要么就得先归约再判空,那已经是单位元方案了。代价:analytics 两处要改,read-scope-sql.test.ts的 pin 要翻向。{ field: {} }(零个操作符的字段约束)在同仓有三个答案:driver-sql 组合子内 TRUE、顶层抛 INVALID_FILTER、formula/driver-memory FALSE #5240 同向,producer 的 bug 在编写期爆炸。代价:要反向改五个后端(含刚合入的 PR fix(driver-sql): 空$and/$or/$not按布尔单位元编译,$or: []不再返回全表 (#5134) #5243 / fix(driver-sql):$not取反前先把操作数编译成全域谓词,NULL 行不再被静默排除 (#5146) #5296),而$or: []在 RLS 里的正确答案(零行)会退化成 500 —— 一条本可安全求值的 scope 变成整条请求失败。B. 让 AI 写的元数据难写错
{$or: []}= 零行是 fail-closed 的 —— 循环出零项的 RLS scope 隐藏全部行,而不是放行全表(那是 SqlDriver.applyFilterCondition 丢弃编译成空的 $and/$or 子过滤器,而不是套用布尔单位元 —— 与同仓 matchesFilterCondition / driver-memory 相反 #5134 修掉的 P0)。但作者拿不到任何信号。updateMany/deleteMany也走同一个 translate 层,那里「炸掉」和「零行」的安全性没差别,而「炸掉」会把一次本可正确执行的批量写变成失败。推荐:取单位元,analytics 两处对齐 —— 理由是 A 轴的组合性(拒收无法回答嵌套),加上 B 轴上
$or: []= 零行本身已经是 fail-closed。若维护者更看重编写期信号,可以两者兼取:归约照做,同时在 publish/lint 面对字面量空组合子响亮拒收 —— 运行期语义单一,作者错误仍在编写期爆炸,这是 PD #12「在生产端拒绝、不要在消费端容忍」的标准形状,也是唯一不需要任何一边让步的读法。连带
$not非 NULL-safe($not的语义在 driver-sql 与 driver-memory / formula 之间分叉:NULL 行的去留相反,$not: {}一个是 TRUE 一个是 FALSE #5146 后的少数派)已由 read-scope-sql 的$not有两处与 SQL 驱动分叉:非 NULL-safe(#5146 后的最后一个异类),且{ $not: {} }编译成空 → RLS 整表放行 #5297 记录 —— 但 read-scope-sql 的$not有两处与 SQL 驱动分叉:非 NULL-safe(#5146 后的最后一个异类),且{ $not: {} }编译成空 → RLS 整表放行 #5297 只点了read-scope-sql.ts,filter-normalizer.ts是同缺陷的第二份拷贝,已在 read-scope-sql 的$not有两处与 SQL 驱动分叉:非 NULL-safe(#5146 后的最后一个异类),且{ $not: {} }编译成空 → RLS 整表放行 #5297 补注。FILTER_LOGIC_CASES不能加这四条(加了就是把另一条车道的未决工作变成这张表的红)。已在filter-logic-conformance.ts留下实测矩阵与本单编号,PR 见 [spec] FILTER_LOGIC_CASES 补空组合子的布尔单位元四条 —— 需与 driver-mongodb 的单位元归约同时落地 #5239 的分支。关联
#5239(一致性表扩条,被本单阻塞)、#5134 / PR #5243(driver-sql 单位元)、#5297(同两处的
$not分叉)、#5240({field:{}}取拒收的先例)、cloud#1073、#3774。