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-closedread-scope-sql.ts:244default 直接抛
driver-memory 把 $regex 当真正则求值memory-matcher.ts:209,`new RegExp(target, condition.$options
driver-sql 把 $regex 编译成子串 LIKEsql-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

vercelBot commented Aug 5, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 5, 2026 6:43pm

Request Review

@github-actions

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 queueAug 5, 2026
Merged via the queue into main with commit c113690Aug 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-zhuang@claude