Skip to content

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions