fix(service-analytics): read scope 的 { $not: {} } 不再整表放行,$not 改为 NULL-safe (#5297) - #5326
Merged
os-zhuang merged 1 commit intoAug 4, 2026
Merged
Conversation
…NULL-safe (#5297) `read-scope-sql.ts` 是 RLS / 租户 read scope 降解成 SQL 的唯一通道(ADR-0021 D-C),它以空字符串表示布尔常量 TRUE。两处忘掉「空串是一个值」的地方,正是本单 要修的两条分叉。 分叉二(要害):`compileNode({})` 返回空串 → `if (inner)` 为假 → 整条 `$not` 不 产出子句 → `compileScopedFilterToSql` 返回 '' → `applyReadScope` 的 `if (!sql) return;` 接手 → 生成的 SQL 里没有 WHERE。语义为 `NOT TRUE ≡ FALSE` (什么都不给看)的 read scope,实际整表放行。同一段循环里 `$and` / `$or` 的空数组 一直是 fail-closed 抛错的,只漏了 `$not` 这一格。现在编译为恒假子句 `1 = 0`,与 driver-sql 在 #5134 / PR #5243 上的口径一致。 同源、方向相反的一处一并修正:`$or` 的空析取项 `{}` 以前被 `.filter(s.length > 0)` 丢掉,`{ $or: [{}, { a: 1 }] }` 收紧成 `a = 1`。`{}` 是 TRUE 析取项,TRUE 吸收整个 析取,现在整条 `$or` 为 TRUE。被丢弃分支的绑定值随之丢弃 —— 每个子节点先编译进自己 的 params 缓冲、存活了才提交,否则 `params` 里会留下没有 `?` 消费的值,把后面每一个 占位符错位到别人的值上(一条绑错租户 id 的 read scope 比一条过宽的更糟)。 分叉一:`$not` 改为 NULL-safe。语义由 #5146 拍板,PR #5296 已在 driver-sql 落地; 本文件此前是仓内最后一个按三值逻辑回答 `$not` 的 SQL 家族实现。操作数在取反前先被 改写成全域谓词(`nullSafeNegationOperand`),守卫按算子极性下推到叶子而不是挂在 `NOT` 旁边 —— 操作数一嵌套,顶层的 `OR col IS NULL` 会把 JS 家族排除的行重新放进来。 极性表与 driver-sql 逐条对齐,两处刻意的差异写在 TSDoc 里:`$null` / `$exists` 按本 文件 emitter 的真值性读取(driver-sql 的 emitter 按与 false 的恒等读取),以及本文件 多出的 `$between` 走正向比较的默认值。 `$not` 路径以外一个字符未动,fail-closed 的全部保证原封不动 —— 未知算子、嵌套关系 值、裸数组、不安全标识符、非 filter 节点的操作数,以及 `$and: []` / `$or: []` 的空 组合子(#5322 的独立裁定)统统照旧抛错。 测试:`read-scope-not-null-safe.test.ts` 是本单自带的 pin。既有的 `read-scope-sql-conformance.test.ts` 跑的 `FILTER_LOGIC_CASES` 刻意不含 null 行、 也不含布尔单位元,两条缺陷因此都在现有门禁下绿着。新用例在 sql.js 上执行取行, `{$not:{}}` 一条断言到 `NativeSQLStrategy.generateSql` 这一层 —— 缺陷正在编译器与 `applyReadScope` 的接缝处,只测 `compileNode` 抓不到。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pbu27iNUfQCHeuS551Rqo7
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 8 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
marked this pull request as ready for review
August 4, 2026 20:47
os-zhuang
deleted the
claude/issue-5297-read-scope-not-null-safe-and-empty
branch
August 4, 2026 20:53
os-zhuang
pushed a commit
that referenced
this pull request
Aug 4, 2026
Second relay merge: #5300 / #5304 / #5306 / #5308 / #5318 / #5326 / #5327. Textually clean, but the os-regen driver defers generated artifacts rather than text-merging them, so `json-schema.manifest.json` again came out holding this branch's pre-merge side — this time still listing `ui/EmbedConfig` and `ui/NotificationAction`, both retired by #5300. Reset the deferred artifacts to `origin/main`, rebuilt from the merged tree, regenerated wholesale. Post-regen assertions (a silent one-side drop is exactly what this catches): api-surface delta vs `origin/main` is exactly this PR's four additions and ZERO removals; manifest delta is one addition (`ui/ViewItemWire`) and zero removals; every sibling retirement stays removed (`ui/EmbedConfig`, `ui/NotificationAction`, `system/HttpServerConfig`, `ui/Animation`, `ui/ZIndex`) and every sibling addition stays present (`FilterArray` ×7, `EmailProvider` ×2). `check:authorable-surface` (+ its #5304 `.base.json` anchor) is green and the anchor file is byte-identical to `origin/main` — not hand-edited. `metadata-form-zod-reconciliation.test.ts` co-edited with #5280/#5318 and merged SEMANTICALLY, not by taking a side: #5318 rewrote the docblock, imports, helpers and test bodies, while this PR's only edit is `unwrap`'s `pipe` case, so the two did not overlap textually — but they do interact, and in the direction that matters. #5318's `isRetiredAt` / `authorableKeysOf` both route through `unwrap`/`keysOf`, and `view`'s root is now a `z.preprocess` pipe. Measured both ways: without this PR's #4488-style fix `unwrap(view root)` resolves to `transform` and `keysOf` returns NULL, so #5318's brand-new tombstone assertions would be VACUOUS on `view` (and the pre-existing key-bearing assertion would fail outright); with it, 89 keys. Both PRs' assertions are live on every type. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB
This was referenced Aug 4, 2026
Closed
fix(service-analytics)!: 作者的
where 也 NULL-safe —— $not 下推守卫、{$not:{}} 为零行、{} 析取项吸收 $or (#5325)
#5335
Merged
os-zhuang
pushed a commit
that referenced
this pull request
Aug 5, 2026
本 PR 的 spec 半边是契约文档,机械合并会把 base 时代的论断带上 main; 逐条对当前 origin/main 实测后校订: - FilterConditionSchema 的 NULL-safe $not 合规段:read-scope-sql 已由 #5326 对齐(#5297 关闭)、filter-normalizer 已由 #5335 对齐(#5325 关闭),七个面全部一致 —— 「尚未合规、指向 #5297」改写为已闭合的事实。 - 「Deliberately NOT declared here」:空组合子单位元由「两立场对峙、 上交 #5322」改为「#5322 已拍板取单位元,实施在 #5365(排在本 PR 之后 合入);main 上两个 analytics 编译器今天仍拒收,故本 PR 仍不在此声明, 声明随 #5365 翻正」;{ field: {} } 由「无后端设闸」改为「#5327 已闸 四家,driver-mongodb 是唯一还在作答的后端(#5376)」。 - filter-logic-conformance.ts 族 2/3 状态行同步重测:族 2 的后端阻塞 已清零,唯余 fixture 工作;族 3 的四家闸门已落,阻塞改为表形扩展 + mongodb(#5376)。族 1 段落一字未动 —— 由 #5365 在其同步轮删除, 已约定分工。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #5297
packages/services/service-analytics/src/read-scope-sql.ts是 RLS / 租户 read scope 降解成SQL 的唯一通道(ADR-0021 D-C),被
NativeSQLStrategy.applyReadScope与ObjectQLStrategy用来给分析查询加可见性约束。它以空字符串表示布尔常量 TRUE。两处忘掉「空串是一个值」的地方,
正是本单的两条分叉。
现场核对(STALE-PREMISE)
issue 今天 16:07 写成,
origin/main此后又前进了 8 个 commit。worktree 基于26e1029f5。逐条核对 issue 引用的现场:compileNode的$not分支(const inner = compileNode(value, …); if (inner) …)applyReadScope的if (!sql) return;native-sql-strategy.ts:284)$and/$or空数组 fail-closed 抛错$or的.filter((s) => s.length > 0)丢掉空析取项read-scope-sql.ts上一次被改动是 #5007,今天的 8 个 commit 一个都没碰它。今天改过本包的PR #5287(#5115,跨 datasource JOIN 编译期拒绝)动的是
dataset-compiler.ts/analytics-service.ts/index.ts,与本单文件零交叠,合并后无冲突。分叉二(要害):
{ $not: {} }的 read scope 完全不加 WHERE一条语义为
NOT TRUE ≡ FALSE(什么都不给看)的 read scope,实际是整张表都给看。同一段循环里
$and/$or的空数组一直是 fail-closed 抛错的,只漏了$not这一格。修法:引入
FALSE_CLAUSE = '1 = 0'(与本文件$in: []已有的写法、以及 driver-sql 在#5134 / PR #5243 上的口径一致),
$not的操作数编译为空串时压入恒假子句。applyReadScope因此照常拼进 WHERE,返回零行。
同源、方向相反的一处一并修正:
{}是 TRUE 析取项,而 TRUE 吸收整个析取,所以{ $or: [{}, { a: 1 }] }整条为 TRUE,不再被.filter((s) => s.length > 0)收紧成a = 1。$and那一侧不变 —— TRUE 是合取的单位元,丢掉本来就对。顺带修掉一处只有在这次改动之后才会发作的绑定错位:一个分组是否为布尔单位元,只有等它的
子节点都编译完才知道,而编译子节点会往
params里追加值。如果照着旧写法直接绑进父数组、再把子句丢掉,那些值就留在
params里没有?消费,把后面每一个占位符错位到别人的值上 ——一条绑错租户 id 的 read scope 比一条过宽的更糟。所以每个子节点先编译进自己的 params 缓冲
(
compileSub),存活了才提交。测试里有一条专钉这个。分叉一:
$not改为 NULL-safe语义已由维护者在 #5146 拍板,PR #5296(
5aae79096)已在 driver-sql 落地 —— 本文件此前是仓内最后一个按三值逻辑回答
$not的 SQL 家族实现。照
sql-driver.ts的nullSafeNegationOperand口径实现:纯函数、逐算子极性表、守卫下推到叶子而不是挂在
NOT旁边(操作数一嵌套两者就不等价 ——$not里套$or时,顶层的
OR col IS NULL会把 JS 家族排除的行重新放进来)。极性按算子逐个判定而不是一刀切:{ $not: { a: { $ne: 5 } } }语义是「a 就是 5」,无条件加OR a IS NULL会把 scope 排除的行交回去。
与 driver-sql 极性表的差异(逐条列出)
本编译器支持的算子集与 driver-sql 不完全相同,按 issue 的要求以本文件实际支持的为准:
$null/$exists的读法val ? … : …)false的恒等compileOperator写的是val ? IS NULL : IS NOT NULL,守卫必须和它读法一致,否则两者会在非布尔值上分歧。不变量是「守卫跟着自己的 emitter」,不是那行字面量。$between$gt/$in/$contains一族同类,取同一个默认。{ field: {} }'none'原样透传,以保留原错误$not路径以外一个字符未动,fail-closed 的全部保证原封不动:未知算子、嵌套关系值、裸数组、不安全标识符、非 filter 节点的
$not操作数,以及$and: []/$or: []的空组合子(那一格是 #5322 的独立裁定,本 PR 刻意不碰)统统照旧抛错 —— 每一条都有用例钉着。
测试
read-scope-sql-conformance.test.ts已在跑共享的FILTER_LOGIC_CASES,但那张表刻意不含null 行、也不含布尔单位元(归 spec 车道 #5239 / #5146 的 spec 半边),所以两条缺陷在现有
门禁下都是绿的。本 PR 自带 pin:
packages/services/service-analytics/src/__tests__/read-scope-not-null-safe.test.ts(36 例,在
sql.js上执行取行,不是断言字符串)。关键的一条断言到
NativeSQLStrategy.generateSql这一层 —— 缺陷正在编译器与applyReadScope的接缝处,只测compileNode抓不到。参照答案是实测得到的:fixture 与
driver-sql的sql-driver-not-null-safe.test.ts逐行相同,每一组 id 都与该文件及两个 JS 后端的 pin
(
formula/src/matches-filter-not-null-safe.test.ts、driver-memory/src/memory-matcher-not-null-safe.test.ts)对同一 filter 的答案一致,写用例时三套都跑过。
反向验证(stash 掉
read-scope-sql.ts,新用例必须红)36 例中 21 例失败,其余 15 例是 fail-closed / 未改动行为的用例,本就该保持绿。失败原文节选:
第二条
Received就是这一单的要害:一条本该零行的 read scope,生成的 SQL 里没有 WHERE。覆盖到的行为
{$not:{}}1 = 0;取行为零;经generateSql确实产出 WHERE;与真实谓词同层嵌套仍为 FALSE;{$not:{$not:{}}}回到 TRUE$or的{}析取项$and的{}成员行为不变$notNULL-safe!(stage == 'won')形状;经generateSql的形状与绑定$notof$or(守卫必须在叶子才对的那一例)、$notof$and、双重否定、$not与兄弟键 AND、$or分支里的$not$ne/$nin不被放宽;$in/$gt/$between/$contains/$startsWith/$endsWith返回 NULL 行;$notContains的镜像case;$null/$exists/$eq: null/$ne: null一个字节不加;空$in/$nin的常量值$ne在否定之外仍是三值逻辑)$and/$or(含$not之内)、未知算子、嵌套关系值、裸数组、零算子 spec、非 filter 节点操作数、不安全标识符 —— 全部仍抛错验证命令
pnpm --filter @objectstack/service-analytics exec vitest run --maxWorkers=243 files / 591 tests passednpx tsc --noEmit -p tsconfig.json(该包无typecheckscript)analytics-service.test.ts/measure-source-field-gate.test.ts/objectql-timedimension-projection.test.ts,均为既有问题,不在本 PR 文件面内)npx eslint(两个改动文件)changeset
.changeset/read-scope-not-null-safe-and-empty.md(@objectstack/service-analytics: patch)。正文明确写明这是一次安全相关的行为变更:原本
{ $not: {} }的 read scope 不加任何WHERE、整表可见,修复后为零行;以及
$not下 NULL 行的去留变化会改变分析查询的行数与图表数值。没有轻描淡写。
范围外
packages/spec/**、packages/plugins/driver-sql/**、packages/plugins/driver-memory/**、packages/formula/**、content/docs/releases/**、os-regen 生成物 —— 一行未改,只读作参照。
$and: []/$or: []空组合子的对立答案)本 PR 刻意不碰,并加了用例把它钉在抛错这一侧,免得这次改写顺手把它变成布尔单位元。
filter-normalizer.buildNode仍带着 #5297 的三条分叉:$not非 NULL-safe、{$not:{}}不加 WHERE、$or的{}析取项被丢 #5325 —— 同包内第二份同缺陷的拷贝:strategies/filter-normalizer.ts的buildNode(作者自己where的编译路径,与compileNode各自独立)三条分叉逐条同向,已实测记录。没有并入本 PR,因为它的修法要给
NormalizedFilterNode加布尔常量 kind、并同时改到
native-sql-strategy与objectql-strategy的三处NOT (…),且「守卫加在 normalizer还是只加在 native-sql 路径」是个需要先定的契约位置问题(objectql 路径把
$not原样交给引擎,引擎背后的驱动已经是 NULL-safe 的)。不是照抄本 PR 能解决的形状。
🤖 Generated with Claude Code
https://claude.ai/code/session_01Pbu27iNUfQCHeuS551Rqo7
Generated by Claude Code