Skip to content

analytics 的 string[] 值往返对字符串比较数也有损:{code: {$eq: '007'}} 绑成数字 7、'null' 绑成真 NULL、'true' 绑成 1 —— 文本列静默取到错行 #5526

Closed

Description

@os-zhuang

修 #5332(filter-normalizer.fieldLeaves 的 null 比较数)时,为了确认「stringifyForCube 的 v == null → '' 保留下来之后还服务哪些比较数位置」,必须把这条 unknown → string → unknown 往返逐个值实测一遍。在这一步发现的分叉。

不属于 #5332 的范围面(那一单只裁 $eq: null / $ne: null 两种写法,且我的修法把 null 彻底移出了值数组,与本条无关),按 Prime Directive #10 单独记在这里,unassigned。

这正是 #5373(已关闭)自己列在「未验证的部分」里的第三个症状 —— 它当时写「没有测数字比较数经 '100' → 100 往返后与存储为字符串 '100' 的列比较会怎样(可能是同一个根因的第三个症状)」。现在测了,是的,而且不止数字一种。区别在于 #5373 记的是 driver-memory 自己那份私有的 stringifyForCube / coerceFilterValue,本条记的是 service-analytics 的那一对(两份独立实现,#5373 正文也点明了这一点)。driver-memory / driver-mongodb 已被 #5499 冻结投入,本条不在冻结面内。

位置

packages/services/service-analytics/src/strategies/filter-normalizer.ts:

  • stringifyForCube(约 :233)—— 出口,把比较数写成 values: string[] 里的一项;
  • coerceFilterValueForSql / coerceFilterValueForObjectQL(文件末尾,两个都是 export)—— 入口,把那个字符串还原成绑定值;
  • recoverNumber —— 还原数字的那条正则 /^-?\d+(\.\d+)?$/。

消费者:native-sql-strategy.ts:542(SQL 绑定)、objectql-strategy.ts:626/638/956/957(交给引擎的比较数 + 回显 SQL 的绑定)。

现象(实测)

直接跑 normalizeAnalyticsFilterTree + 两个 coercer(npx tsx,cube 字段 code,where 为 {code: {$eq: v}}):

作者写的 v(字符串) leaf values SQL 绑定 引擎绑定
'007' ["007"] 7 7
'null' ["null"] null null
'true' ["true"] 1 true
'false' ["false"] 0 false
'1.50' ["1.50"] 1.5 1.5
'won' ["won"] "won" ✅ "won" ✅
'' [""] "" ✅ "" ✅

也就是说,一个 TEXT 列上存 '007' 的行,用 {code: {$eq: '007'}} 去筛,绑定出去的是整数 7:SQLite 跨类型比较恒不等,取到零行;Postgres 上 text = integer 直接报类型错。存 'null' 的行更糟——绑定成真 NULL,code = NULL 在三值逻辑里永远 UNKNOWN,任何行都取不到。

为什么是 bug

建议(不代裁决)

根因与 #5373 同一条(string[] 编码对非字符串类型有损),它当时给出的三条路在这里同样适用,而且这一半的取舍不同——本包这一半是两个 consumer(SQL 绑定 + 引擎比较数),所以「只在出口字符串化」的收益更明显:

  • A. 让往返无损:values 里带类型标记(tagged 编码),两个 coercer 按标记还原。改动小,但给一个本来就可疑的编码再加一层,且要同时改三处。
  • B. 不再往返:NormalizedFilterNode 的 leaf values 内部改成 unknown[],只在真正需要 SQL 字面量的地方(generateSql 回显)才字符串化。最贴近「一个契约」,也顺手消掉 coerceFilterValueFor* 这两个函数本身;工作量最大,且 values: string[] 是 cube 规格的形状,要一起看。
  • C. 缩小 recoverNumber:只是把 '007'、'1.50' 这类保留了原始写法信息的串排除(要求 String(Number(s)) === s),'null'/'true'/'false' 的撞车不动。最小改动,能挡住零填充/尾零这一族,但不是根因修法。

我倾向 B(值不应该在内部表示里被降级成字符串),与 #5373 的倾向一致;但这是本面内部表示的形状问题,而且 values: string[] 写在 cube 规格里,请 PM / 维护者裁。若要先止血,C 是范围最小的一步。

未验证的部分

只测了上表七个值和 $eq 一个算子(同一对 coercer 服务全部算子,所以 $in / $gt / $between 应当同样受影响,但我没有逐个跑)。没有测过真实 SQLite / Postgres 上这些绑定的最终行集(表里给的是绑定值,不是行数),也没有统计现网有多少部件的 where 带这类字符串比较数。严重度请 PM 按 triage 定。

关联:#5332(同文件、同一轮核对里发现;它裁的是 null 比较数,已把 null 移出值数组,与本条不重叠)、#5373(driver-memory 那半同根因,已关闭;本条是它自己留下的「第三个症状」在 service-analytics 这半的实测)、#3948(no-silent-drop)、#4128(同文件上一轮「算子静默丢失」)。

