Repository navigation
analytics filter-normalizer:未映射的算子被静默丢弃 → 查询放宽到全表($between 已修,还剩四个) #4128
Copy link
Copy link
Closed
Description
Activity
- added a commit that references this issue
on Jul 30, 2026 已在 #4109 收口。
先更正本 issue 的一处错误
原文把
$regex列为 spec 词汇,这是错的——我当时用grep -oE '\$[a-zA-Z]+'扫filter.zod.ts,把注释里的「MongoDB: $regex」当成了声明。核对StringOperatorSchema/SpecialOperatorSchema的实际 key,可授权词汇里没有$regex(driver-sql 里那处$regex注释也写明它只经 better-auth adapter 进来,且只当 substring 用)。所以 ❌ 那一档实际是三个,不是四个。实际修了什么
盘点时又挖出两处比「丢弃」更糟的——语义被悄悄写反:
算子 修复前 修复后 $startsWith/$endsWith静默丢弃 → 全表 raw-SQL 侧编译为带锚点的 LIKE 'x%'/LIKE '%x';ObjectQL 侧透传规范算子(各驱动原生实现),锚点不依赖 regex 方言$null静默丢弃 → 全表 按布尔值编译为 IS NULL/IS NOT NULL$exists被与取值无关地映射成 set,于是{$exists: false}编译成IS NOT NULL——与语义完全相反与 $null一起显式解析(键→名的映射表本就表达不了「含义随取值翻转」的算子,这正是反转的成因)$notContains进到 ObjectQLStrategy后没有对应 case,落到default: return v0返回裸值——「不包含 x」被编译成「等于 x」透传 $notContains未知算子 归一化层 continue丢弃;ObjectQL 层default当成相等两处都抛错(#3948 对同一形态的先例) $or/$not仍然跳过:要表达它们需要递归 WHERE 构建器,而策略消费的是扁平数组——这一条现在写进模块文档作为已声明的缺口,不再是隐性的。验证
新增
filter-operator-coverage.test.ts:把filter.zod.ts的整个可授权词汇跑在真实 SQLite(sql.js)上,断言行 id,并额外断言结果集不等于全表(「谓词消失」唯一那种看起来像正常查询的错误答案)。用git stash实测过:不带修复时 6 个用例红。这类缺陷之所以能活这么久,正是因为这两个策略现有的套件断言的都是 SQL 字符串——而谓词被删掉对字符串断言完全隐形。
service-analytics 413 全绿(原 391)。
Generated by Claude Code
- added a commit that references this issue
on Jul 30, 2026 - added 4 commits that reference this issue
on Jul 31, 2026 - added a commit that references this issue
on Sep 28, 2026
Metadata
Metadata
Assignees
Labels
No labels
从 #4081 的温度一致性矩阵补第六个消费者(
NativeSQLStrategy)时发现的一类缺陷,$between那一格已在该 PR 内修掉,但成因还在,这里单独立项。成因
packages/services/service-analytics/src/strategies/filter-normalizer.ts把 Mongo 风格的where拍平成内部管线形式,靠一张MONGO_TO_CUBE_OP映射表:continue掉一个谓词不是「不支持」,是把查询放宽。少一个 WHERE 条件的 SQL 依然合法,只是多返回行——所以它对断言 SQL 字符串的测试完全隐形,对用户表现为图表画出全部历史(#3650 的症状)。normalizeAnalyticsFilters同时被NativeSQLStrategy和ObjectQLStrategy使用,两条路径都受影响。现状盘点
spec 的
FilterCondition词汇(filter.zod.ts)对照映射表:$eq$ne$gt$gte$lt$lte$in$nin$contains$notContains$exists$betweengte+lte,畸形输入抛错)$startsWith$endsWith$null$regex$and$or/$not四个 ❌ 都是作者可以合法写进 dashboard widget / dataset 过滤器的 spec 词汇(
$null尤其常见:「未指派」「无关闭日期」这类看板)。今天它们不会报错、不会告警,只会多给行。建议的方向
两步,第二步需要决策:
实现能实现的。
$startsWith/$endsWith/$regex都能落到已有的contains(LIKE)家族上;$null落到已有的set/notSet(IS [NOT] NULL,两条策略都已支持)。这四个都不需要新概念,只是映射表缺项。把兜底从
continue改成throw。 这是 A filter with an operator outside VALID_AST_OPERATORS is silently dropped, not rejected — single-condition views return unfiltered results #3948 在 driver-memory 上已经做过的同一个决定(convertConditionToMongo从return null改为抛错,注释原文:「Returning no predicate would silently match every record」),也是 Prime Directive chore: version packages #10「declared ≠ enforced 要么实现要么报错,别假装覆盖」的直接应用。爆炸半径需要评估:改完之后,任何今天在用未映射算子的看板会从「悄悄画错」变成「明确报错」——方向正确,但属于行为变更,值得单独确认。$or/$not是第三档:注释写明「策略尚不支持递归 WHERE 构建,忽略以便部分查询仍能运行」。这条理由在compileScopedFilterToSql(read-scope 已能编译完整的$or树,见 #3774 的一致性表)存在之后是否还成立,值得复查——如果能复用它,$or就不必再被丢。注意 RLS read scope 走的是另一条路径,不受影响;受影响的是作者自己写的where。复现
矩阵消费者本身就是复现器(
packages/services/service-analytics/src/__tests__/native-sql-temporal-conformance.test.ts,真实 sql.js 引擎 + 断言行 id)。把$between换成上表任一 ❌ 算子,返回的就是全表。关联
#4081(矩阵)、#4098(矩阵落地 + preview 侧同species 的
$between缺陷)、#3650(窗口被丢弃画出全部历史)、#3948(driver-memory 同一决定的先例);ADR-0053 D-A3.1。