Skip to content

drivers(sql): 共享 canonical 修复表达式把「裸数字文本」当儒略日读,backfill 会把这个误读写进盘里(观察项) #6009

Description

@os-zhuang

观察项,不是今天有人会撞上的缺陷 —— 在做 #5770(driver-turso remote backfill,PR #6006)时实测到,顺手记录。不建议在没有真实业务拉动前动手

实测

SqlDriver.sqliteCanonicalDatetimeSql 的 else 分支是 coalesce(strftime('%Y-%m-%dT%H:%M:%fZ', col), col)。SQLite 的 strftime 接受裸数字并按儒略日解释,于是一个 Field.datetime 列里的裸数字文本会被读成公元前的日期(better-sqlite3 13.0.2 实测):

'12'                       -> -4713-12-06T12:00:00.000Z
'0'                        -> -4713-11-24T12:00:00.000Z
'1999'                     -> -4707-05-15T12:00:00.000Z
'2026'                     -> -4707-06-11T12:00:00.000Z
'86400'                    -> -4476-06-15T12:00:00.000Z
'2440587.5'                -> 1970-01-01T00:00:00.000Z   (儒略日纪元,确实是这个语义)
'2026-08-06'               -> 2026-08-06T00:00:00.000Z   (正常)
'2026-08-06T00:00:00.000Z' -> 2026-08-06T00:00:00.000Z   (正常,不动点)

coalesce 的兜底在这里帮不上忙:strftime 不返回 NULL,它返回了一个自信的错误答案,所以「解析不了的值原样保留」这条设计意图在这一支上不成立。

为什么今天基本撞不上

需要「Field.datetime 列里存着裸数字 TEXT」。两种 affinity 的行为不同:

  • local / knex datetime 列是 NUMERIC 亲和:'12' / '2026' 入库即被转成 INTEGER(实测 typeofinteger),于是走的是表达式的 integer/real 分支(按 epoch 毫秒),不是儒略日分支。所以 local 基本绕开了。
  • remote / RemoteTransport.mapFieldTypeToSQL 把时间列声明为 TEXT:值保持 TEXT,儒略日分支就会命中。这是唯一比较容易到达的一侧。

而要有这种值,本身就得是脏数据(合法的 canonical 拼写一定带 - / : / T / Z)。

值得记一笔的地方:读是临时的,backfill 是永久的

误读本身自 #3912 起就存在,但那时它只是每次读临时算出来,盘上的原始字节还在。backfillCanonicalDatetimes 的 SET 表达式就是同一个表达式,所以它会把这个儒略日误读写回磁盘,原值不可恢复。PR #6006 让 remote 也有了 backfill,于是这条路径在 remote 侧也从「临时误读」变成了「落盘」。

需要说清楚的是:#6004/#6006 没有改变任何查询答案 —— backfill 写下去的正是读路径本来就返回的值(这也是那个模块的核心不变量)。变的只是「原始字节是否还留着」。#6006 的 epoch 恢复限带在 1e12 ≤ v 小于 4102444800000,正是为了不去碰这类小数字,把它们留给共享表达式并计入 unresolvedEpochTextRows

如果将来要处理

倾向不要在共享表达式里加启发式 —— 维护者 2026-08-03 在 cloud#1005 已就近似的提案裁过否决(公共契约 + 每次读都跑 + 会误读合法的数字字符串列)。真要做,更像是在 backfill 的 SET 侧加一道「不把 strftime 的儒略日结果写下去」的守卫(纯迁移期、一次性、可按声明类型收窄),读路径保持原样。

判断留给 PM/维护者分诊。

参考:packages/drivers/driver-sql/src/sql-driver.tssqliteCanonicalDatetimeSql / backfillCanonicalDatetimes;ADR-0053 storage-form 轴(#4191)。

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions