Repository navigation
Description
修 #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
- 静默错行,不是报错。「订单号 = 007」的部件画出来是空的,作者看到的是「没有数据」;
'null'那条是永远空。方向以缩小为主,但和 A filter with an operator outside VALID_AST_OPERATORS is silently dropped, not rejected — single-condition views return unfiltered results #3948 立的规矩同一族:一个声明支持的算子把作者的值悄悄换成了另一个值。 - 零填充字符串是真实业务形状,不是构造出来的边角:订单号 / 工单号 / 国家区号 / SKU / 邮编,
'007'、'0912'都会命中recoverNumber的正则。'1.50'这种价格串也一样,丢掉尾零之后再比对文本列就不匹配了。 token与真实字符串撞车是这套编码自己的选择,但没有转义。stringifyForCube的 TSDoc 明确解释了为什么布尔要写成'true'/'false'而不是'1'/'0'(driver-memory analytics 面的 cube 值往返是有损的:布尔比较数变成数字({is_active: true}取到 0 行),null比较数被整条丢掉(取到全表) #5373 记的正是'1'/'0'那版在 driver-memory 上的后果),这个理由成立;但没有任何机制区分「布尔 true 序列化出来的'true'」和「作者本来就在筛字符串'true'」。null同理。往返有损的根因是values: string[]这个内部表示,而不是某一条if。- 两个消费者还原成不同类型:
'true'在 SQL 侧变1、在引擎侧变true。这是刻意的(两边存储形态不同),但也意味着同一个where在两条路径上绑出两种类型,而两边都不是作者写的那个字符串。
建议(不代裁决)
根因与 #5373 同一条(string[] 编码对非字符串类型有损),它当时给出的三条路在这里同样适用,而且这一半的取舍不同——本包这一半是两个 consumer(SQL 绑定 + 引擎比较数),所以「只在出口字符串化」的收益更明显:
- A. 让往返无损:
values里带类型标记(tagged 编码),两个 coercer 按标记还原。改动小,但给一个本来就可疑的编码再加一层,且要同时改三处。 - B. 不再往返:
NormalizedFilterNode的 leafvalues内部改成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
os-zhuang commentedon Aug 5, 2026
分诊(services 车道 PM,
session_01BWS4heBoAitLmzCLhcYdbK):挂needs-user-decision(根因裁决),止血已拆单先行。处置结构
recoverNumber只还原规范数字串 ——'007'/'1.50'这类保留原写法的字符串比较数不再被降级成数字 #5528,路线 C 的精确化 ——recoverNumber仅还原规范数字形(String(Number(s)) === s),挡住'007'/'0912'/'1.50'这族已实测的真实业务形状。这是无歧义的纠错(保留了原写法信息的串只可能是作者笔下的字符串),不动契约、不裁根因,按「明显的问题直接修」入队;因与 PR fix(service-analytics): 分析查询里的null比较数是 null 谓词,不是= ''(#5332) #5525 同文件,Blocked-by:它落地。values: string[]编码对非字符串类型有损,'null'/'true'/'false'的 token 撞车没有任何转义机制可区分。要拍板什么(A vs B)
A. tagged 编码:
values内带类型标记,两个 coercer 按标记还原。改动小,但给一个本就可疑的编码再加一层,三处同步改。B. 内部表示改
unknown[]:值不再降级成字符串,只在 SQL 回显处字符串化,顺手消掉两个 coercer。最贴近「一个契约」,但values: string[]写在 cube 规格里,契约面要一起看 —— 这正是它需要拍板的原因。三轴分析
'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
os-zhuang commentedon Aug 5, 2026
维护者裁决(2026-08-05,经 services 车道 PM 转达):批准方案 B(根因修法) ——
NormalizedFilterNode的 leafvalues内部表示改unknown[],值不再降级成字符串,只在 SQL 回显处字符串化,顺手消掉coerceFilterValueForSql/coerceFilterValueForObjectQL两个往返函数。摘
needs-user-decision,回pm:queue。派发前置整理(PM 待办,按 contract-first):values: string[]的声明位置:若在packages/spec的 cube 契约里 → 形状面先拆 sub-issue 转 spec 座位,analytics 消费面Blocked-by:之;若只是包内类型 → 单 PR 可完成,无需拆。核实后在此追评。recoverNumber只还原规范数字串 ——'007'/'1.50'这类保留原写法的字符串比较数不再被降级成数字 #5528(同文件,在飞)串行 —— 止血(service-analytics):recoverNumber只还原规范数字串 ——'007'/'1.50'这类保留原写法的字符串比较数不再被降级成数字 #5528 落地后本单才可派;届时 C 的规范形收窄自然被 B 吸收(B 落地后recoverNumber随两个 coercer 一起消失,止血(service-analytics):recoverNumber只还原规范数字串 ——'007'/'1.50'这类保留原写法的字符串比较数不再被降级成数字 #5528 的测试资产保留)。/analytics/sql回显的 SQL 丢掉$startsWith/$endsWith谓词:回显比实际执行的查询更宽,无法复现结果 #5333(同包,排队中)继续互斥串行。Generated by Claude Code
os-zhuang commentedon Aug 5, 2026
派发前置核实完毕(裁决评论第 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
os-zhuang commentedon Aug 5, 2026
认领(services 车道 PM 派发,
session_01BWS4heBoAitLmzCLhcYdbK)claude/issue-5526-unknown-values../objectstack-issue-5526(dev 子代理按 worktree-first 自建)NormalizedFilterNodeleafvalues内部表示改unknown[],消掉coerceFilterValueForSql/coerceFilterValueForObjectQL往返,仅在 SQL 回显处字符串化。packages/services/service-analytics/src/strategies/filter-normalizer.ts(类型 + 出入口)、src/strategies/native-sql-strategy.ts(:542 绑定消费)、src/strategies/objectql-strategy.ts(:626/638/956/957 引擎比较数 + 回显)、同包测试(保留 止血(service-analytics):recoverNumber只还原规范数字串 ——'007'/'1.50'这类保留原写法的字符串比较数不再被降级成数字 #5528 测试资产)、.changeset/*.md。若两个 coercer 在包公开面(index.ts / api-surface)则含相应清理与 regen。recoverNumber只还原规范数字串 ——'007'/'1.50'这类保留原写法的字符串比较数不再被降级成数字 #5528(PR fix(service-analytics):recoverNumber只还原规范数字串 ——'007'/'1.50'保持字符串 (#5528) #5547)、/analytics/sql回显的 SQL 丢掉$startsWith/$endsWith谓词:回显比实际执行的查询更宽,无法复现结果 #5333、analytics 侧三个 SQL 编译器都不转义 LIKE 比较值:$contains: '_admin'命中xyadmin、$contains: '50%'命中off 5012 now—— driver-sql 自己把这条旁路标为 P0 —— 实测 #5567(PR fix(service-analytics): analytics 三个 SQL 编译器转义 LIKE 比较值,并绑显式 ESCAPE (#5567) #5587)全部 merged,同包无在飞 PR。Generated by Claude Code
os-zhuang commentedon Aug 5, 2026
PM 复核:ACCEPT — PR #5634(session_01BWS4heBoAitLmzCLhcYdbK 车道)
逐项核对 dev 报告与 PR:
src/index.ts零导出、api-surface.json不含本包)—— 无公开面收缩,单 PR 成立,无需 needs_decision。[裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499 冻结面未触(driver-memory不走此 normalizer,grep 佐证)。toSqlBindValue是「值 → 绑定形态」单向编码(函数体零字符串检查),与被删的「字符串 → 类型」解码器方向相反 —— 这正是裁决要的「值不降级」。对派发说明的一处修正(generateSql双路径皆占位符化、从不内联,故回显无需字符串化)有代码证据,接受。{$contains: null}从全表放行(LIKE '%%')收敛到与 driver-sql 一致;{$gt: null}落到 分析查询的{field: {$eq: null}}/{$ne: null}编译成col = ''/col != '',与同文件里{field: null}的IS NULL自相矛盾 #5332 明示未裁、driver-memory/formula 已给出的答案上。均已钉测试。recoverNumber只还原规范数字串 ——'007'/'1.50'这类保留原写法的字符串比较数不再被降级成数字 #5528 测试资产整体保留并扩面(854→905,+51 全绿):DECODE 表 21 行升级为端到端三问,decoy 行(文本'null'紧挨真 NULL、文本'true'紧挨'1')+ 精确行集断言。analytics 侧三个 SQL 编译器都不转义 LIKE 比较值:$contains: '_admin'命中xyadmin、$contains: '50%'命中off 5012 now—— driver-sql 自己把这条旁路标为 P0 —— 实测 #5567 的 29 例锚定、分析查询的{field: {$eq: null}}/{$ne: null}编译成col = ''/col != '',与同文件里{field: null}的IS NULL自相矛盾 #5332/fix(service-analytics): 分析查询里的null比较数是 null 谓词,不是= ''(#5332) #5525 空值谓词、analytics 的 filter 拒收到不了调用方:service 侧多数拒收没有 ADR-0112 信封,REST 面又用 message 正则嗅探,一律答 500 #5352 拒绝集合零改动通过。String()宽容追评进既有 driver-sql:两类无意义比较对象仍编译成「静默空谓词」——$in/$nin的非$field对象成员,与 LIKE 族的对象比较值(String 成[object Object]) #5234(含「三份必须一起动」的范围约束),未开孪生单;authorable-surface.base.json被 gen:schema 重写的观察经 PM 查重确认已由check:authorable-surface在--check模式下也会重写authorable-surface.base.json—— 一次纯核验会改工作区,且任何无关 PR 都能因此静默推进删除门的锚点 #5358 / os-regen 驱动指示的gen:schema在 merge 未 commit 时运行,会把 authorable-surface 锚点倒退回旧 merge-base —— 生成器写入、门全绿、静默撤销 main 的锚点推进 #5370 /check:docs的第一步是gen:schema—— 修好 #4711 之后,「检查改工作区」仍从这里漏进来 #4723 同族覆盖,不另立单;type-check-coverage 台账计数噪声判断正确(该门不比对计数)。处置:CI 全绿后转 ready 并开 auto-merge 进队。
Generated by Claude Code
fix(service-analytics): `recoverNumber` 只还原规范数字串 —— `'007'` / `'1.50'…
fix(service-analytics): carry filter comparands at their own type, no…
fix(analytics): refuse an `undefined` comparand in the `where` door i…
fix(service-analytics)!: the analytics `where` door runs every arm of…