Skip to content

fix(service-analytics): contains 以规范算子 $contains 送进引擎,比较值不再落进正则位置 (#5557) - #5566

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-5557-contains-canonical-operator
Aug 5, 2026
Merged

fix(service-analytics): contains 以规范算子 $contains 送进引擎,比较值不再落进正则位置 (#5557)#5566
os-zhuang merged 2 commits into
mainfrom
claude/issue-5557-contains-canonical-operator

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #5557

前提核对(对 origin/main @ 9ecdca9,含刚合并的 PR #5558)

issue 的前提成立,行号已漂移到 convertFilter 的第 1018 行附近。三条论证逐条实测:

issue 的论断 核对结果
case 'contains': return { $regex: values[0] }; ✅ 在 objectql-strategy.ts:1018,三个同族算子确为规范算子
$regex 不在 filter.zod.tsFILTER_OPERATORS ✅ 15 个算子(filter.zod.ts:957),无 $regex
同包 read-scope-sql.tscompileOperator$regex fail-closed read-scope-sql.ts:244default 直接抛
driver-memory 把 $regex 当真正则求值 memory-matcher.ts:209,`new RegExp(target, condition.$options
driver-sql 把 $regex 编译成子串 LIKE sql-driver.ts:6495,与 $contains 共用 applyContainsLike

改动

一行(objectql-strategy.ts):

case 'contains': return { $contains: values[0] };

filter-normalizer.tsMONGO_TO_CUBE_OP 只把 $contains 映到 contains,别无来源,所以这里回送 $contains 就是作者自己那个 key 的往返 —— 与三个同族算子完全一致。

⛔ 按分诊要求未碰 filter-normalizer.ts(#5526-B 排下一位)。

测试

新增 packages/services/service-analytics/src/__tests__/objectql-contains-canonical-operator.test.ts。issue 明确指出「只断言 SQL/filter 字符串会漏掉转义这一半」,所以两半都断言:

  1. 引擎 filter 的算子键逐个对 filter.zod.tsALL_OPERATORS 校验 —— 取自 spec 而非手抄一份(filter-refusal.ts 对它自己那份词表说过同样的话:手抄的副本只在打字那天与契约一致)。
  2. 行结果跑在一个复刻 memory-matcher.ts 各 arm 的求值面上(镜像而非 import:service-analytics 不依赖任何驱动)。元字符用例:
  3. 同包另一个 FilterCondition 消费方:四个 LIKE 算子的产出全部送进 compileScopedFilterToSql 确认编译得过(修前 $contains 那条抛 fail-closed)。

50% (+) 刻意用在第 3 项上:那个编译器的 LIKE 元字符转义是它自己的另一个缺陷(已另行归档,见下),从本文件断言它等于替别人的 bug 钉桩。

反向验证(方向修前预判,结果与预判一致:修前红 / 修后绿)

修前(新测试文件,case 'contains' 仍为 $regex):6 个断言红

FAIL  ... > the issue's measured table > `$contains` is forwarded as `$contains` — was the undeclared `$regex`
FAIL  ... > the issue's measured table > every operator key it emits is one the spec DECLARES
      AssertionError: $contains emitted undeclared operator "$regex": expected false to be true
FAIL  ... > a `.` in the comparand matches a literal dot, not any character
      AssertionError: expected [ '1', '2' ] to deeply equal [ '1' ]
FAIL  ... > a comparand that is not a valid regex still finds its rows
      AssertionError: expected [] to deeply equal [ '3' ]
FAIL  ... > compiles every LIKE-family filter the strategy produces — `$regex` threw
      "Error: [read-scope-sql] unsupported operator \"$regex\" on \"stage\" (fail-closed)."
FAIL  ... > and returns the same row the regex-evaluating face returns

修后:Test Files 50 passed (50) / Tests 825 passed (825)

#5558 留下的翻译已删除 —— 本修复最直接的反向证据

PR #5558objectql-echo-operator-coverage.test.ts 的替身引擎里留了一个 asCondition(),把 $regex 翻回 $contains(注释指向本单)。它存在的理由现在没有了,已删除,options.filter 原样送进 compileScopedFilterToSql

于是把 case 'contains' 退回 $regex 之后,该文件也红 —— 7 个断言、2 个文件:

FAIL  objectql-echo-operator-coverage.test.ts > [#5333] ... > running the echo returns the rows the query returns, not more
Error: [read-scope-sql] unsupported operator "$regex" on "stage" (fail-closed).
 ❯ compileOperator src/read-scope-sql.ts:244:13
 ❯ Object.executeAggregate src/__tests__/objectql-echo-operator-coverage.test.ts:177:33
 ❯ ObjectQLStrategy.execute src/strategies/objectql-strategy.ts:152:28

Test Files  2 failed | 48 passed (50)
      Tests  7 failed | 818 passed (825)

恢复修复后回到 50/50 全绿。

消费半径清扫

service-analytics 的下游是 cli / rest / verify / qa-dogfood。逐个 grep 过它们的 fixture:

  • packages/rest/src/analytics-filter-refusal-envelope.test.ts:292$regex字面错误消息字符串(mockRejectedValue),不经 convertFilter,且 read-scope-sql.ts 的 fail-closed 行为本次未改 —— 不受影响;
  • packages/rest/src/export-integration.test.ts 的替身驱动已经理解 $contains($search 折叠出的形状),本改动只让它更对;
  • 其余无耦合。

pnpm --filter @objectstack/rest test:Test Files 48 passed (48) / Tests 719 passed (719)

typecheck

service-analyticsscripts/check-type-check-coverage.mjs 里是 measured DEBT 条目(无 typecheck script),所以直接跑了它自己的 tsc --noEmit -p tsconfig.json:7 条报错,全部落在 analytics-service.test.ts / measure-source-field-gate.test.ts / objectql-timedimension-projection.test.ts —— 本 PR 三个文件一条都没有,均为先存在的债(与 ledger 记的 3 条有漂移,那是 main 上的既有情况,不是本改动引入)。

node scripts/check-nul-bytes.mjs:OK (scanned 5509 tracked text file(s));本 PR 四个文件另做了 grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f]' 自扫,干净。

范围外发现

$contains / $startsWith / $endsWith 的比较值在 analytics 侧的三个 SQL 编译器里都没有做 LIKE 元字符转义(read-scope-sql.tscompileOperatornative-sql-strategy.tslikePatternobjectql-strategy.tsLIKE_SQL_OPS),而 driver-sqlapplyLike 是显式 ESCAPE '\' 的、并在注释里把 % 旁路称作 P0。已按 Prime Directive #10 另行归档(unassigned),不在本 PR 修。

关联:#5333 / PR #5558(同文件回显侧)、#4128(同函数上次同类修复)、#4706($regex 语义待裁 —— 本改动让 $regex 在 service-analytics 失去生产方,反而缩小该裁决的影响面)。

🤖 Generated with Claude Code

https://claude.ai/code/session_01BWS4heBoAitLmzCLhcYdbK


Generated by Claude Code

… not an unescaped `$regex` (#5557)

`ObjectQLStrategy.convertFilter` handles the four LIKE-family operators in one
`switch`. Three of them — `notContains` / `startsWith` / `endsWith` — have passed
through as canonical spec operators since #4128; `contains` alone was
`{ $regex: values[0] }`, dropping the comparand VERBATIM into a regex position.

Measured, engine filter per `where`:

    {stage: {$contains:    'a.b'}}  =>  {stage: {$regex:       'a.b'}}
    {stage: {$notContains: 'a.b'}}  =>  {stage: {$notContains: 'a.b'}}
    {stage: {$startsWith:  'a.b'}}  =>  {stage: {$startsWith:  'a.b'}}
    {stage: {$endsWith:    'a.b'}}  =>  {stage: {$endsWith:    'a.b'}}

Three consequences, none of which waits on #4706's ruling about `$regex`:

1. `$regex` is not in `filter.zod.ts`'s `FILTER_OPERATORS` (fifteen operators,
   none of them `$regex`), so this was a PRODUCER emitting an operator the
   contract does not declare — Prime Directive #12 fixes the producer.
2. `compileScopedFilterToSql` in this same package is a `FilterCondition`
   consumer and fails closed on `$regex`, so one filter tree no longer travelled
   between two consumers of the same contract in the same directory.
3. A backend that reads `$regex` as a real regex — driver-memory's
   `memory-matcher.ts`, deliberately, for plugin-auth's adapter — read `a.b` as
   "a, any character, b" and could not compile `50% (+)` at all, so its
   `catch { return false }` answered zero rows for a filter that has a match.
   `driver-sql` compiles the same `$regex` to a substring LIKE: one widget, two
   row sets, depending on the driver.

`MONGO_TO_CUBE_OP` maps `$contains` -> `contains` and nothing else does, so
returning `$contains` is the round trip of the author's own key.

`objectql-contains-canonical-operator.test.ts` pins both halves, because either
alone misses the defect: the emitted operator keys are checked against the spec's
own `ALL_OPERATORS` rather than a hand-written copy, and the ROW IDS are taken
through an evaluator that mirrors `memory-matcher.ts`'s arms — the only face on
which "the comparand is not escaped" is visible at all.

Also drops the `$regex` -> `$contains` translation PR #5558 left in
`objectql-echo-operator-coverage.test.ts`'s stand-in engine: its reason to exist
was this issue. That file is now the most direct reverse evidence — reverting
`case 'contains'` turns its `$contains` row red with the fail-closed refusal from
consequence 2 (measured: 7 failures across 2 files reverted, 0 restored).

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

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

Request Review

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

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-analytics.

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

  • content/docs/api/data-api.mdx (via @objectstack/service-analytics)
  • content/docs/api/index.mdx (via @objectstack/service-analytics)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/service-analytics)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/service-analytics)
  • content/docs/plugins/packages.mdx (via @objectstack/service-analytics)
  • content/docs/releases/implementation-status.mdx (via @objectstack/service-analytics)
  • content/docs/releases/v17.mdx (via @objectstack/service-analytics)
  • content/docs/releases/v9.mdx (via @objectstack/service-analytics)

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.

…ately does not pin

The comment said "filed separately" with no number, which is the kind of
dangling reference the next reader cannot follow. #5567 is the analytics SQL
compilers interpolating a LIKE comparand without escaping `%` / `_`; the decoy
fixture row also now says why it earns its place on the regex face (the exact
row set) rather than through a wildcard assertion that face cannot make.

Comments only — 50 files / 825 tests still pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BWS4heBoAitLmzCLhcYdbK
@os-zhuang
os-zhuang marked this pull request as ready for review August 5, 2026 19:05
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 5, 2026
Merged via the queue into main with commit c113690 Aug 5, 2026
24 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-5557-contains-canonical-operator branch August 5, 2026 19:17
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

2 participants