fix(objectql): HAVING 的 $nin / $notContains 对无值行 NULL-safe (#5905) - #6446
Merged
Conversation
#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
…ing-filter-null-safe
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 14 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
Contributor
Author
|
ACCEPT(engine-core 席 #6019,会话
CI 0 红(head Generated by Claude Code |
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 #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—— 它是同一套算子词表的第五个求值面,于是成为唯一仍与已生效裁决相反的一面,而且是唯一没有任何一致性表覆盖的一面。一、前提复核(动手前逐条实测)
having-filter.ts两处仍是分诊记录的形状:112豁免名单实为$exists/$ne/$null(无$nin、无$notContains);:137case '$notContains': if (typeof value !== 'string' || value.includes(target)) return false;matches-filter.ts:$nin→Array.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.tsFILTER_LOGIC_CASES今天不驱动 HAVING 路径grep -rn FILTER_LOGIC packages/objectql/= 0 命中;applyHaving/matchesHaving的消费者只有engine.ts:6751,6760与本文件自己的测试基线
origin/mainbe87153a4(复核时)。二、改动前后真值表(两格 × NULLED / MISSING)
NULLED = 键在、值为
null;MISSING = 键不在(聚合行读作undefined)。$nin× NULLED=== undefined,不拦null;[…].includes(null)假 ⇒ 改前就已 NULL-safe$nin× MISSING$nin分支本来会答 true 却从未被走到$notContains× NULLEDtypeof null !== 'string'⇒ 判否$notContains× MISSING三、改法(只动这两格)
$nin/$notContains,并抽成具名常量NO_VALUE_ANSWERED_BY_OPERATOR,把「哪些算子自己回答无值」写在一处;$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 冻结了它们;本文件的分叉是相对一个被冻结的面,不是相对裁决 —— 这一点写进了文件头。
四、反向验证(方向先写死,再跑)
预测记录在动手前,逐用例细化也在跑之前写死。两次回退逐条命中,无偏差:
$nin/$notContains(保留$notContains新读法)$notContains还原typeof value !== 'string' || …(保留豁免名单)回退 A 实测红名单(与预测同名同序):
回退 B 实测红名单:
两种回退下对照组全绿:
$nin/$notContains的四条有值用例、$in/$contains的无值拒绝对照、$ne(#5905 之前就已豁免)三条。这组对照正是防「豁免名单开太宽」的那道闸 —— 把正向算子也放进名单会让它们转绿。五、命令输出
check:type-check-debt(objectql TEST_DEBT 棘轮,冻结 355):新增测试代码引入 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 的回归。