Skip to content

aggregate()/distinct() 不过 formatOutput —— SQLite 上 Field.datetime 的裸 epoch 从聚合出口漏出去(#3773 同根不同出口) #3797

Description

@os-zhuang

SqlDriver.aggregate()distinct() 都直接 return await builder,不过 formatOutput。所以在 SQLite 上,Field.datetime 列的原始存储形态(INTEGER epoch 毫秒)会从这两个出口原样漏给调用方 —— 而 find() 同一列返回的是规范化的 ISO-Z

实测(better-sqlite3,一行 closed_at = 2026-01-10T09:00:00Z):

路径 返回
find() "2026-01-10T09:00:00.000Z"
distinct('closed_at') [1768035600000] ❌ 裸 epoch
aggregate() max(closed_at) as latest 1768035600000
aggregate() groupBy: ['closed_at'](无 granularity) {closed_at: 1768035600000}

Field.date 四条全对 —— 它在每个方言上都是 ISO TEXT,存储形态本来就等于呈现形态。和 #3773同一个根因的不同出口:凡是绕过 formatOutput 的读路径,在唯一「存储 ≠ 呈现」的方言上都会漏。

可达面

  • min/max 度量:analytics 的 inferMeasure_min/_max 后缀(analytics-service.ts:952),所以 closed_at_max 这样的「最近成交时间」KPI 磁贴在 SQLite 上会显示 1768035600000
  • 无 granularity 的 groupBy:桶键是裸 epoch。这同时是一条 parity 漂移 —— 内存兜底 applyInMemoryAggregation 吃的是 driver.find() 的行(已经 formatOutput 过),它给的是 ISO 字符串。同一个数据集换条路走,桶键类型都不一样,drill-down 跨不过去。这正是 SQLite 上 Field.datetime 的 dateGranularity 分桶恒为 NULL —— 趋势图塌成一根柱子 #3773 刚为 date-bucket 那一半钉下的契约。
  • distinct():扫了全仓,零调用方(除驱动自身测试)。漏是真漏,但没有用户可达路径 —— 顺手修保持一致即可,不构成紧急度。

修法上的一个陷阱

不能简单地「对着输出行跑一遍 formatOutput」,也不能按列名匹配声明字段:

所以规范化必须由**「这一列是什么产生的」**驱动 —— 在建查询时就记下每个输出列的语义,而不是事后按名字猜。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions