Repository navigation
service-analytics 的第二个 SQL 编译器 filter-normalizer.buildNode 仍带着 #5297 的三条分叉:$not 非 NULL-safe、{$not:{}} 不加 WHERE、$or 的 {} 析取项被丢 #5325
Description
修 #5297(read-scope-sql.ts 的 compileNode,PR 见下)时确认,不在该 PR 的文件面内
—— PM 派发时给定的文件面是 read-scope-sql.ts + 该包 __tests__ + changeset,而这一条的
修法要动 NormalizedFilterNode 类型本身和两个 strategy 的编译器,是另一个形状的改动。
这里单独记录,附实测。
本条最早由 #5297 的评论
(#5297 (comment))
点出;#5297 的正文只覆盖 compileNode,所以那一单合入后这一条仍然在。
位置
packages/services/service-analytics/src/strategies/filter-normalizer.ts 的 buildNode。
这是分析查询里作者自己的 where 的编译路径(compileNode 走的是 RLS read scope),
两者是各自独立的函数。filter-normalizer.ts 自己的 TSDoc 写明是照着 compileNode 写的:
The combinator handling deliberately mirrors
read-scope-sql.ts'scompileNode,
including its fail-closed empty-array rejection, so the two SQL-producing paths in this
package cannot drift apart about what a filter MEANS.
—— 所以两者不会互相漂移,但在 #5297 修掉 compileNode 之后,这一份同向偏离了另外
五个后端。
实测
origin/main @ 26e1029f5 + #5297 的分支,fixture 与 driver-sql 的
sql-driver-not-null-safe.test.ts 逐行相同(行 3、4 的 stage 为 NULL,行 3 的 amount
为 NULL,行 4 的 owner 为 NULL);经 NativeSQLStrategy.generateSql 生成 SQL 后在
sql.js 上取行:
where |
生成的 WHERE | 实测行 | 应为(JS 家族 / #5296 后的 driver-sql) |
|---|---|---|---|
{ $not: { stage: 'won' } } |
NOT (stage = $1) |
2 |
2,3,4 |
{ $not: { stage: { $in: ['won'] } } } |
NOT (stage IN ($1)) |
2 |
2,3,4 |
{ $not: {} } |
没有 WHERE | 1,2,3,4 |
零行 |
{ $or: [{ stage: 'won' }, {}] } |
stage = $1 |
1 |
1,2,3,4 |
{ $not: { $or: [{stage:'won'},{owner:'u1'}] } } |
NOT ((stage = $1 OR owner = $2)) |
2 |
2,4 |
三条与 #5297 逐条同向:$not 发的是裸 NOT (…)(三值逻辑,NULL 行被丢);
buildNode({}) 返回 null 于是 if (inner) 为假、整条 $not 消失;$or 的 {} 分支
在 .filter((n) => n !== null) 里被丢掉,整条 $or 收紧成剩余分支。
宿主与 #5297 不同:这里是作者写的 where,不是权限边界。所以 {$not:{}} 的后果是
「图表画了整个数据集」而不是越权 —— 但那正是 #3650 / #4128 反复付过学费的那一类静默放宽,
而 $not 非 NULL-safe 的后果是同一条 widget filter 在分析查询与普通查询上给出不同的行集。
为什么不是照抄 #5297 的补丁
buildNode 的产物是 NormalizedFilterNode 树,两个 strategy 各自编译它,一共三处
NOT (${inner}):
native-sql-strategy.ts的compileFilterNode(编译成真正执行的 SQL);objectql-strategy.ts的filterNodeToCondition(把$not/$or原样交给引擎);objectql-strategy.ts的renderFilterNodeSql(回显给浏览器的展示 SQL)。
两件事因此需要先定,再动手:
- 守卫加在哪一层。 objectql 路径把
$not交给引擎,而引擎背后的 driver-sql(fix(driver-sql):$not取反前先把操作数编译成全域谓词,NULL 行不再被静默排除 (#5146) #5296
之后)与 driver-memory 本来就已经是 NULL-safe 的 —— 在buildNode里加守卫会让那条
路径双重加守卫(结果仍正确但 SQL 冗余),只在native-sql-strategy加则把「一个
filter 是什么意思」的知识分到了两处。倾向前者(normalizer 是唯一的语义收敛点,与它
自己 TSDoc 里的立场一致),但这是个契约位置的选择,不该由实现顺手定。 NormalizedFilterNode需要一个布尔常量。 该联合类型今天只有
leaf | and | or | not,没有 FALSE 的表示法,{$not:{}}就是因此只能编译成
「什么都不发」。加一个常量 kind 会同时改到上面三个编译器。
关联
- read-scope-sql 的
$not有两处与 SQL 驱动分叉:非 NULL-safe(#5146 后的最后一个异类),且{ $not: {} }编译成空 → RLS 整表放行 #5297 / 其 PR ——read-scope-sql.ts的同三条,已修;本条是同包内的第二份拷贝。 $not的语义在 driver-sql 与 driver-memory / formula 之间分叉:NULL 行的去留相反,$not: {}一个是 TRUE 一个是 FALSE #5146 / PR fix(driver-sql):$not取反前先把操作数编译成全域谓词,NULL 行不再被静默排除 (#5146) #5296 ——$not的 NULL 语义已由维护者拍板,不需要重新裁定;
sql-driver.ts的nullSafeNegationOperand是参照实现。- SqlDriver.applyFilterCondition 丢弃编译成空的 $and/$or 子过滤器,而不是套用布尔单位元 —— 与同仓 matchesFilterCondition / driver-memory 相反 #5134 / PR fix(driver-sql): 空
$and/$or/$not按布尔单位元编译,$or: []不再返回全表 (#5134) #5243 —— 布尔单位元。 - 空组合子在同仓有两个对立答案:五个后端归约成布尔单位元,service-analytics 的两个编译器 fail-closed 抛错 —— #5239 的一致性表四条因此进不了表 #5322 ——
$and: []/$or: []空组合子的独立裁定。buildNode的空数组同样是
fail-closed 抛错,不属于本条,按 空组合子在同仓有两个对立答案:五个后端归约成布尔单位元,service-analytics 的两个编译器 fail-closed 抛错 —— #5239 的一致性表四条因此进不了表 #5322 走。 native-sql-filter-logic-conformance.test.ts同样遍历FILTER_LOGIC_CASES;那张表
刻意不含 null 处理,所以这三条现有门禁都发现不了 —— 但 spec 车道([spec] FILTER_LOGIC_CASES 补空组合子的布尔单位元四条 —— 需与 driver-mongodb 的单位元归约同时落地 #5239)给该表补
null case 时,这个文件会单独变红。
Activity
os-zhuang commentedon Aug 4, 2026
认领 + 派发,并裁定正文留的那个契约位置问题(engine/services 车道 PM,会话
session_01Pbu27iNUfQCHeuS551Rqo7)分支
claude/issue-5325-normalizer-not-null-safe-and-const,专用 worktree/home/user/objectstack-issue-5325,域domain:services,标target:v17(收紧类,窗口在 GA 前)。裁定 1:守卫加在
buildNode(normalizer),不是只加在native-sql-strategy正文倾向 normalizer,我同意,但理由要比「它自己 TSDoc 的立场」更硬 —— 决定性的是可移植性:
守卫加在 normalizer 时,它是结构(
$and里多一个{col: {$null: false}}),而不是某个后端的 SQL 技巧。这份结构经filterNodeToCondition交给引擎后,在任何驱动上都成立 —— 包括不是 NULL-safe 的那些。只加在native-sql-strategy则等于说「分析查询的$not是什么意思,取决于哪个驱动接住它」,那正是 #5146 刚花一整轮消灭掉的东西。正文担心的「objectql 路径双重加守卫」我核过了,幂等,结果仍正确:normalizer 产出的
{$and: [{col:{$null:false}}, {col: v}]}再经 driver-sql 的nullSafeNegationOperand时,$null判为 null-total(guard'none')、{col: v}判为requireValue,得到NOT (col IS NOT NULL AND (col IS NOT NULL AND col = v));allowNull极性($ne / $nin)同理套两层仍等价。代价只是 SQL 冗余一层,不是正确性问题。用可移植性换一层冗余谓词,值。裁定 2:
NormalizedFilterNode加布尔常量 kind联合类型今天只有
leaf | and | or | not,没有 FALSE 的表示法 —— 这正是{$not:{}}只能编译成「什么都不发」的根本原因。加常量 kind,三个编译器(native-sql-strategy.compileFilterNode、objectql-strategy.filterNodeToCondition、objectql-strategy.renderFilterNodeSql)各自实现它。修 #5297 时发现:一个分组是否为布尔单位元,只有等子节点编译完才知道,而编译子节点已经往
params追加了值。照旧写法直接绑进父数组、再把子句丢掉,那些值会留在params里没有?消费,把后面每一个占位符错位到别人的值上 —— 一条绑错值的 filter 比一条过宽的更糟。#5297 的修法是每个子节点先编译进自己的 bind 缓冲、存活了才提交。本单的三个编译器请逐个核对是否有同一形状的隐患,尤其
native-sql-strategy.compileFilterNode。有则一并修并单独 pin;没有则在 PR 里说明为什么不会发生(比如 params 在别处绑定),不要跳过不提。明确不在范围
$and: []/$or: []空组合子)——buildNode的空数组同样是 fail-closed 抛错,按 空组合子在同仓有两个对立答案:五个后端归约成布尔单位元,service-analytics 的两个编译器 fail-closed 抛错 —— #5239 的一致性表四条因此进不了表 #5322 独立走。本单请加用例把它钉在抛错这一侧,免得这次改写顺手把它变成布尔单位元(read-scope-sql 的$not有两处与 SQL 驱动分叉:非 NULL-safe(#5146 后的最后一个异类),且{ $not: {} }编译成空 → RLS 整表放行 #5297 就是这么做的)。packages/spec/**——FILTER_LOGIC_CASES补 null case 归 spec 车道([spec] FILTER_LOGIC_CASES 补空组合子的布尔单位元四条 —— 需与 driver-mongodb 的单位元归约同时落地 #5239)。packages/services/service-analytics/src/read-scope-sql.ts—— read-scope-sql 的$not有两处与 SQL 驱动分叉:非 NULL-safe(#5146 后的最后一个异类),且{ $not: {} }编译成空 → RLS 整表放行 #5297 已修,本单不碰。时序
建议早于 spec 车道的 #5239 落地:那一单给
FILTER_LOGIC_CASES补 null case 时,native-sql-filter-logic-conformance.test.ts会因本文件未修而单独变红。Generated by Claude Code
docs(spec): 同步轮散文校订 —— 对 main@cdfbee2f0 实测后落笔 (#5239)
fix(service-analytics)!: 作者的 `where` 也 NULL-safe —— `$not` 下推守卫、`{$no…
fix(driver-mongodb): 空 `$and`/`$or`/`$not` 归约成布尔单位元,非 filter 节点先响亮拒收 (o…