Skip to content

fix(service-analytics): /analytics/sql 回显补上 $startsWith / $endsWith 谓词 (#5333) - #5558

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-5333-echo-startswith-endswith
Aug 5, 2026
Merged

fix(service-analytics): /analytics/sql 回显补上 $startsWith / $endsWith 谓词 (#5333)#5558
os-zhuang merged 1 commit into
mainfrom
claude/issue-5333-echo-startswith-endswith

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #5333

前提核实(先做的事)

issue 正文是线索,不是规格,所以先在当前 origin/main(229d29ea4)上核对:

  • buildFilterClauseSql 仍是「set/notSet/in/notIn/contains/notContains 显式分支 + 落到 SCALAR_SQL_OPS(只有 equals/notEquals/gt/gte/lt/lte)查表,查不到即 return null」;startsWith/endsWith 两处都不在。
  • NativeSQLStrategy.buildFilterClauselikePattern 表(约 666 行)已有两条:startsWith 的 pattern 是 `${v}%`,endsWith 的是 `%${v}`
  • 实测(把新用例跑在未修改的代码上):{stage: {$startsWith: 'w'}} 的回显是 SELECT id AS "id", COUNT(*) AS "total" FROM "deal" GROUP BY id —— 没有 WHERE,params 为空。issue 那张对照表逐行成立,9 条断言红。

前提有效。

主修:LIKE 家族收进一张表

新增 LIKE_SQL_OPS,四个算子的 SQL 拼写与 pattern 并排:

where 实际执行(NativeSQLStrategy) 修复前回显 修复后回显
{stage: {$startsWith: 'w'}} WHERE stage LIKE $1 / ['w%'] 没有 WHERE,params 为空 WHERE stage LIKE $1 / ['w%']
{stage: {$endsWith: 'n'}} WHERE stage LIKE $1 / ['%n'] 没有 WHERE,params 为空 WHERE stage LIKE $1 / ['%n']
{stage: {$contains: 'w'}} WHERE stage LIKE $1 WHERE stage LIKE $1 不变(产物逐字节相同)

NativeSQLStrategyopMap / likePattern 一一对应 —— 回显描述的正是那个编译器产出的语句,两张表并列摆着,漂移才看得见。

回显比执行更宽是这个字符串最不能有的毛病:它存在的唯一理由就是复现执行(文件自己在渲染块顶上写着 “a rendering that contradicts execution is worse than no rendering”)。作者带着「为什么这张图少了几行」来看回显,拿到一条没有该筛选条件的语句,跑一遍返回更多行,于是结论是「筛选器没生效」—— 而实际执行是生效的。不涉及越权或错行:该字符串从不执行(execute() 的 echo 丢弃 params),损害限于可调试性。

次修:return null 改 THROW —— 前提已验证

PM 预批的方向,前提是「算子词汇表确实封闭」。已验证,证据:

  • filter-normalizer.tsfieldLeaves 是叶节点的唯一生产者(全仓 grep kind: 'leaf' 只有它一处非测试命中,objectql-strategy.ts 自己不造叶)。
  • 它对 MONGO_TO_CUBE_OP 之外的算子在建叶之前就以 invalidFilterError(INVALID_FILTER / 400)拒绝。
  • 因此能到达回显端的叶算子集合有限可枚举,共 14 个:MONGO_TO_CUBE_OP 的 12 个 + set / notSet($null / $exists / 裸 null / $eq: null / $ne: null 都归到这两个);$between 降解成 gte / lte,已在 12 个里。

所以任何调用方写出的过滤器都到不了这个出口;真到了,只能是 normalizer 的表新增了这里没有分支的算子 —— 我们自己两张表漂移,而对此唯一不能给的答案就是悄悄放宽作者的查询。与 convertFilterdefault: 分支在 #4128 做出的是同一个选择。刻意invalidFilterError 的 400 信封:这不是调用方形状的错误,是我们自己的 bug。

values.length === 0 那条 return null 原样保留 —— 它表达的是「这个叶没有谓词」,与 execute() / NativeSQLStrategy 一致,和「渲染不了」是两件事。函数的 doc 注释也据此改写。

反向验证 —— 方向先预测,再跑

两项修改各自独立验证,方向不同,如实报告:

  1. 主修(常规红→绿):未修改的代码上跑新用例 → 9 红,$startsWith 回显为 SELECT … GROUP BY id(无 WHERE)。修完 → 817/817 绿。
  2. 次修(只动一条断言,枚举断言不动):保留第 1 项、只把 throw 改回 return null恰好 1 条断言红(throws for an operator with no SQL spelling),枚举断言与回显对照表全部保持绿色

第 2 项的方向是预测好的,也是这次要如实说的一点:补上 startsWith/endsWith 之后,词汇表内 14 个算子全部有分支,那个出口今天从公共入口不可达,所以改回 return null 不可能让枚举断言变红。它是一个漂移探针,不是行为修复 —— 行为修复是第 1 项。把它写成「改回去就全红」会是一个符合模板但不成立的说法。

$contains 等既有算子无回归:产物逐字节未变,817 条用例全绿。

测试

新增 objectql-echo-operator-coverage.test.ts,两层:

  1. 行结果钉住 issue 那张表。 回显语句在同一份 sql.js fixture 上真的被执行,行 id 与查询实际返回的行 id 比对($startsWith: 'w'['1'],$endsWith: 'n'['1'],$contains: 'o'['1','2']),外加「回显不得返回整份 fixture」的兜底断言。只断言 SQL 字符串会放过下一个未映射的算子 —— analytics filter-normalizer:未映射的算子被静默丢弃 → 查询放宽到全表($between 已修,还剩四个) #4128$between 就藏在 $startsWith 后面。fixture 里 'won' 是唯一既以 w 开头又以 n 结尾的行,'lost'o 但两头都不沾,所以「前缀锚点被编译成子串」会表现为错的 id,而不是碰巧通过。
  2. 按封闭词汇表枚举。 直接从 spec 导入 FILTER_OPERATORS(15 个可编写算子)驱动,逐个断言回显渲染出谓词、$n placeholder 数与 params.length 对齐,并断言「执行侧有 WHERE 的地方回显也必须有」。另有一条断言表本身覆盖了 spec 声明的全部算子 —— spec 新增算子而这张表没跟上,这里先红。

命令与结果:

pnpm --workspace-concurrency=2 --filter @objectstack/service-analytics test -- --maxWorkers=2
  修前: Test Files 1 failed | 48 passed (49)   Tests 9 failed | 808 passed (817)
  修后: Test Files 49 passed (49)              Tests 817 passed (817)

tsc --noEmit -p packages/services/service-analytics/tsconfig.json
  7 errors —— 与 origin/main 基线(git stash 后同样 7)完全一致,新增文件 0 error
  (该包无 typecheck 脚本,在 scripts/check-type-check-coverage.mjs 里带实测 DEBT 条目)

pnpm --filter @objectstack/service-analytics build   → Build success
node scripts/check-nul-bytes.mjs                     → OK (5507 files)
grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f]' 本次改动的三个文件 → CLEAN

消费半径已清扫:ObjectQLStrategy.generateSql 的回显没有别的包在断言;driver-memory 的 analytics 面自带 ANALYTICS_FILTER_CAPABILITIES 能力门,对 $startsWith/$endsWith声明式的响亮拒绝(#5345),不是静默丢,不受本改动影响。

packages/spec/authorable-surface.base.json 在 spec 构建时被 gen:schema 顺手重锚了 baseRev,与本 PR 无关,已 revert,不随本 PR 进来。

范围外发现(已另行开单,未在本 PR 修)

未碰 filter-normalizer.ts(#5526 根因单待派)。


🤖 Generated with Claude Code

https://claude.ai/code/session_01BWS4heBoAitLmzCLhcYdbK

…ics/sql echo (#5333)

`ObjectQLStrategy.buildFilterClauseSql` is the third compiler of the analytics
filter tree — the one whose output is echoed to the browser. It handled
`set`/`notSet`/`in`/`notIn`/`contains`/`notContains` explicitly and sent
everything else to `SCALAR_SQL_OPS`, whose six entries do not include
`startsWith`/`endsWith`; the unmapped exit was `return null`, which every
compiler of this tree reads as "this node carries no constraint".

So `{stage: {$startsWith: 'w'}}` echoed a statement with no WHERE at all while
the query it documents ran `stage LIKE 'w%'`. The echo was strictly WIDER than
execution, and its only reason to exist is reproducing execution: an author
debugging "why does this chart show fewer rows" ran it, got more rows, and
concluded the filter never applied. Same class as #3601 / #3602 / #3650,
reached through the operator table.

Two changes:

1. `LIKE_SQL_OPS` collects the four LIKE-family operators with their SQL
   spelling and pattern, one row each, mirroring `NativeSQLStrategy`'s
   `opMap`/`likePattern` pair so the two tables sit side by side and drift is
   visible. `contains`/`notContains` output is byte-identical.

2. The unmapped exit THROWS instead of returning null. It can, because the
   upstream vocabulary is closed: `fieldLeaves` in `filter-normalizer.ts` is
   the only leaf producer and refuses an operator outside `MONGO_TO_CUBE_OP`
   with INVALID_FILTER/400 before a leaf exists. An arrival therefore means
   our own two tables drifted — same call `convertFilter`'s `default:` arm
   made in #4128 — and a silently wider query is the one answer that must not
   be given. Deliberately not the 400 envelope: this is not a caller mistake.
   Measured: this exit is unreachable through the public door today, so
   reverting it to `return null` turns exactly one assertion red and leaves
   the enumeration green. It is a drift tripwire, not the behaviour fix.

`objectql-echo-operator-coverage.test.ts` pins the issue's table by ROW IDS —
the echoed statement is executed against the same fixture and its rows compared
to what the query returns — then enumerates all 15 of `filter.zod.ts`'s
`FILTER_OPERATORS`, asserting each renders a predicate with placeholders and
`params` aligned.

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 5:55pm

Request Review

@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling and removed size/l labels 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.

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 tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

/analytics/sql 回显的 SQL 丢掉 $startsWith / $endsWith 谓词:回显比实际执行的查询更宽,无法复现结果

2 participants