SQLite 上任何按天/周/月/年分桶的趋势图,只要分桶列是 Field.datetime,全部记录塌进一个 (null) 桶 —— 图上只剩一根柱子。度量总额是对的,坏的只有分桶键。Field.date(ISO TEXT 存储)不受影响,所以同一个仪表盘里两种列并排放着,一个正常一个全塌。
真实 SQLite 已复现,并由 #3650(PR #3766)pin 成 sql-driver-aggregate-datetime-window.test.ts 里的 "KNOWN GAP" 断言 —— 断言当时写的就是坏值 { null: 330 }。
根因链
- better-sqlite3 把
Field.datetime 存成 INTEGER epoch 毫秒(knex 把 JS Date 绑成 .getTime())。
SqlDriver.buildDateBucketExpr(sql-driver.ts:526)对 SQLite 产出裸的 strftime('%Y-%m', ??)。SQLite 把裸 INTEGER 当 Julian day number 解释,epoch-ms 远超合法范围 → strftime 返回 NULL。表达式对列的存储形态一无所知。
- engine 不兜底:
engine.aggregate(engine.ts:3633)只在「该 granularity 未被 supports.queryDateGranularity 声明」或「非 UTC 时区」两种情况下才回落到 applyInMemoryAggregation。SQLite 声明了 day/month/quarter/year = true,所以 UTC 下必然下推给 driver;driver 返回 NULL 桶,没有任何一层察觉。
为什么现有测试没抓到
sql-driver-date-bucket.test.ts 建表用的是 knex.schema.createTable + t.string('ts') —— 存的是 ISO TEXT,而 TEXT 恰好是 strftime 能直接解析的那一半。它从没走过 driver.initObjects([{fields: {x: {type: 'datetime'}}}]) 那条真实路径,所以四个粒度全绿,而生产里 100% 的 Field.datetime 列全坏。
SqliteWasmDriver extends SqlDriver,继承同一个 buildDateBucketExpr,所以 wasm 驱动同病,且它的 date-bucket 测试用的是同一套 TEXT fixture。
同族前置
#2034(commit db02bd5)为同一个 epoch-vs-TEXT 根因给 driver 加过 storage-aware 的 temporalFilterValue,修的是 filter 比较值那一半。分桶表达式这一半漏了 —— 于是 window 正确、分桶全 NULL:窗口内的总额,整个堆在一个 (null) 柱子上。两个 bug 叠加时(#3650 之前),SQLite 上的按月趋势图 = 一根柱子 + 全量历史。
顺带:两条分桶路径的 parity 漂移
bucketDateValue(objectql 内存兜底)对数值 epoch-ms 输入走 new Date(String(1767225600000)) → Invalid Date → '(null)'。修好 driver 而不修它,只会把「两条路一起错」换成「两条路各错各的」—— sql-driver-date-bucket.test.ts 顶上那句 "⚠️ Keep in sync" 就是这条契约。
其它方言
Postgres / MySQL 的分支不受影响:defineColumn 把 Field.datetime 映射成原生 timestamp(table.timestamp),这也正是 temporalFilterValue 在这两个方言上不做 epoch 强转的原因。真要碰上 bigint 列(external 表拿 datetime 声明一个整数列),Postgres 会直接拒掉 ::timestamptz 强转 —— 是 SQLite 没给的那种响亮失败。
验收
sql-driver-aggregate-datetime-window.test.ts 里那条断言从 { null: 330 } 改成 { '2026-01': 300, '2026-02': 30 },并覆盖 day/month/quarter/year 四个粒度 × datetime/date 两种存储。
SQLite 上任何按天/周/月/年分桶的趋势图,只要分桶列是
Field.datetime,全部记录塌进一个(null)桶 —— 图上只剩一根柱子。度量总额是对的,坏的只有分桶键。Field.date(ISO TEXT 存储)不受影响,所以同一个仪表盘里两种列并排放着,一个正常一个全塌。真实 SQLite 已复现,并由 #3650(PR #3766)pin 成
sql-driver-aggregate-datetime-window.test.ts里的 "KNOWN GAP" 断言 —— 断言当时写的就是坏值{ null: 330 }。根因链
Field.datetime存成 INTEGER epoch 毫秒(knex 把 JSDate绑成.getTime())。SqlDriver.buildDateBucketExpr(sql-driver.ts:526)对 SQLite 产出裸的strftime('%Y-%m', ??)。SQLite 把裸 INTEGER 当 Julian day number 解释,epoch-ms 远超合法范围 →strftime返回 NULL。表达式对列的存储形态一无所知。engine.aggregate(engine.ts:3633)只在「该 granularity 未被supports.queryDateGranularity声明」或「非 UTC 时区」两种情况下才回落到applyInMemoryAggregation。SQLite 声明了day/month/quarter/year = true,所以 UTC 下必然下推给 driver;driver 返回 NULL 桶,没有任何一层察觉。为什么现有测试没抓到
sql-driver-date-bucket.test.ts建表用的是knex.schema.createTable+t.string('ts')—— 存的是 ISO TEXT,而 TEXT 恰好是strftime能直接解析的那一半。它从没走过driver.initObjects([{fields: {x: {type: 'datetime'}}}])那条真实路径,所以四个粒度全绿,而生产里 100% 的Field.datetime列全坏。SqliteWasmDriver extends SqlDriver,继承同一个buildDateBucketExpr,所以 wasm 驱动同病,且它的 date-bucket 测试用的是同一套 TEXT fixture。同族前置
#2034(commit db02bd5)为同一个 epoch-vs-TEXT 根因给 driver 加过 storage-aware 的
temporalFilterValue,修的是 filter 比较值那一半。分桶表达式这一半漏了 —— 于是 window 正确、分桶全 NULL:窗口内的总额,整个堆在一个(null)柱子上。两个 bug 叠加时(#3650 之前),SQLite 上的按月趋势图 = 一根柱子 + 全量历史。顺带:两条分桶路径的 parity 漂移
bucketDateValue(objectql 内存兜底)对数值 epoch-ms 输入走new Date(String(1767225600000))→ Invalid Date →'(null)'。修好 driver 而不修它,只会把「两条路一起错」换成「两条路各错各的」——sql-driver-date-bucket.test.ts顶上那句 "其它方言
Postgres / MySQL 的分支不受影响:
defineColumn把Field.datetime映射成原生 timestamp(table.timestamp),这也正是temporalFilterValue在这两个方言上不做 epoch 强转的原因。真要碰上 bigint 列(external 表拿datetime声明一个整数列),Postgres 会直接拒掉::timestamptz强转 —— 是 SQLite 没给的那种响亮失败。验收
sql-driver-aggregate-datetime-window.test.ts里那条断言从{ null: 330 }改成{ '2026-01': 300, '2026-02': 30 },并覆盖 day/month/quarter/year 四个粒度 × datetime/date 两种存储。