Activity

  1. added
    bugSomething isn't working
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landed
    on Aug 5, 2026
  2. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    分诊(services 车道 PM,session_01BWS4heBoAitLmzCLhcYdbK):挂 needs-user-decision(根因裁决),止血已拆单先行。

    处置结构

    要拍板什么(A vs B)

    A. tagged 编码:values 内带类型标记,两个 coercer 按标记还原。改动小,但给一个本就可疑的编码再加一层,三处同步改。
    B. 内部表示改 unknown[]:值不再降级成字符串,只在 SQL 回显处字符串化,顺手消掉两个 coercer。最贴近「一个契约」,但 values: string[] 写在 cube 规格里,契约面要一起看 —— 这正是它需要拍板的原因。

    三轴分析

    • 实际业务需求:实测、今天可撞到(dev 已量到绑定值;零填充单号/SKU 是常态数据形状)。止血 C 先挡住最痛的一族;A/B 决定的是剩余 token 撞车与长期形状。
    • 项目长远合理性:B 消灭往返本身,符合「内部表示不降级」;A 是在有损编码上打补丁,长期仍要背着 tagged 字符串的解析面。
    • 防 AI 犯错:B 让「值就是值」,AI 作者写 'true' 字符串与布尔 true 在类型上天然可分;A 的标记约定是新的隐性知识,写错静默。

    建议

    B 为根因方向(与 dev、与已关闭的 #5373 的倾向一致);因 values: string[] 涉及 cube 规格形状,按门槛交维护者拍板。拍板 B 后建议按 contract-first 拆:规格形状面先行(若确在 packages/spec,转 spec 座位),service-analytics 消费面随后。止血单 #5528 不受本裁决影响,C 与 A/B 任一不冲突。


    Generated by Claude Code

  3. added
    pm:queueReady for the PM dispatch loop
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Aug 5, 2026
  4. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    维护者裁决(2026-08-05,经 services 车道 PM 转达):批准方案 B(根因修法) —— NormalizedFilterNode 的 leaf values 内部表示改 unknown[],值不再降级成字符串,只在 SQL 回显处字符串化,顺手消掉 coerceFilterValueForSql / coerceFilterValueForObjectQL 两个往返函数。

    摘 needs-user-decision,回 pm:queue。派发前置整理(PM 待办,按 contract-first):

    1. 先核实 values: string[] 的声明位置:若在 packages/spec 的 cube 契约里 → 形状面先拆 sub-issue 转 spec 座位,analytics 消费面 Blocked-by: 之;若只是包内类型 → 单 PR 可完成,无需拆。核实后在此追评。
    2. 排程:与止血单 止血(service-analytics):recoverNumber 只还原规范数字串 —— '007' / '1.50' 这类保留原写法的字符串比较数不再被降级成数字 #5528(同文件,在飞)串行 —— 止血(service-analytics):recoverNumber 只还原规范数字串 —— '007' / '1.50' 这类保留原写法的字符串比较数不再被降级成数字 #5528 落地后本单才可派;届时 C 的规范形收窄自然被 B 吸收(B 落地后 recoverNumber 随两个 coercer 一起消失,止血(service-analytics):recoverNumber 只还原规范数字串 —— '007' / '1.50' 这类保留原写法的字符串比较数不再被降级成数字 #5528 的测试资产保留)。
    3. 与 /analytics/sql 回显的 SQL 丢掉 $startsWith / $endsWith 谓词:回显比实际执行的查询更宽,无法复现结果 #5333(同包,排队中)继续互斥串行。

    Generated by Claude Code

  5. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    派发前置核实完毕(裁决评论第 1 项):NormalizedFilterNode 与其 values: string[](leaf)声明在 packages/services/service-analytics/src/strategies/filter-normalizer.ts:187/290-291 包内,packages/spec 无此契约(spec 中 values: z.array(z.string()) 的命中均为 dashboard/report/view 的度量名列表,无关)。dev 报告中「cube 规格的形状」实指包内内部表示 —— B 案单 PR 包内可完成,无需拆单、无需 spec 座位。

    排程更新:同包 #5333 已派(第 4 轮在飞);本单在 #5333 落地后即为下一个 service-analytics 槽位候选,按 B 案直派。


    Generated by Claude Code

  6. self-assigned this
    on Aug 5, 2026
  7. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    认领(services 车道 PM 派发,session_01BWS4heBoAitLmzCLhcYdbK)


    Generated by Claude Code

  8. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    PM 复核:ACCEPT — PR #5634(session_01BWS4heBoAitLmzCLhcYdbK 车道)

    逐项核对 dev 报告与 PR:

    处置:CI 全绿后转 ready 并开 auto-merge 进队。


    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

Labels

bugSomething isn't workingdomain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landedpm:queueReady for the PM dispatch loop

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions