Skip to content

read-scope-sql 的 $not 有两处与 SQL 驱动分叉:非 NULL-safe(#5146 后的最后一个异类),且 { $not: {} } 编译成空 → RLS 整表放行 #5297

Closed

Description

@os-zhuang

修 #5146(driver-sql 的 $not 改 NULL-safe,PR #5296)时扫到,不在该 PR 范围内
(packages/services/service-analytics/** 是 #5115 的在飞文件面)。这里只记录。

位置

packages/services/service-analytics/src/read-scope-sql.ts 的 compileNode:

} else if (key === '$not') {
  const inner = compileNode(value, qAlias, params);
  if (inner) clauses.push(`NOT (${inner})`);
}

它是 RLS read scope 的独立 SQL 降解器(与 driver-sql 的 applyFilterCondition
不是同一段代码),被 native-sql-strategy.applyReadScope 与 objectql-strategy 用来
给分析查询加可见性约束。

分叉一:$not 非 NULL-safe

发的是裸 NOT (…)。SQL 三值逻辑下,被比较列为 NULL 的行落进 UNKNOWN 而被排除;
driver-memory / formula 返回这些行。#5146 已由维护者拍板取 NULL-safe,
PR #5296 把 driver-sql 对齐到了 JS 家族(NOT (col IS NOT NULL AND …),守卫按算子
极性下推到叶子)。该 PR 合入后,本文件是仓内最后一个仍按三值逻辑回答 $not 的
SQL 家族实现
—— 也就是说:同一条 read scope,普通查询与分析查询会给出不同的可见
集合。

修的方向已经有现成参照:PR #5296 的 nullSafeNegationOperand(纯函数,逐算子极性
表),照抄到这个编译器上即可,不需要重新裁定语义。

分叉二:{ $not: {} } 编译成空字符串 → 整表

compileNode({}) 返回 '',于是 if (inner) 为假,整条 $not 不产出任何子句;
applyReadScope 又有 if (!sql) return;,所以一条 { $not: {} } 的 read scope
完全不加 WHERE —— 本该零行(NOT TRUE ≡ FALSE),实际整表。

这正是 #5134 / PR #5243 在 driver-sql 上修掉的那条(那边现在编译成 1 = 0),
方向是静默放松,而这里的宿主是 RLS。同一段循环里 $and/$or 的空数组是
fail-closed 抛错的(第 54-56 行),所以缺的只有 $not 这一格。

顺带(方向相反、危害小得多):$or: [{}, {a: 1}] 里的 {} 编译成 '' 后被
.filter((s) => s.length > 0) 丢掉,结果是 (a = 1),而布尔代数上 {} 是 TRUE
析取项、整个 $or 为 TRUE —— 静默收紧。

建议

两处都在同一个函数里,建议一次 PR 修完(与 #5115 的在飞改动错开或合并排期)。
packages/services/service-analytics/src/__tests__/read-scope-sql-conformance.test.ts
已经在跑 FILTER_LOGIC_CASES,但那张表刻意不含 null 处理,所以两条都不会被现有
门禁发现 —— 跨驱动的 null case 归 spec 车道(#5239 / #5146 的 spec 半边)。

关联

#5146($not NULL 语义拍板)、PR #5296(driver-sql 侧实现,可直接参照)、
#5134 / PR #5243(布尔单位元)、#5115(该包在飞)。

Activity

  1. os-zhuang commented on Aug 4, 2026

    @os-zhuang
    ContributorAuthor

    实测补一条:同包内还有第二份同缺陷的拷贝,本单正文只点了 read-scope-sql.ts 的 compileNode。

    packages/services/service-analytics/src/strategies/filter-normalizer.ts 的 buildNode 是分析查询里作者自己的 where 的编译路径(compileNode 走的是 RLS read scope),两者是各自独立的函数,而三处分叉逐条同向。实测(origin/main @ 175d789,fixture 的 stage 列在行 3、4 为 NULL):

    过滤器 driver-sql(PR #5296 后) read-scope-sql filter-normalizer
    { $not: { stage: 'won' } } 行 2,3,4 行 2 行 2
    { $not: { stage: { $in: ['won'] } } } 行 2,3,4 行 2 行 2
    { $not: {} } 零行 整表 整表
    { $or: [{ a: 'x' }, {}] } 全部行 行 1,2 行 1,2

    filter-normalizer.ts 自己的注释也写明是照着 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」)—— 所以两者不会互相漂移,但同向偏离了另外五个后端。修本单时请一并处理,否则 native-sql-filter-logic-conformance.test.ts(同样遍历全部 FILTER_LOGIC_CASES)会在 spec 车道补 null case 时单独变红。

    另外分一条出去:本单正文提到「同一段循环里 $and/$or 的空数组是 fail-closed 抛错的」并把它当现状记录,但那一格其实与 #5134 / #5239 裁定的布尔单位元方向相反且有 pin 测试钉着(read-scope-sql.test.ts:86),不是照抄 PR #5296 能解决的跟进型缺陷 —— 已按需拍板另立 #5322,阻塞 #5239 的一致性表扩条。本单的两条(NULL-safe + {$not:{}})方向已定,不受其影响。


    Generated by Claude Code

  2. added
    bugSomething isn't working
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landed
    target:v17Release blocker for v17 — on the board until fixed, dropped as no longer valid, or accepted for GA
    on Aug 4, 2026
  3. self-assigned this
    on Aug 4, 2026
  4. os-zhuang commented on Aug 4, 2026

    @os-zhuang
    ContributorAuthor

    认领 + 派发(engine/services 车道 PM,会话 session_01Pbu27iNUfQCHeuS551Rqo7)

    分支 claude/issue-5297-read-scope-not-null-safe-and-empty,专用 worktree /home/user/objectstack-issue-5297,域 domain:services。
    文件面:packages/services/service-analytics/src/read-scope-sql.ts + 该包 __tests__,外加一份 changeset。

    优先级上调,插在 #5240 之前派。 理由是分叉二的宿主:

    compileNode({}) 返回空串 → if (inner) 为假 → 整条 $not 不产出子句 → applyReadScope 的 if (!sql) return; 接手 → 一条 { $not: {} } 的 RLS read scope 完全不加 WHERE。本该零行(NOT TRUE ≡ FALSE),实际整表放行。同一段循环里 $and/$or 的空数组是 fail-closed 抛错的(第 54-56 行),只漏了 $not 这一格。这与 #5134/PR #5243 在 driver-sql 上修掉的是同一条,但那边的宿主是普通查询,这边的宿主是权限边界 —— 严重度不是一个量级。

    已定范围(两处同函数,一次 PR 修完):

    1. 分叉二(先修,这是本单的要害):{ $not: {} } 按 fix(driver-sql): 空 $and/$or/$not 按布尔单位元编译,$or: [] 不再返回全表 (#5134) #5243 的口径编译为恒假子句。$or 里的 {} 空析取项被 .filter(s => s.length > 0) 丢掉(方向相反、危害小得多,但同源)一并按布尔单位元修正。
    2. 分叉一:$not 改 NULL-safe。不需要重新裁定语义 —— $not 的语义在 driver-sql 与 driver-memory / formula 之间分叉:NULL 行的去留相反,$not: {} 一个是 TRUE 一个是 FALSE #5146 已由维护者拍板,PR fix(driver-sql): $not 取反前先把操作数编译成全域谓词,NULL 行不再被静默排除 (#5146) #5296 已合入 origin/main(5aae79096),sql-driver.ts 里的 nullSafeNegationOperand 是纯函数 + 逐算子极性表,照它的口径实现即可。合入后本文件是仓内最后一个按三值逻辑回答 $not 的 SQL 家族实现。

    ⛔ 不碰 packages/spec/**(跨驱动 null case 归 spec 车道,#5239 / #5146 spec 半边)。
    ⛔ 不碰 packages/plugins/driver-sql/** —— 那是 #5240 的在飞文件面。
    ⛔ 不碰 content/docs/releases/**。

    read-scope-sql-conformance.test.ts 已在跑 FILTER_LOGIC_CASES,但那张表刻意不含 null 处理,所以两条缺陷现有门禁都发现不了 —— 本单必须自带 pin,不能指望那张表。


    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

Labels

bugSomething isn't workingdomain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landedpm:dispatchedDispatched to a dev agent by /pm-dispatchtarget:v17Release blocker for v17 — on the board until fixed, dropped as no longer valid, or accepted for GA

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions