Skip to content

ObjectQLStrategy.convertFilter 把 $contains 送成未声明的 $regex(比较值不转义),三个同族算子早在 #4128 已改成规范算子 —— 实测 #5557

Description

@os-zhuang

修 #5333(回显丢 $startsWith/$endsWith)时核对 objectql-strategy.ts 的算子覆盖面,发现这一条在执行侧,与 #5333 的回显侧只隔一个函数。

位置

packages/services/service-analytics/src/strategies/objectql-strategy.ts 的 convertFilter(约 1018 行):

case 'contains': return { $regex: values[0] };
// `notContains` had no arm and fell to the `default` below ... These three pass
// through as the canonical spec operators every driver implements directly, so an
// anchored match stays anchored rather than depending on regex dialect (#4128).
case 'notContains': return { $notContains: values[0] };
case 'startsWith':  return { $startsWith: values[0] };
case 'endsWith':    return { $endsWith: values[0] };

紧挨着的注释把三个同族算子改成规范算子的理由写得很清楚 ——「so an anchored match stays anchored rather than depending on regex dialect」—— 而 contains 留在了 $regex 上,同一个 switch 里的四个 LIKE 家族算子,三个走规范算子,一个走正则。

实测(ObjectQLStrategy.execute 捕获交给 executeAggregate 的 filter)

{"stage":{"$contains":"a.b"}}     => engine filter: {"stage":{"$regex":"a.b"}}
{"stage":{"$notContains":"a.b"}}  => engine filter: {"stage":{"$notContains":"a.b"}}
{"stage":{"$startsWith":"a.b"}}   => engine filter: {"stage":{"$startsWith":"a.b"}}
{"stage":{"$endsWith":"a.b"}}     => engine filter: {"stage":{"$endsWith":"a.b"}}

比较值原样放进 $regex,不转义。于是在任何把 $regex 当真正则求值的后端上:

new RegExp('a.b').test('axb') === true      // 作者要的是字面子串,拿到的是通配
new RegExp('50% (+)')         THROWS SyntaxError: Nothing to repeat

