Commit 1792384
`filter-normalizer.ts` 的 `buildNode` 是这个包里第二份同缺陷拷贝。第一份
(`read-scope-sql.ts` 的 `compileNode`,RLS 读作用域)由 #5297 修好;这一份编译的是
dashboard widget / dataset 作者自己写的 `where`,是各自独立的函数,所以那一单合入后
同样三条仍然在。以 driver-sql `sql-driver-not-null-safe.test.ts` 逐行相同的 fixture
在 sql.js 上实测(行 3、4 的 stage 为 NULL,行 3 的 amount 为 NULL,行 4 的 owner 为 NULL):
| `where` | 改前 | 改后 |
|---|---|---|
| `{ $not: { stage: 'won' } }` | `2` | `2,3,4` |
| `{ $not: { stage: { $in: ['won'] } } }` | `2` | `2,3,4` |
| `{ $not: {} }` | 全表 | 零行 |
| `{ $or: [{ stage: 'won' }, {}] }` | `1` | 全表 |
| `{ $not: { $or: [{stage:'won'},{owner:'u1'}] } }` | `2` | `2,4` |
守卫加在 normalizer 而不是 `native-sql-strategy`:在这一层它是结构(`$and` 里多一个
`{col: {$null: false}}`),经 `filterNodeToCondition` 交给引擎后在任何驱动上都成立,
包括本身不 NULL-safe 的那些。只加在 raw-SQL 那条路径等于说「分析查询的 `$not` 是什么
意思取决于哪个驱动接住它」,正是 #5146 花一整轮消灭掉的东西。引擎路径因此会双重加
守卫,已实测幂等:`NOT (c IS NOT NULL AND (c IS NOT NULL AND c = v))` 与单层等价,
代价只是一层冗余谓词。
`NormalizedFilterNode` 新增布尔常量 kind。该联合此前只有 `leaf | and | or | not`,
没有 FALSE 的表示法 —— 这正是 `{$not:{}}` 只能编译成「什么都不发」的根本原因。三个
编译器各自实现它:`native-sql-strategy.compileFilterNode`(`1 = 0` / `1 = 1`,与
`read-scope-sql` 和 driver-sql 的 `applyFalseConstant` 同一拼法)、
`objectql-strategy.filterNodeToCondition`(`{$not: {}}`,driver-sql / formula /
driver-memory 参考匹配器早已钉住的零行写法,#5134)、`renderFilterNodeSql`(回显给
浏览器的展示 SQL,它同样必须复现执行)。`collectFilterLeaves` 对常量返回空数组 ——
常量约束的是行,不是列,不参与跨对象信封检查。
params 绑定错位隐患(#5297 的现场教训)逐个核对过:改前三个编译器都不会发生,因为
每个返回 `null` 的分支都在 push 任何值之前就决定了。但本次新增的「TRUE 吸收 OR」
规则会丢弃已经编译(并已绑定)的兄弟分支,于是引入该隐患;两个 SQL 编译器因此都记下
进入组合子时的 `params.length`,吸收时截断回去(`native-sql-strategy` 连 joins 一起
还原),不变量写进 TSDoc:返回 `null` 的调用必须让 `params` 与进入时逐字节相同。
`filterNodeToCondition` 不绑值,无此形状。
一并收进来的两条,都是本次改动逼出来的,不是顺手扩范围:
- 空集合 `{$in: []}` / `{$nin: []}` 此前编译成空子句(= 无约束 = 画全表),现在是布尔
常量。不这么改,NULL-safe 的 `$not` 会把 `{$not: {a: {$in: []}}}` 从「全部行」变成
「只有 NULL 行」—— 被丢掉的合取项在否定里会翻转整条的答案。`read-scope-sql` 早就
按常量处理(`FALSE_CLAUSE` / `1 = 1`)。
- 零个操作符的字段约束 `{a: {}}` 改为拒收,按 #5240 已拍板的口径(driver-sql /
driver-memory / formula 三个后端在 #5327 已经这么做,analytics 是第四道门)。它此前
不产出任何 leaf,而「不产出」就是常量 TRUE —— 在新的吸收规则下
`{$or: [{a: {}}, {b: 2}]}` 会从 `b = 2` 放宽成全表。三个答案里必须选一个,跟随已有
拍板而不是另造第三个。
非对象的 `$not` 操作数 / `$and` `$or` 分支元素同样改为拒收:此前 `{$not: null}` 整条
消失(等于不筛),而在吸收规则下把它读成 TRUE 会放宽到全表 —— 两种读法都不是垃圾输入
的正当解释,`read-scope-sql` 拒收同样的形状。
`$and: []` / `$or: []` 的空组合子不在本单范围(独立裁定 #5322),仍然 fail-closed 抛错,
并加了用例把它钉在抛错这一侧,免得这次改写顺手把它变成布尔单位元。
反向验证:把三个源文件 stash 掉后,新用例 48 条里 36 条失败,五条实测行逐条复现 issue
正文的「实测行」那一列。
Fixes #5325
Claude-Session: https://claude.ai/code/session_01Pbu27iNUfQCHeuS551Rqo7
Co-authored-by: Claude <noreply@anthropic.com>
1 parent c7406b0 commit 1792384
5 files changed
Lines changed: 1117 additions & 33 deletions
File tree
- .changeset
- packages/services/service-analytics/src
- __tests__
- strategies
Lines changed: 56 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
0 commit comments