fix(service-analytics): /analytics/sql 回显补上 $startsWith / $endsWith 谓词 (#5333) - #5558
Merged
Merged
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 8 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
This was referenced Aug 5, 2026
os-zhuang
marked this pull request as ready for review
August 5, 2026 18:07
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.buildFilterClause的likePattern表(约 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 并排:whereNativeSQLStrategy){stage: {$startsWith: 'w'}}WHERE stage LIKE $1/['w%']params为空WHERE stage LIKE $1/['w%']{stage: {$endsWith: 'n'}}WHERE stage LIKE $1/['%n']params为空WHERE stage LIKE $1/['%n']{stage: {$contains: 'w'}}WHERE stage LIKE $1WHERE stage LIKE $1与
NativeSQLStrategy的opMap/likePattern一一对应 —— 回显描述的正是那个编译器产出的语句,两张表并列摆着,漂移才看得见。回显比执行更宽是这个字符串最不能有的毛病:它存在的唯一理由就是复现执行(文件自己在渲染块顶上写着 “a rendering that contradicts execution is worse than no rendering”)。作者带着「为什么这张图少了几行」来看回显,拿到一条没有该筛选条件的语句,跑一遍返回更多行,于是结论是「筛选器没生效」—— 而实际执行是生效的。不涉及越权或错行:该字符串从不执行(
execute()的 echo 丢弃params),损害限于可调试性。次修:
return null改 THROW —— 前提已验证PM 预批的方向,前提是「算子词汇表确实封闭」。已验证,证据:
filter-normalizer.ts的fieldLeaves是叶节点的唯一生产者(全仓 grepkind: 'leaf'只有它一处非测试命中,objectql-strategy.ts自己不造叶)。MONGO_TO_CUBE_OP之外的算子在建叶之前就以invalidFilterError(INVALID_FILTER/ 400)拒绝。MONGO_TO_CUBE_OP的 12 个 +set/notSet($null/$exists/ 裸null/$eq: null/$ne: null都归到这两个);$between降解成gte/lte,已在 12 个里。所以任何调用方写出的过滤器都到不了这个出口;真到了,只能是 normalizer 的表新增了这里没有分支的算子 —— 我们自己两张表漂移,而对此唯一不能给的答案就是悄悄放宽作者的查询。与
convertFilter的default:分支在 #4128 做出的是同一个选择。刻意不用invalidFilterError的 400 信封:这不是调用方形状的错误,是我们自己的 bug。values.length === 0那条return null原样保留 —— 它表达的是「这个叶没有谓词」,与execute()/NativeSQLStrategy一致,和「渲染不了」是两件事。函数的 doc 注释也据此改写。反向验证 —— 方向先预测,再跑
两项修改各自独立验证,方向不同,如实报告:
$startsWith回显为SELECT … GROUP BY id(无 WHERE)。修完 → 817/817 绿。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,两层:$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,而不是碰巧通过。FILTER_OPERATORS(15 个可编写算子)驱动,逐个断言回显渲染出谓词、$nplaceholder 数与params.length对齐,并断言「执行侧有 WHERE 的地方回显也必须有」。另有一条断言表本身覆盖了 spec 声明的全部算子 —— spec 新增算子而这张表没跟上,这里先红。命令与结果:
消费半径已清扫:
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 修)
ObjectQLStrategy.convertFilter把$contains送成未声明的$regex(比较值不转义),三个同族算子早在 #4128 已改成规范算子 —— 实测 #5557 —convertFilter把contains送成未声明的$regex且比较值不转义({$contains: 'a.b'}→{$regex: 'a.b'},new RegExp('a.b').test('axb') === true;'50% (+)'直接 SyntaxError),而它三个同族算子早在 analytics filter-normalizer:未映射的算子被静默丢弃 → 查询放宽到全表($between 已修,还剩四个) #4128 就改成了规范算子。这是执行侧的缺陷,与本 issue 的回显侧只隔一个函数,故单独开单。本 PR 的 stand-in 引擎里对它做了一处$regex→$contains翻译(测试脚手架,注释指向ObjectQLStrategy.convertFilter把$contains送成未声明的$regex(比较值不转义),三个同族算子早在 #4128 已改成规范算子 —— 实测 #5557),因为read-scope-sql.ts对$regexfail-closed —— 这本身就是ObjectQLStrategy.convertFilter把$contains送成未声明的$regex(比较值不转义),三个同族算子早在 #4128 已改成规范算子 —— 实测 #5557 的第 2 条证据。未碰
filter-normalizer.ts(#5526 根因单待派)。🤖 Generated with Claude Code
https://claude.ai/code/session_01BWS4heBoAitLmzCLhcYdbK