Skip to content

fix(analytics): $ne / $nin / $notContains 在 Cube 面保留无值行 (#5298 第二批) (#5977) - #6004

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-5977-filter-normalizer-null-safe
Aug 6, 2026
Merged

fix(analytics): $ne / $nin / $notContains 在 Cube 面保留无值行 (#5298 第二批) (#5977)#6004
os-zhuang merged 1 commit into
mainfrom
claude/issue-5977-filter-normalizer-null-safe

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #5977

#5298 裁决的第二批(第一批 PR #5962 已 MERGED:driver-sql 五出口 + read-scope-sql + formula)。方向按「四项确认落档」④:非否定路径 $ne / $nin / $notContains 包含 NULL 行

一、先实测(派发词第 1 条):前提成立,但发射点与 issue 正文的指认不同

用真实消费端驱动(NativeSQLStrategy 端到端跑 sql.js;ObjectQL 路径在 executeAggregate 缝上捕获引擎 filter 并真实执行),fixture 为本包既有的 filter-normalizer-not-null-safe.test.ts 那张表(行 3/4 的 stage 为 NULL)。{stage: {$ne: 'won'}} 改动前实测:

编译器 改前 改后
NativeSQLStrategy 原生 SQL 2 2,3,4
ObjectQLStrategy 回显 SQL(/analytics/sql) 2 2,3,4
ObjectQLStrategy → 引擎 condition 2,3,4 2,3,4

改前 WHERE 逐字为 stage != $1 / stage NOT IN ($1) / stage NOT LIKE $1 ESCAPE $2 —— 三条在 SQL 三值逻辑下对 NULL 列都是 UNKNOWN,WHERE 于是丢掉正是 JS 家族会返回的行。与裁决方向分叉,实锤,所以本单不是零改动单。

但正文对落点的指认需要修正(rule 6:issue body is a lead, not a spec):正文说的 filter-normalizer.ts:267-275MONGO_TO_CUBE_OP 这张算子改名表,它本身不决定 NULL 行为;真正的发射器是 native-sql-strategy.tsbuildFilterClauseobjectql-strategy.tsbuildFilterClauseSql。成本清单 §1.C 把 Cube 面记成一个发射点,实际是两个

第三列也值得单独读:引擎那列改前就是对的,但不是因为 analytics 层做对了,而是因为 #5962driver-sql 自己加了守卫。也就是说,一条 widget filter 画出哪些行,取决于下游哪个编译器先接住这枚叶子。回显那列最糟——它描述的语句比真正跑的语句更窄,正是 #5333 反过来的失败模式。

二、改法:守卫做成树结构,而不是某一个 strategy 里的 SQL 技巧

fieldLeaves 现在把叶子发成 or(notSet, 比较)。这样这棵树的三个编译器产出同一个谓词,没有一个需要知道这条规则;也因此没有触碰 ⛔ 的 objectql-strategy.ts,回显路径是白拿的。

这正是本文件里 #5146 $not 改写已经采用的同一笔交易(header 里写着「the rewrite lives HERE rather than in native-sql-strategy on purpose」),包括它的代价:引擎路径会守卫两次,而这是幂等的(c IS NULL OR (c IS NULL OR c <> v))。

哪些算子被守卫不是新列表——复用既有极性对 nullValueSatisfiesOperator / operatorIsNullTotal,守卫恰好落在它们的 allowNull 判定上(即 nullGuardForFieldSpec 对单算子的问法)。这两个 arm 读的是比较数而非算子名,所以:

  • $ne: null 仍编译成 set(IS NOT NULL),是 total,绝不能被拓宽 —— 硬编码三个算子名字在这里会出错;
  • $nin 仍是 TRUE 常量;
  • 正向比较($eq / $in / $contains / 序关系族)逐字节不变;
  • {$not: {stage: {$ne: 'won'}}} 仍然是「stage 就是 won」,没有被拓宽(极性per算子,这条有专门用例)。

三、反向验证:方向如预判为 RED(非计数门反转)

本改动是直白的谓词变更(不是 PR #5046 那种喂给计数的 alias 肢),所以预判「拆掉守卫 → 新钉子转红」,实测吻合:把守卫条件短路掉后 15 条转红,含新增 7 条 #5298 钉子全红,恢复后全绿。

派发词第 4 条预判的「native-sql harness 的 N1 格转绿」不适用:N1 至今未入 FILTER_LOGIC_CASES(见下),所以 harness 里没有 N1 这一格可转;等效证据是上面那张实测表 + 新增钉子。如实记录而非套模板。

四、fixture 三分诊(逐条判,不批量改写)

4 个文件 10 条红,逐条判定:

  • 补/改期望值:filter-operator-coverage.test.ts 三格加 d_null 行;objectql-daterange.test.ts 引擎 filter 形状(它的主题「两个操作数都活下来、后者不覆盖前者」不变且仍可见)。
  • 整体替换:objectql-contains-canonical-operator.test.ts 的替身引擎只读 filter.stage,而加守卫后条件树顶层已没有 stage —— 它于是拿到一个算子对象,循环体一次不执行,全表通过。这条断言当时是「因为什么都没产生而绿」,正是要整体替换的那一类:改成走树的求值器;算子键断言同样从 Object.keys(filter.stage) 换成树遍历。
  • 翻面:filter-normalizer-not-null-safe.test.ts 里那行把此分叉记作「real and out of $not 的语义在 driver-sql 与 driver-memory / formula 之间分叉:NULL 行的去留相反,$not: {} 一个是 TRUE 一个是 FALSE #5146's scope」的注释,现在正是裁决的答案。

消费半径按规则的调用面扫,不是按改动包扫:rest(依赖 service-analytics)全绿 854;cli / verify / qa-dogfood 无相关 fixture。

五、N1 入表判定(派发词第 3 条):留给 #5903

#5298 PM 裁定的「后落地者入表」规则,收尾时实测 git log origin/main 与 issue 状态:#5903 仍 open、未 MERGED(git log origin/main --grep=5903 空),本 PR 是两者中的先落地者,故 FILTER_LOGIC_CASES 一行未加,N1 如实留给 #5903

但 spec 表文档里那张实测 blocker 矩阵必须订正:它写着 Cube 面 $ne['2'],本 PR 之后这句话就成了假话。已把该行摘掉,并写明 Cube 面已对齐、driver-turso remote 成为 $ne$not 两行共同的最后一个 blocker,于是两行随 #5903 一起入表。

六、边界与验证

⛔ 全部遵守:未动 like-pattern.ts / strategies/objectql-strategy.ts(#5234 在飞)、未动任何 driver 包、未动 driver-memory/mongodb、未动 content/docs/releases/

  • pnpm --filter @objectstack/service-analytics test59 files / 1105 passed(基线 1097,新增 8 条)
  • pnpm --filter @objectstack/spec test323 files / 8288 passed
  • pnpm --filter @objectstack/rest test62 files / 854 passed
  • 两包 typecheck 绿;check-nul-bytes 绿;changeset 自带
  • 基于最新 origin/main(ffd51fd7a)rebase 后复跑

⚠️ #5234 与本单同包不同源文件、零源码重叠;其 LIKE 收紧可能与 objectql-contains-canonical-operator.test.ts 相邻,后落地者 rebase。


Generated by Claude Code

…he Cube face (#5977)

Second batch of the #5298 ruling; PR #5962 landed the first on driver-sql,
read-scope-sql and formula.

Measured before the change on this package's own fixture, for
`{stage: {$ne: 'won'}}` with rows 3-4 carrying a NULL `stage`: the raw-SQL
strategy answered `2`, the display-SQL echo answered `2`, and the ObjectQL
engine condition answered `2,3,4`. Three compilers of ONE normalized tree, two
answers — and the engine column was right only because driver-sql guards for
itself since #5962, not because the analytics layer did. The echo was the worst
of the three: it described a NARROWER query than the one that ran.

`fieldLeaves` now emits the guard as tree STRUCTURE — an `or` of the `notSet`
leaf with the comparison — rather than as a SQL trick inside one strategy, so
all three compilers produce one predicate and none of them needs to know the
rule. Same trade the #5146 `$not` rewrite in this file already took, including
its cost: the engine path guards twice, which is idempotent.

Which operators are guarded is NOT a new list. It is the existing polarity pair
`nullValueSatisfiesOperator` / `operatorIsNullTotal` — the guard applies exactly
on their `allowNull` verdict. Those arms read the COMPARAND, so `$ne: null`
stays `IS NOT NULL` and an empty `$nin` stays the TRUE constant; a hard-coded
list of three operator names would have been wrong as well as duplicated.

Positive comparisons compile byte-identically, and `{$not: {stage: {$ne: 'v'}}}`
still means "stage is v" rather than widening.

Fixture triage, each re-judged individually rather than batch re-spelled:
- `filter-operator-coverage.test.ts` — three cases gain the `d_null` row.
- `objectql-daterange.test.ts` — engine-filter shape; its subject (both operands
  survive the merge) is unchanged and still visible.
- `objectql-contains-canonical-operator.test.ts` — its stand-in engine read only
  `filter.stage`, which the guarded tree no longer has at top level, so it was
  handed an EMPTY operator object and passed every row. Replaced with a tree
  walk; the operator-key assertion got the same treatment.
- `filter-normalizer-not-null-safe.test.ts` — the line that recorded this
  divergence as "real and out of #5146's scope" is now the ruling's answer.

FILTER_LOGIC_CASES is unchanged: `$ne` / `$not` enrol in #5903's PR, the last
blocker. The spec table's measured matrix drops the Cube row it no longer
describes.

Fixes #5977

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

vercel Bot commented Aug 6, 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 6, 2026 2:22pm

Request Review

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

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/service-analytics, @objectstack/spec.

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

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/data-api.mdx (via @objectstack/service-analytics)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/service-analytics, @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/service-analytics, @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/service-analytics, @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/service-analytics, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/service-analytics, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/service-analytics, @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/service-analytics, @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

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 protocol:data tests tooling labels Aug 6, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 6, 2026 14:37
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 6, 2026
Merged via the queue into main with commit de70b42 Aug 6, 2026
25 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-5977-filter-normalizer-null-safe branch August 6, 2026 14:55
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 protocol:data size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[#5298 第二批] filter-normalizer(Cube 面)NULL-safe 对齐:先实测既有 NULL 行为,分叉则改写;N1 入表按后落地者规则

2 participants