Skip to content

service-analytics 的第二个 SQL 编译器 filter-normalizer.buildNode 仍带着 #5297 的三条分叉:$not 非 NULL-safe、{$not:{}} 不加 WHERE、$or 的 {} 析取项被丢 #5325

Closed

Description

@os-zhuang

修 #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's compileNode,
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)。

两件事因此需要先定,再动手:

  1. 守卫加在哪一层。 objectql 路径把 $not 交给引擎,而引擎背后的 driver-sql(fix(driver-sql): $not 取反前先把操作数编译成全域谓词,NULL 行不再被静默排除 (#5146) #5296
    之后)与 driver-memory 本来就已经是 NULL-safe 的 —— 在 buildNode 里加守卫会让那条
    路径双重加守卫(结果仍正确但 SQL 冗余),只在 native-sql-strategy 加则把「一个
    filter 是什么意思」的知识分到了两处。倾向前者(normalizer 是唯一的语义收敛点,与它
    自己 TSDoc 里的立场一致),但这是个契约位置的选择,不该由实现顺手定。
  2. NormalizedFilterNode 需要一个布尔常量。 该联合类型今天只有
    leaf | and | or | not,没有 FALSE 的表示法,{$not:{}} 就是因此只能编译成
    「什么都不发」。加一个常量 kind 会同时改到上面三个编译器。

关联

Activity

  1. self-assigned this
    on Aug 4, 2026
  2. os-zhuang commented on Aug 4, 2026

    @os-zhuang
    ContributorAuthor

    认领 + 派发,并裁定正文留的那个契约位置问题(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 的现场教训)

    修 #5297 时发现:一个分组是否为布尔单位元,只有等子节点编译完才知道,而编译子节点已经往 params 追加了值。照旧写法直接绑进父数组、再把子句丢掉,那些值会留在 params 里没有 ? 消费,把后面每一个占位符错位到别人的值上 —— 一条绑错值的 filter 比一条过宽的更糟。#5297 的修法是每个子节点先编译进自己的 bind 缓冲、存活了才提交。

    本单的三个编译器请逐个核对是否有同一形状的隐患,尤其 native-sql-strategy.compileFilterNode。有则一并修并单独 pin;没有则在 PR 里说明为什么不会发生(比如 params 在别处绑定),不要跳过不提。

    明确不在范围

    时序

    建议早于 spec 车道的 #5239 落地:那一单给 FILTER_LOGIC_CASES 补 null case 时,native-sql-filter-logic-conformance.test.ts 会因本文件未修而单独变红。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions