fix(service-analytics)!: 作者的 where 也 NULL-safe —— $not 下推守卫、{$not:{}} 为零行、{} 析取项吸收 $or (#5325) - #5335
Merged
os-zhuang merged 1 commit intoAug 4, 2026
Conversation
…t:{}}` 为零行、`{}` 析取项吸收 `$or` (#5325)
`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
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 23:44
os-zhuang
deleted the
claude/issue-5325-normalizer-not-null-safe-and-const
branch
August 4, 2026 23:49
This was referenced Aug 5, 2026
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 was referenced Aug 5, 2026
os-zhuang
pushed a commit
that referenced
this pull request
Aug 5, 2026
…,不是 00:0xZ issue #5441 正文写「#5335 在 00:0xZ 合入」,核 GitHub API 的 merged_at 与 main 上 squash 提交的 committer date,两者一致给出 2026-08-04T23:49:44Z。改写成「起飞后 32 分钟合入(merged_at 2026-08-04T23:49:44Z)」,把不可核验的钟点换成可核验的 时间差 + 权威字段;起飞时刻 23:17Z 沿用 issue 的记述(无仓内产物可核,且非承重)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GX3sL71LFq8m2usg6VqTSE
This was referenced Aug 5, 2026
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Aug 6, 2026
…bjectstack-ai#5239) (objectstack-ai#5323) * fix(driver-mongodb): reduce empty $and/$or/$not to their boolean identity, refusing non-nodes first (objectstack-ai#5239) `translateFilter` passed combinator arrays through verbatim, and MongoDB answers an empty one with neither TRUE nor FALSE but a third behaviour: it refuses the query (`$and/$or/$nor must be a nonempty array`). So `{$and: []}` and `{$or: []}` reached find/count/updateMany/deleteMany as a server error carrying no ADR-0112 code, while driver-sql (objectstack-ai#5134), driver-memory and formula all answered them as identities. Replaced with the same STRUCTURAL three-valued reduction: reduce the whole tree to true/false/clause first, then emit. Empty `$and` becomes TRUE (no condition); empty `$or` becomes FALSE and emits a real zero-row condition (`{_id: {$in: []}}`) — emitting nothing would be `{}`, which find/updateMany/ deleteMany read as EVERY document, the opposite answer. Every `$and`/`$or` array emitted is therefore guaranteed non-empty. Shape rejection lands in the same change and runs BEFORE any identity: measured on main, `{$or: [new Date()]}` translated to `{$or: [{}]}` (every document) and `{$or: 'x'}` / `{$not: null}` translated to `{}` (every document). updateMany and deleteMany translate the same `where`, where that is data loss rather than a wrong row count. Non-nodes now raise INVALID_FILTER / 400 naming the position; the gate judges by PROTOTYPE, since Date/RegExp/class instances satisfy `typeof x === 'object'` while enumerating empty. spec is documentation only: FilterConditionSchema's contract TSDoc now states the NULL-safe `$not` semantics ruled in objectstack-ai#5146, and filter-logic-conformance.ts records the measured matrix for the three ruled-but-not-yet-enrolled case families. The four FILTER_LOGIC_CASES rows objectstack-ai#5239 asks for are deliberately NOT added: read-scope-sql and the analytics filter-normalizer, both enrolled backends, refuse empty combinators fail-closed by design and pinned test, which contradicts the identity ruling — escalated as objectstack-ai#5322. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB * docs(spec): 同步轮散文校订 —— 对 main@cdfbee2f0 实测后落笔 (objectstack-ai#5239) 本 PR 的 spec 半边是契约文档,机械合并会把 base 时代的论断带上 main; 逐条对当前 origin/main 实测后校订: - FilterConditionSchema 的 NULL-safe $not 合规段:read-scope-sql 已由 objectstack-ai#5326 对齐(objectstack-ai#5297 关闭)、filter-normalizer 已由 objectstack-ai#5335 对齐(objectstack-ai#5325 关闭),七个面全部一致 —— 「尚未合规、指向 objectstack-ai#5297」改写为已闭合的事实。 - 「Deliberately NOT declared here」:空组合子单位元由「两立场对峙、 上交 objectstack-ai#5322」改为「objectstack-ai#5322 已拍板取单位元,实施在 objectstack-ai#5365(排在本 PR 之后 合入);main 上两个 analytics 编译器今天仍拒收,故本 PR 仍不在此声明, 声明随 objectstack-ai#5365 翻正」;{ field: {} } 由「无后端设闸」改为「objectstack-ai#5327 已闸 四家,driver-mongodb 是唯一还在作答的后端(objectstack-ai#5376)」。 - filter-logic-conformance.ts 族 2/3 状态行同步重测:族 2 的后端阻塞 已清零,唯余 fixture 工作;族 3 的四家闸门已落,阻塞改为表形扩展 + mongodb(objectstack-ai#5376)。族 1 段落一字未动 —— 由 objectstack-ai#5365 在其同步轮删除, 已约定分工。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB * chore(spec): gen:schema 前移 authorable-surface 锚点至合并后的 merge-base (cdfbee2) 合并 origin/main 后重建时由 gen:schema 写出(先 commit merge 再跑生成, objectstack-ai#5370 的锚点倒退陷阱按序避开):baseRev 28ad90e → cdfbee2,随锚点带入 objectstack-ai#5312 的 api/ApiEndpoint 键面。check:generated 9/9 up to date, check:authorable-surface 绿。非手改。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB --------- Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Aug 6, 2026
…tstack-ai#5322) (objectstack-ai#5365) * fix(service-analytics): 空组合子按布尔单位元归约,两个编译器对齐五后端 (objectstack-ai#5322) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB * chore: changeset for objectstack-ai#5322 (service-analytics + spec) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB * fix(service-analytics): 空数组单位元在 objectstack-ai#5335 的 const 节点体系上重放,pin 翻向 (objectstack-ai#5322) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB * chore(objectstack-ai#5322): 收官同步 —— filter.zod 空组合子宣告转正、objectstack-ai#5366 refusal 表随裁定翻向 - filter.zod.ts:按 objectstack-ai#5323 同步散文预留的交接("The declaration flips to stated contract with that PR"),空组合子单位元从「Deliberately NOT declared」段转为正式契约段;{field:{}} 半边保持未宣告(objectstack-ai#5376 仍开)。 - filter-refusal-envelope.test.ts(objectstack-ai#5366 新到):空数组两行从 REFUSALS 翻入 ACCEPTED(单位元树断言),同一守卫点的非数组拼写补位 REFUSALS,信封不变。 - filter-logic-conformance.ts:族 1 段落按分工删除(四行已进表),族 2/3 原样。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB * test(rest): objectstack-ai#5352 信封 suite 的空 $or 行随 objectstack-ai#5322 拍板翻向 —— 单位元 200+行数语义,非数组拼写补位 400 REST 层是 objectstack-ai#5352 refusal pin 的第三份拷贝(service-analytics 两份已翻)。 harness 的 executeAggregate 从常量改为按引擎侧 filter 求值,四条单位元 断言(空 $or 零行、空 $and 全部行、{$not:{}} 零行、{} 析取项吸收) 因此承重 —— 200 之外还钉行数,与被丢弃的 filter 可区分。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB --------- Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Aug 6, 2026
…摆纠偏、飞行中重叠、预期红停放 (objectstack-ai#5441) (objectstack-ai#5501) * docs(pm-dispatch,os-dev): 串行接力一夜沉淀的六条缺口补进 SKILL —— 接力模式、锚点措辞、裁决传播扫描、停摆纠偏、飞行中重叠、预期红停放 (objectstack-ai#5441) 2026-08-04/05 夜 spec 车道以串行接力连落 10 个 PR(objectstack-ai#5304 → objectstack-ai#5365),六个情形是 现行 SKILL 没有覆盖、靠现场即兴的,各有实付学费。按 issue 注明的落点章节逐条插入: - 第 7 步「入队与落地」新增平行小节「串行接力」:每棒一整圈(auto-merge 由 PM 挂、 dev 永不碰,ready 与 auto-merge 顺序不可反且每棒各走一次)、相邻棒同文件交接语义 而非文本(objectstack-ai#5318/objectstack-ai#5319 实例)、两棒散文互锁由 PM 指派分工(objectstack-ai#5323↔objectstack-ai#5365、objectstack-ai#5335)。 - 「入队与落地 A」新增锚点断言措辞:authenticity = baseRev 是 origin/main 祖先 且 keys 与该 commit 逐行一致;baseRev 允许滞后;⛔ 不得要求 baseRev == merge-base (那会教唆手改锚点,即 objectstack-ai#4650 攻击自身)。四步序的第 3 步补上「先 commit merge」, 并引用 objectstack-ai#5370 / objectstack-ai#5371 两个新陷阱(仅引用,不实现)。 - 第 5 步派发词 + 第 7 步 review 新增「裁决传播 = 全仓 pin 扫描」:翻 pin 一轮翻完 且必须保留承重(objectstack-ai#5322 裁决、objectstack-ai#5365 的 REST 层漏翻)。 - 第 6 步 Collect 的 subagent 半边新增停摆纠偏:watcher 永不触发,中途状态即停摆 信号,第三次视为不可靠改走接手协议。生产端半边同步落到 os-dev.md 资源纪律第 6 条。 - 第 5 步 same-day churn 新增姊妹段「飞行中范围重叠」:每轮读 origin/main 时对每个 在飞 dispatch 做相交判断,相交即预警(objectstack-ai#5322 × objectstack-ai#5335 实例)。 - 「入队与落地 B」新增依赖 PR 的预期红停放:draft 停放 + 签名级预期红清单 + 解除 条件,新签名才是真问题(objectstack-ai#5365 的 REST 红即由此识别)。 仅改 `.claude/` 内部 agent 协议文本,不发布任何包;六条落点之外未动任何段落。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GX3sL71LFq8m2usg6VqTSE * docs(pm-dispatch): 停摆纠偏第三条的交叉引用改指「step 5」—— 接手协议在第 5 步,不在第 6 步之下 自查发现的方向错误:「Handing off an interrupted dev」小节是第 5 步的子节 (SKILL.md:839),而新增段落写在第 6 步(:923),原文写「below」会把读者指向 第 6 步之后的 Cloud mode 段。同段里对 cloud-mode ~2h 阈值的「below」是对的,保留。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GX3sL71LFq8m2usg6VqTSE * docs(pm-dispatch): 飞行中重叠一段的时间戳改用可核验值 —— objectstack-ai#5335 的 merged_at 是 23:49:44Z,不是 00:0xZ issue objectstack-ai#5441 正文写「objectstack-ai#5335 在 00:0xZ 合入」,核 GitHub API 的 merged_at 与 main 上 squash 提交的 committer date,两者一致给出 2026-08-04T23:49:44Z。改写成「起飞后 32 分钟合入(merged_at 2026-08-04T23:49:44Z)」,把不可核验的钟点换成可核验的 时间差 + 权威字段;起飞时刻 23:17Z 沿用 issue 的记述(无仓内产物可核,且非承重)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GX3sL71LFq8m2usg6VqTSE --------- Co-authored-by: Claude <noreply@anthropic.com>
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 #5325
filter-normalizer.ts的buildNode是service-analytics里第二份同缺陷拷贝。第一份(read-scope-sql.ts的compileNode,RLS 读作用域)由 #5297 / PR #5326 修好并已合入;这一份编译的是 dashboard widget / dataset 作者自己写的where,两者是各自独立的函数,所以那一单落地后同样三条仍然在。现场核对(STALE-PREMISE)
issue 正文写于
26e1029f5+ #5297 的分支。worktree 基于origin/main,开工后又同步到c7406b0ec(含 #5319、#5329)。issue 引用的现场逐条仍然成立:buildNode的$not分支if (inner) children.push(…){$not:{}}因此整条消失$and/$or的.filter((n) => n !== null){}析取项被丢NOT (${inner})NormalizedFilterNode只有leaf | and | or | notFILTER_LOGIC_CASES不含 null case期间落地的 #5329(#5158 拍板 C 第 2 步)只动 engine 入口与四个驱动,不与本单冲突 —— 但暴露了 analytics 是它漏掉的第五道门,已单独记录(见范围外清单)。
实测(sql.js,fixture 与 driver-sql 的
sql-driver-not-null-safe.test.ts逐行相同)行 3、4 的
stage为 NULL,行 3 的amount为 NULL,行 4 的owner为 NULL。断言打到NativeSQLStrategy.generateSql/.execute这一层(#5297 的教训:缺陷常在编译器与调用方的接缝处)。where{ $not: { stage: 'won' } }22,3,42,3,4✅{ $not: { stage: { $in: ['won'] } } }22,3,42,3,4✅{ $not: {} }1,2,3,4{ $or: [{ stage: 'won' }, {}] }11,2,3,41,2,3,4✅{ $not: { $or: [{stage:'won'},{owner:'u1'}] } }22,42,4✅「改前」一列是把三个源文件
git stash掉后由同一批用例实测出来的,与 issue 正文的「实测行」逐条吻合。两条裁定的实现
裁定 1 —— 守卫加在
buildNode(normalizer)nullSafeNegationOperand重写$not的操作数(仍是一个FilterCondition),把守卫下推到叶子,极性逐算子决定;buildNode再照常编译它,没有新的分支。因此守卫是结构而不是 SQL 技巧:
{$not: {stage: 'won'}}的操作数变成{$and: [{stage: {$null: false}}, {stage: 'won'}]},这份结构经filterNodeToCondition交给 ObjectQL 引擎后在任何驱动上都成立。双重加守卫已用执行验证幂等,不只是推演:测试里
compileScopedFilterToSql(同包另一个FilterCondition消费者,#5297 已是 NULL-safe)充当引擎,对本路径已经守卫过的条件再守卫一次,执行后取到的行与 raw-SQL 路径逐行相同(2,3,4),SQL 里出现两层IS NOT NULL。裁定 2 ——
NormalizedFilterNode加布尔常量 kind| { kind: 'const'; value: boolean }。TRUE 保留既有拼法(null= 无约束 = AND 单位元),FALSE 需要一个节点因为它必须活着进 WHERE。四个消费者各自实现:native-sql-strategy.compileFilterNode1 = 01 = 1objectql-strategy.filterNodeToCondition{$not: {}}null(无约束)objectql-strategy.renderFilterNodeSql(回显)1 = 01 = 1filter-normalizer.collectFilterLeaves[][]1 = 0是仓里两侧已有的拼法(read-scope-sql的FALSE_CLAUSE、driver-sql 的applyFalseConstant,#5134),不绑值、每种方言都合法;引擎路径的{$not: {}}是 driver-sql / formula / driver-memory 参考匹配器早已钉住的零行写法,没有另造第二种。params 绑定错位隐患 —— 三个编译器逐个核查
结论:改前三个都不会发生;但本次新增的「TRUE 吸收 OR」规则会引入它,所以两个 SQL 编译器都按 #5297 的修法处理了。
null的分支都在 push 任何值之前就决定了 ——buildFilterClause/buildFilterClauseSql的空值判断全在函数开头,not的inner === null由归纳法保证内层没 push 过,and/or的parts.length === 0同理。被.filter(Boolean)丢掉的兄弟子句一定是零绑定的。{$or: [{stage:'won'}, {}]}里'won'已进params)。直接返回null会让它留在params里没有$n消费,把后面每个占位符错位到别人的值上。params.length,吸收时截断回去;native-sql-strategy连joins一起还原(丢弃的分支可能注册过 LEFT JOIN,留着会在 to-many 关系上放大行数)。不变量写进 TSDoc:返回null的调用必须让params与进入时逐字节相同,归纳法对每种节点成立。filterNodeToCondition不绑值(产出的是FilterCondition),无此形状 —— 但它同样有「丢分支」的 bug,已按同样的语义修。单独 pin:
binds NOTHING — the discarded branch takes its comparand with it、keeps the rest of the filter aligned when a later predicate follows(断言owner = 'u1'绑到$1而不是$2)、以及回显路径的同名两条。三个编译器各自的改动
native-sql-strategy.compileFilterNode—— 新增const分支;not的空内层从null改为1 = 0(NOT TRUE ≡ FALSE);组合子改为逐子节点循环 +paramBase/joinBase截断,or遇到null子节点整组返回null。objectql-strategy.filterNodeToCondition—— 新增const分支;not的空内层从null改为{$not: {}};or的.filter(Boolean)改为「有null分支即整组无约束」。objectql-strategy.renderFilterNodeSql—— 与 1 相同的三条,加paramBase截断。回显必须复现执行:一条跑成零行、却回显「没有 WHERE」的语句,会让来查「为什么这张图是空的」的人拿到一条返回全表的 SQL。与本文件算子集的差异(参照实现 vs 这里)
守卫表按
sql-driver.ts/read-scope-sql.ts抄,差异全部来自本文件自己的 emitter(「每个 guard 匹配自己的 emitter」是sql-driver.ts早就写明的不变量):$null/$exists=== true/=== false),fieldLeaves就是这么读的read-scope-sql按真值读。两者都编译成 null 谓词 → 都是 null-total → guard'none',不影响结果$betweengte+lte两个正向比较$in: []/$nin: []read-scope-sql的FALSE_CLAUSE/1 = 1对齐 → null-total → guard'none'$eq/$ne的null比较数read-scope-sql的value === null两条 armnull比较数stringifyForCube成'',{$eq: null}在这里是普通值比较。这本身是缺陷,本单不裁定,已单独记录为 #5332,并刻意不在测试里钉住它的行集$notContainsformula(NULL 满足)本次改动逼出来的两条(不是顺手扩范围)
{$in: []}/{$nin: []}改为布尔常量。 不改的话 NULL-safe 的$not会回归:{$not: {a: {$in: []}}}从「全部行」变成「只有 NULL 行」,因为被丢掉的合取项在否定里会翻转整条的答案(read-scope-sql的对应用例钉的是「全部行」)。顺带修掉一条独立的静默放宽:{stage: {$in: []}}此前画全表。{a: {}}改为拒收,按{ field: {} }(零个操作符的字段约束)在同仓有三个答案:driver-sql 组合子内 TRUE、顶层抛 INVALID_FILTER、formula/driver-memory FALSE #5240 已拍板、fix(driver-sql,driver-memory,formula)!:{ field: {} }四个后端一律拒收 —— 零个操作符的字段约束不再有三个答案 (#5240) #5327 已在 driver-sql / driver-memory / formula 落地的口径。它此前不产出任何 leaf,而「不产出」就是常量 TRUE —— 在新的吸收规则下{$or: [{a: {}}, {b: 2}]}会从b = 2放宽成全表。三个可能答案里必须选一个,跟随已有拍板而不是另造第三个。非对象的
$not操作数 /$and·$or分支元素同样改为拒收:{$not: null}此前整条消失(等于不筛),而在吸收规则下把它读成 TRUE 会放宽到全表 —— 垃圾输入的两种读法都不正当,read-scope-sql拒收同样的形状。测试
新文件
packages/services/service-analytics/src/__tests__/filter-normalizer-not-null-safe.test.ts,48 条,全部在 sql.js 上取行,不只断言 SQL 字符串:NativeSQLStrategy.execute);{$not:{}}/{}析取项 / 常量的组合与嵌套,含generateSql层的 WHERE 与params断言;$ne/$nin不被放宽这条单独钉住);filterNodeToCondition的产出经compileScopedFilterToSql执行取行,与 raw-SQL 路径逐条比对;双重加守卫幂等单独钉住;renderFilterNodeSql回显 SQL 的常量、吸收、守卫、占位符对齐;$and: []/$or: []在$not内外都仍然抛non-empty array;{ field: {} }(零个操作符的字段约束)在同仓有三个答案:driver-sql 组合子内 TRUE、顶层抛 INVALID_FILTER、formula/driver-memory FALSE #5240 拒收、非对象操作数拒收、未知算子仍然抛。反向验证(
git stash掉三个源文件后跑新用例):Tests 36 failed | 11 passed (47),五条实测行的失败原文:tsc --noEmit:7 条 error,与改动前git stash后的基线逐条相同(全部在既有测试文件里,与本次无关)。eslint packages/services/service-analytics/src --no-inline-config:无输出。turbo run build --filter=@objectstack/service-analytics:成功。本地跳过/CI 才跑的盲区:无。
grep -rn "skipIf\|describe.skip\|it.skip\|test.skip\|todo("在service-analytics/src下零命中,44 个测试文件全部在本地真实执行。消费面静态清点:grep -rn "NormalizedFilterNode"全仓只命中native-sql-strategy.ts、objectql-strategy.ts、filter-normalizer.ts三个文件,即本 PR 覆盖的三个编译器 +collectFilterLeaves,无第四个消费者。与 #5297 的关系
同一组缺陷的第二份拷贝,同一套修法(常量拼法、守卫下推到叶子、每个子节点先编译进自己的 bind 缓冲/可回滚),但落点不同:#5297 修的是 RLS 读作用域(越权面),本单修的是作者的 widget filter(数值面)。两份实现现在对「一个 filter 是什么意思」给出同一个答案,
filter-normalizer.ts的 TSDoc 声明的「两条 SQL 产出路径不得漂移」因此成立。明确不在范围
$and: []/$or: []空组合子)—— 独立裁定,本单只加用例把它钉在抛错这一侧。packages/spec/**——FILTER_LOGIC_CASES补 null case 归 spec 车道([spec] FILTER_LOGIC_CASES 补空组合子的布尔单位元四条 —— 需与 driver-mongodb 的单位元归约同时落地 #5239)。时序建议:本 PR 早于 [spec] FILTER_LOGIC_CASES 补空组合子的布尔单位元四条 —— 需与 driver-mongodb 的单位元归约同时落地 #5239 落地,否则native-sql-filter-logic-conformance.test.ts会因本文件未修而单独变红。read-scope-sql.ts、packages/plugins/**、packages/formula/**—— 一行未改,只读作参照。content/docs/releases/**—— 一行未改;变更记录只走.changeset/*.md。范围外发现(已单独立 issue,unassigned)
{field: {$eq: null}}/{$ne: null}编译成col = ''/col != '',与同文件里{field: null}的IS NULL自相矛盾 #5332 ——{field: {$eq: null}}/{$ne: null}在本文件编译成col = ''/col != ''(stringifyForCube(null)→''),与同文件{field: null}的IS NULL自相矛盾,也与read-scope-sql/ driver-sql 分叉。本 PR 的守卫表因此刻意不含那两条 arm,并注明由该单裁定;测试也刻意不钉它的行集。/analytics/sql回显的 SQL 丢掉$startsWith/$endsWith谓词:回显比实际执行的查询更宽,无法复现结果 #5333 ——/analytics/sql回显的 SQL 丢掉$startsWith/$endsWith谓词(SCALAR_SQL_OPS缺这两个,落到return null),回显比实际执行更宽,无法复现结果。where为数组(FilterArray 糖)时被normalizeAnalyticsFilterTree静默丢弃,图表画全表 #5334 ——normalizeAnalyticsFilterTree对数组where(FilterArray 糖)return null静默丢弃 → 画全表。fix(objectql,driver-sql,driver-memory,driver-mongodb)!:FilterArray在 engine 门下沉,四驱动数组方言删除 (#5158 拍板 C 第 2 步) #5329([engine] driver-sql 编译 spec 未声明的「数组 where 方言」—— 与 Turso remote 的拒收分叉,需一次定调(接纳进 spec 或响亮弃用) #5158 拍板 C 第 2 步)刚把 FilterArray 在 engine 六入口下沉、四驱动改拒收,analytics 是漏掉的第五道门。🤖 Generated with Claude Code
https://claude.ai/code/session_01Pbu27iNUfQCHeuS551Rqo7
Generated by Claude Code