fix(service-analytics): recoverNumber 只还原规范数字串 —— '007' / '1.50' 保持字符串 (#5528) - #5547
Merged
Merged
Conversation
…, so '007' / '1.50' stay strings (#5528) An analytics `where` round-trips every comparand through `values: string[]` — `stringifyForCube` out, `coerceFilterValueForSql` / `coerceFilterValueForObjectQL` back — and the decoder decided "this is a number" from the string's SHAPE alone (`/^-?\d+(\.\d+)?$/`), which cannot tell a stringified number from a string the author wrote. Measured on main, cube `orders` / TEXT column `code`: `'007'` bound `7`, `'0912'` bound `912`, `'1.50'` bound `1.5` — on BOTH consumers, the raw-SQL bind and the engine comparand. The failure was silent and MIS-TARGETED rather than empty: SQLite applies the text column's affinity to the integer bind, so a widget filtered on order number `'007'` returned the row storing `'7'`. On Postgres the same query is a `text = integer` type error; on the engine path the strict comparison matched nothing (measured: 0 rows). Zero-padded and trailing-zero strings are ordinary business shapes — order numbers, SKUs, dialling codes, postcodes, prices. Recovery is now limited to a number's own canonical spelling (`String(Number(s)) === s`): a comparand that really was a number arrives as `String(n)` by construction and still round-trips (`7` → `'7'` → `7`), while a string `Number()` would rewrite cannot have come from a number and stays the author's string. The shape regex still runs FIRST, so the change can only remove recoveries — `'1e3'`, `'1e+21'`, `'+7'`, `' 7'`, `'0x10'`, `'Infinity'`, `'NaN'` were strings before and are strings after. This is the direction ADR-0053 D-A2 already set for this function: last-resort textual recovery behind the driver-backed `coerceTemporalFilterValue` hook. Stopgap by design, and named as one in the TSDoc and the tests: `values: string[]` still has no escape, so the author strings `'null'` / `'true'` / `'false'` still collide with the tokens the encoder writes for the real values. That is #5526's root cause (tagged values, or an `unknown[]` internal representation), pinned here as UNCHANGED so it is not mistaken for fixed. 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:
|
Left over from dropping three `eslint-disable-next-line
@typescript-eslint/no-explicit-any` comments: that rule is not defined in this
repo's eslint config, so the disables themselves were errors ("Definition for
rule ... was not found"). The sibling `filter-operator-coverage.test.ts` uses the
same `let db: any` harness with no disable comment.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BWS4heBoAitLmzCLhcYdbK
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 #5528
#5526 的止血单(其路线 C)。本单不裁根因 ——
values: string[]往返编码本身的有损性(以及'null'/'true'/'false'的 token 撞车)仍归 #5526,这里只收窄解码侧,挡住已实测的真实业务形状。前提复核(切分支后按 rule 6 实测)
#5525(#5332)已在基上;它改的是编码侧(
fieldLeaves把null移出值数组),本单改的是解码侧,两者不相交。在 origin/mainf2a1c0be1上跑normalizeAnalyticsFilterTree+ 两个 coercer,#5526 表格逐行复现:vvalues'007'["007"]77'0912'["0912"]912912'1.50'["1.50"]1.51.5'7'["7"]7✅7✅'1e3'["1e3"]"1e3"✅"1e3"✅前提成立。顺手实测到两条 issue 未列的同族形状:
'-0'→0(丢符号)、'12345678901234567890'→12345678901234567000(丢精度),同样是往返有损,一并纳入。改法
recoverNumber在原有形状正则之后加一条往返判据String(Number(s)) === s:String(n),天然规范,照旧还原(7→'7'→7);Number()会改写的串(前导零、尾零、'-0'、超出双精度位数)不可能来自数字,只能是作者写的字符串,保持原样。形状正则留在前面是刻意的:这样本改动只可能减少还原,不会新增。
'1e3'、'1e+21'、'+7'、' 7'、'0x10'、'Infinity'、'NaN'改前是字符串、改后仍是字符串 —— 其中'1e+21'本身是String(1e21)的规范形,仅靠往返判据会被还原,正则挡住了它。方向上也是 ADR-0053 D-A2 早已定的:这个函数是 driver 侧
coerceTemporalFilterValue钩子背后的最后兜底,按形状猜类型的面越小越好。TSDoc 里已留下 ADR 编号与 issue 号。coerceFilterValueForSql/coerceFilterValueForObjectQL两个 coercer 同步(两者共用同一个recoverNumber;它们只在布尔写法上刻意不同,"哪些串算数字"必须是同一条规则),测试里有一条专门守这个一致性。反向验证(方向先预测,后运行)
预测:非规范形的 decode 行转红;行集用例转红且取到错行(不是零行 —— SQLite 会把 TEXT 列的 affinity 套到整数绑定上);引擎用例转红在比较数上;规范形 / 真数字往返 / token 撞车三组保持绿。
把
recoverNumber退回旧实现后实测,13 条红,方向逐条命中:一处如实说明:引擎用例的红停在比较数断言上,后面那句"行数为 0"没跑到。所以我用
tsx在退回态下单独量了一次,'007'→ 比较数7→order_count: 0,'1.50'→1.5→0;测试注释里的"Was 0 before #5528"是量出来的,不是推出来的。行集用例特意放了
r_7(存'7')和r_15(存'1.5')两行:改前作者查'007'拿到的是'7'那行 —— 比零行更难发现,这才是现场会报的症状。测试
新增
src/__tests__/filter-value-canonical-number.test.ts(39 例,复用本包既有的两套 harness):string[]值往返对字符串比较数也有损:{code: {$eq: '007'}}绑成数字7、'null'绑成真 NULL、'true'绑成1—— 文本列静默取到错行 #5526 三行、规范形、以及"改前后都是字符串"的 7 种形状;normalizeAnalyticsFilterTree出去再解回来(不是手写字符串),防过度收窄;$eq/$in,外加两条直接断言绑定类型的用例(数字列那条 SQLite 的 numeric affinity 会"救"回来,行集看不出差别,只有绑定类型能);AnalyticsService+ 严格相等的executeAggregate替身,比较数与行数一起断言;string[]值往返对字符串比较数也有损:{code: {$eq: '007'}}绑成数字7、'null'绑成真 NULL、'true'绑成1—— 文本列静默取到错行 #5526 撞车原样钉住(pinned, not endorsed),避免后来的读者误以为本单修掉了它。命令与结果:
typecheck:本包无typecheckscript(在check-type-check-coverage.mjs里带 DEBT 台账条目),故 CI 不对它做类型检查。仍手工跑了npx tsc --noEmit -p packages/services/service-analytics/tsconfig.json,报的 7 条全是既有测试文件的旧账,filter-normalizer.ts与新测试文件均无报错。消费半径已按"规则被谁消费"扫过:两个 coercer 只被本包
native-sql-strategy.ts/objectql-strategy.ts引用;仓库内既有的数字串比较数 fixture($gte: '80'、{ v: '9' })都是规范形,不受影响。范围红线
objectql-strategy.ts(/analytics/sql回显的 SQL 丢掉$startsWith/$endsWith谓词:回显比实际执行的查询更宽,无法复现结果 #5333 另单)、未碰stringifyForCube的编码方向(analytics 的string[]值往返对字符串比较数也有损:{code: {$eq: '007'}}绑成数字7、'null'绑成真 NULL、'true'绑成1—— 文本列静默取到错行 #5526 待裁)、未处理'null'/'true'/'false'token 撞车;'007'写在数字列上,改前会被"猜"成7而匹配,改后按作者写的字符串绑定,在 SQLite 上不再匹配。analytics 的string[]值往返对字符串比较数也有损:{code: {$eq: '007'}}绑成数字7、'null'绑成真 NULL、'true'绑成1—— 文本列静默取到错行 #5526 路线 C 的原话是"保留了原始写法信息的串只可能是作者笔下的字符串",这里照此执行 —— 让它按作者写的意思生效,而不是继续替作者改写。🤖 Generated with Claude Code
https://claude.ai/code/session_01BWS4heBoAitLmzCLhcYdbK
Generated by Claude Code