Skip to content

fix(objectql): HAVING 的 $nin / $notContains 对无值行 NULL-safe (#5905) - #6446

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-5905-having-filter-null-safe
Aug 7, 2026
Merged

fix(objectql): HAVING 的 $nin / $notContains 对无值行 NULL-safe (#5905)#6446
baozhoutao merged 2 commits into
mainfrom
claude/issue-5905-having-filter-null-safe

Conversation

@baozhoutao

@baozhoutao baozhoutao commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Fixes #5905

把 HAVING 求值面对齐到 #5298 已生效的裁决(方案 A:非否定路径对无值行 NULL-safe)。restore-invariant,不重开语义取舍。

#5298 的裁决由 PR #5962(main 07f1822)落到了 driver-sql / formula / service-analytics 与 FILTER_LOGIC_* 一致性表,但那次清点的 20 个文件里没有 packages/objectql/src/having-filter.ts —— 它是同一套算子词表的第五个求值面,于是成为唯一仍与已生效裁决相反的一面,而且是唯一没有任何一致性表覆盖的一面。

一、前提复核(动手前逐条实测)

# 前提 实测 证据
P1 having-filter.ts 两处仍是分诊记录的形状 ✅ 成立 :112 豁免名单实为 $exists / $ne / $null(无 $nin、无 $notContains);:137 case '$notContains': if (typeof value !== 'string' || value.includes(target)) return false;
P2 #5962 的答案 = 无值行成立,且没带上 having-filter ✅ 成立 formula matches-filter.ts:$ninArray.isArray(v) && !v.some(…)$notContains!(typeof actual === 'string' && …);driver-sql 极性表 case '$nin': return true; / case '$notContains': return true;git show --name-only 07f1822 的 20 个文件不含 having-filter.ts
P3 FILTER_LOGIC_CASES 今天不驱动 HAVING 路径 ✅ 成立 grep -rn FILTER_LOGIC packages/objectql/ = 0 命中;applyHaving / matchesHaving 的消费者只有 engine.ts:6751,6760 与本文件自己的测试

基线 origin/main be87153a4(复核时)。

二、改动前后真值表(两格 × NULLED / MISSING)

NULLED = 键在、值为 null;MISSING = 键不在(聚合行读作 undefined)。

单元 before after 谁在管这一格
$nin × NULLED true true(不变) 早退守卫只判 === undefined,不拦 null;[…].includes(null) 假 ⇒ 改前就已 NULL-safe
$nin × MISSING false true 早退守卫先返回 false,$nin 分支本来会答 true 却从未被走到
$notContains × NULLED false true 算子分支:typeof null !== 'string' ⇒ 判否
$notContains × MISSING false true 早退守卫先拦(守卫与分支两处都要改这一格才成立)
两算子 × 有值行(命中 / 不命中) 逐条不变 见测试对照组

⚠️ 诚实记录:分诊评论把「无值」笼统写成一格,实测是 $nin × NULLED 改前就已经成立 —— 四格里只有三格发生翻转。没有为了凑「before 全 false」的模板而改写读数。

三、改法(只动这两格)

  1. 早退守卫豁免名单$nin / $notContains,并抽成具名常量 NO_VALUE_ANSWERED_BY_OPERATOR,把「哪些算子自己回答无值」写在一处;
  2. $notContains 分支改为 formula 读法:if (typeof value === 'string' && value.includes(target)) return false; —— 它是 $contains镜像而不是「取反的副本」:非字符串/无值的列不可能包含子串,故「不包含」成立。

未动的格子:$exists / $null / $eq 等未被裁决的算子;以及 $notContains比较数类型(formula 额外要求 typeof v === 'string',本 PR 不引入 —— 那一格不在裁决范围内)。

第三种被顺带影响的形状,如实记下:$notContains 改成镜像读法后,值为非字符串且非空的列(例如把 $notContains 写在数值 measure 上)也从「判否」变为「成立」。这不是本 PR 额外做的取舍,而是 formula 读法本身的形状(!(typeof actual === 'string' && …)),两面因此逐格一致;并且该形状在类型化的编写面上本就不可达 —— filter.zod.ts:475$notContains 声明为 T[K] extends string ? string : never

driver-memory / driver-mongodb 仍是旧答案,因为 #5499 冻结了它们;本文件的分叉是相对一个被冻结的面,不是相对裁决 —— 这一点写进了文件头。

四、反向验证(方向先写死,再跑)

预测记录在动手前,逐用例细化也在跑之前写死。两次回退逐条命中,无偏差:

回退 预测转红 实测转红 预测保持绿 实测保持绿
A 豁免名单去掉 $nin/$notContains(保留 $notContains 新读法) 5 5 18 18
B $notContains 还原 typeof value !== 'string' || …(保留豁免名单) 4 4 19 19

回退 A 实测红名单(与预测同名同序):

× a MISSING column satisfies $nin
× applyHaving keeps both no-value rows and drops only the listed value
× a MISSING column satisfies $notContains
× applyHaving keeps both no-value rows and drops only the containing value
× $not inverts the leaf answer instead of re-applying the guard
Tests  5 failed | 18 passed (23)

回退 B 实测红名单:

× a NULLED column satisfies $notContains
× a MISSING column satisfies $notContains
× applyHaving keeps both no-value rows and drops only the containing value
× $not inverts the leaf answer instead of re-applying the guard
Tests  4 failed | 19 passed (23)

两种回退下对照组全绿:$nin / $notContains 的四条有值用例、$in / $contains 的无值拒绝对照、$ne(#5905 之前就已豁免)三条。这组对照正是防「豁免名单开太宽」的那道闸 —— 把正向算子也放进名单会让它们转绿。

五、命令输出

$ pnpm --filter @objectstack/objectql exec vitest run src/having-filter.test.ts
 Test Files  1 passed (1)
      Tests  23 passed (23)          # 9 原有 + 14 新增

$ pnpm --workspace-concurrency=2 --filter @objectstack/objectql test
 Test Files  141 passed (141)
      Tests  2350 passed (2350)

$ pnpm --workspace-concurrency=2 --filter @objectstack/objectql typecheck
> tsc --noEmit                        # exit 0,无输出

$ node scripts/check-nul-bytes.mjs
check-nul-bytes: OK (scanned 6078 tracked text file(s); … no raw ASCII control bytes)

check:type-check-debt(objectql TEST_DEBT 棘轮,冻结 355):

ℹ @objectstack/objectql: TEST_DEBT records 355, tsc now reports 351 (-4) -- the entry can be lowered.

新增测试代码引入 tsc 错误 0 条 —— 复算了该 ledger 的测试面测量(临时 tsconfig 解除 *.test.ts 排除)得 351 条,其中落在 having-filter.ts / having-filter.test.ts 的为 0未抬账

该 gate 另报 15 条 ledger 上飘(cloud-connection / hono / runtime / plugin-approvals 等),与本 PR 无关且非本 PR 造成:把工作树两个文件 git checkout origin/main -- 还原成纯净 origin/main 后重跑同一 gate,得到同样的 15 条、同样的增量(objectql 同样读 351)。这是未全量 build 的工作树上的既有读数,不是本 PR 的回归。

claude added 2 commits August 7, 2026 20:08
#5298 的方案 A 裁决(「列没有值」满足「不是这个值」的测试)由 PR #5962 落到了
driver-sql / formula / service-analytics 与 `FILTER_LOGIC_*` 一致性表,但那次
清点里没有 `packages/objectql/src/having-filter.ts` —— HAVING 是同一套算子词表
的第五个求值面,于是成为唯一仍与已生效裁决相反的一面,而且是唯一没有任何
一致性表覆盖的一面(`FILTER_LOGIC_CASES` 不驱动 HAVING 路径,已实测:
packages/objectql 里零处引用)。

本 PR 只对齐被裁决的两格,不重开语义取舍:

- 早退守卫的豁免名单补上 `$nin` / `$notContains`,并抽成具名常量
  `NO_VALUE_ANSWERED_BY_OPERATOR` 把「哪些算子自己回答无值」写在一处。
  此前守卫先于算子分支返回 false,`$nin` 分支本来会答 true 却从未被走到。
- `$notContains` 改为 formula 的读法(`matches-filter.ts`:
  `!(typeof actual === 'string' && …)`),即它是 `$contains` 的镜像而不是
  「取反的副本」—— 非字符串/无值的列不可能包含子串,故成立。driver-sql 的
  极性表对同一算子早已如此(`case '$notContains': return true`)。

⛔ 未动的格子:`$exists` / `$null` / `$eq` 及 `$notContains` 的比较数类型(formula
额外要求 `typeof v === 'string'`,本 PR 不引入 —— 那一格未被裁决)。

真值表(两格 × 无值两形):`$nin` × NULLED 改前就已成立(守卫只拦 `undefined`),
其余三格 false → true;有值行逐条不变。

driver-memory / driver-mongodb 仍是旧答案,因为 #5499 冻结了它们 —— 本文件的
分叉是相对一个被冻结的面,不是相对裁决。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We
@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 7, 2026 8:16pm

Request Review

@github-actions github-actions Bot added the size/m label Aug 7, 2026
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql.

14 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • content/docs/kernel/runtime-services/examples.mdx (via packages/objectql)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/objectql)
  • content/docs/kernel/services.mdx (via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql)
  • content/docs/plugins/index.mdx (via @objectstack/objectql)
  • content/docs/plugins/packages.mdx (via @objectstack/objectql)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/objectql)
  • content/docs/protocol/objectql/query-syntax.mdx (via packages/objectql)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql)
  • content/docs/releases/implementation-status.mdx (via @objectstack/objectql)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

ACCEPT(engine-core 席 #6019,会话 session_019Q7oc7ASjh8yxyS3Yz78We,第 15 轮验收):

CI 0 红(head 085304efe)。转 ready + auto-merge,合入即 Fixes #5905 自动收官。


Generated by Claude Code

@baozhoutao
baozhoutao marked this pull request as ready for review August 7, 2026 20:31
@baozhoutao
baozhoutao added this pull request to the merge queue Aug 7, 2026
Merged via the queue into main with commit f09a2e7 Aug 7, 2026
26 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-5905-having-filter-null-safe branch August 7, 2026 20:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

objectql having-filter.ts 是「无值字段」语义的第五个求值面,且带着 #5299 同款的早退守卫($nin / $notContains 不在豁免名单)

2 participants