driver-memory 的 memory-matcher.ts 正是 new RegExp(target, …),并且 catch 之后 return false —— 所以带正则元字符的比较值要么多匹配(. [ |),要么静默零行(括号/量词非法时)。对照:driver-memory 自己的 analytics 面(memory-analytics.ts)对 contains 是 substring: (v) => this.driver.filterSubstringPattern(v),即用驱动自己的规则转义成字面子串;经由本 issue 这条路径进来的 $regex 绕过了那层转义。

为什么是 bug(三点,都不依赖 #4706 的裁决)

  1. $regex 不在契约里。 packages/spec/src/data/filter.zod.ts 的 FILTER_OPERATORS 是 15 个:$eq $ne $gt $gte $lt $lte $in $nin $between $contains $notContains $startsWith $endsWith $null $exists —— 没有 $regex。这里是一个生产方在向引擎发送 schema 未声明的算子;按 Prime Directive Add comprehensive test suite for Zod schema validation #12,修法在生产方,而不是让消费方各自容忍。
  2. 同包内已有消费方对它 fail-closed。 同一个包的 read-scope-sql.ts 的 compileOperator 遇到 $regex 直接抛 [read-scope-sql] unsupported operator "$regex" on "stage" (fail-closed)(/analytics/sql 回显的 SQL 丢掉 $startsWith / $endsWith 谓词:回显比实际执行的查询更宽,无法复现结果 #5333 的测试用它做 stand-in 引擎时当场撞上)。同一个 FilterCondition 在同一个包的两个消费方之间已经不通。
  3. 跨驱动结果不一致。 driver-sql 把 $regex 编译成子串 LIKE(sql-driver.ts 约 6489 行,注释还写着「$regex reaches SQL only via the better-auth adapter」—— 它不知道 analytics 是第二个生产方),而 regex 求值的后端按正则匹配。同一个 dashboard 的 $contains widget 在不同驱动上返回不同行集 —— 这正是 $regex on driver-sql is not a regex — it compiles to a substring LIKE, so it both over-matches and silently matches nothing #4706 第 3 点最想要答案的那个形状。

建议

case 'contains': return { $contains: values[0] }; —— 与它三个同族算子一致,一行。这样 $regex 在 service-analytics 里就没有生产方了,#4706 那道「$regex 到底该是什么语义」的裁决也少一个受影响的调用方(本条不等它:改成规范算子无论 #4706 怎么裁都是对的)。

需要的用例:contains 的比较值带正则元字符(a.b、50% (+))时,行结果与字面子串一致 —— 断言 SQL/filter 字符串会漏掉转义这一半。

关联:#5333(同文件、回显侧的同类算子表漂移)、#4128(convertFilter 的 default: 从「当成等值」改成抛错、三个同族算子改成规范算子的那次)、#4706($regex 语义待裁决,needs-user-decision)、#5374(driver-memory analytics 面改成整条谓词构建的那次,contains 在那边是转义过的)。

Activity

  1. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    分诊(services 车道 PM,session_01BWS4heBoAitLmzCLhcYdbK):晋级 pm:queue。

    判定:具体缺陷、已实测、用户今天可撞到($contains 带正则元字符 → 多匹配或静默零行),修法一行且三条论证均不依赖 #4706 的裁决(改成规范算子无论 #4706 怎么裁都是对的 —— 且让 $regex 在 service-analytics 失去生产方,反而缩小该裁决的影响面)。生产方修复,符合 PD #12。

    排程(同文件串行,step 3 记录):objectql-strategy.ts 上 PR #5558(#5333)在飞未合 —— 本单 Blocked-by: 它落地;落地后本单先于 #5526-B 派发(一行修复先落,根因重构后行,避免 B 的大 diff 吞掉这行修复的独立可查性)。#5526 顺延一位。

    Blocked-by: #5558


    Generated by Claude Code

  2. self-assigned this
    on Aug 5, 2026
  3. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    认领:PM 循环第 5 轮(services 车道)。Blocked-by 已解除:PR #5558(#5333)已 MERGED。
    会话:session_01BWS4heBoAitLmzCLhcYdbK
    分支:claude/issue-5557-contains-canonical-operator
    Worktree:objectstack-issue-5557
    域:domain:services
    文件面:packages/services/service-analytics/src/strategies/objectql-strategy.ts(convertFilter 的 contains arm)、同包测试、.changeset/*.md。⛔ 不碰 filter-normalizer.ts(#5526-B 排下一位)。


    Generated by Claude Code

  4. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    实现完成,Draft PR:#5566(分支 claude/issue-5557-contains-canonical-operator,会话 session_01BWS4heBoAitLmzCLhcYdbK)。

    前提核对:成立(对 origin/main @ 9ecdca9,含已合并的 PR #5558)。三条论证逐条实测过,行号漂移到 convertFilter 第 1018 行;$regex 确实不在 FILTER_OPERATORS(15 个,filter.zod.ts:957),read-scope-sql.ts:244 的 default 确实 fail-closed 抛,memory-matcher.ts:209 确实当真正则求值(而且 filter-refusal.ts 的 SUPPORTED_FIELD_OPERATORS 有意接受 $regex,所以不会被拒收)。

    **改动:**一行 case 'contains': return { $contains: values[0] }; + 新测试 + changeset(patch)。未碰 filter-normalizer.ts。

    反向验证(方向修前预判、结果一致):新测试修前 6 红 / 修后全绿;#5558 在 objectql-echo-operator-coverage.test.ts 留的 asCondition() 翻译已删除,把 case 'contains' 退回 $regex 后该文件也红(共 7 红 / 2 文件,报错正是 unsupported operator "$regex" … (fail-closed)),恢复后 50/50 全绿。

    范围外发现已按 PD #10 归档为 #5567(unassigned,未打标签待分诊):analytics 侧三个 SQL 编译器(read-scope-sql.ts / native-sql-strategy.ts / objectql-strategy.ts 的 LIKE_SQL_OPS)都不转义 LIKE 比较值 —— 实测 $contains: '_admin' 命中 xyadmin、$contains: '50%' 命中 off 5012 now,而 driver-sql 的 applyLike 自己把这条旁路标为 P0。与本单不重叠(本单裁算子选择,#5567 裁收到正确 $contains 之后怎么拼 pattern),也不在 #5526 的范围面内。


    Generated by Claude Code


    Generated by Claude Code

  5. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    复核:ACCEPT → PR #5566(CI 绿后转 ready 入合并队列)。

    已核项:

    范围外发现 #5567(LIKE 元字符不转义,read-scope 侧 = RLS 放宽)另行分诊 —— 见该单评论。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions