D-A3 的矩阵已经立起来了(#4081 已关),但 ADR 为它点名的 storage-form 轴只落在一个格子里。
D-A3's conformance matrix should gain a storage-form axis (canonical, legacy-epoch, legacy-naive) and a server-timezone axis, since both are now known to have produced dialect-divergent row results.
—— docs/adr/0053-date-and-datetime-semantics.md,D-B3 的 Consequences
现状(已逐个核对,不是推测)
sql-driver-temporal-conformance.test.ts:107 有一个 un-backfilled legacy storage 的 describe:用 LegacyStorageDriver.seedLegacyRows 绕过写入路径,把同一张表按 writer-form 标签分别注入 INTEGER epoch ms 和 zone-naive TEXT,然后跑全套 TEMPORAL_CASES。这个做法是对的,问题是它只有一份。
| 消费者 |
canonical |
legacy-epoch / legacy-naive |
driver-sql(datetime) |
✅ |
✅ :107 |
driver-sql(Field.time) |
✅ :150 |
❌ |
driver-sqlite-wasm |
✅ |
❌ |
driver-memory |
✅ |
❌ |
driver-mongodb |
✅ |
❌ |
formula |
✅ |
❌ |
service-analytics(native-sql / preview) |
✅ |
❌ |
其余六个消费者全部只做 driver.create(...) 播种,即
at: r.writerForm === 'native' ? new Date(r.at) : r.at,
wire | native 两种形态都是今天的写入路径会产出的东西。也就是说这些夹具在构造上就不可能含有遗留形态——矩阵测的是「我写进去的我能读回来」,而四次事故里有两次的机理是读到自己没写过的形态。
为什么 mongo 是最尖的一格
其他后端的遗留数据是过渡态,mongo 的是永久态,这是 ADR 明写的设计决定:
MongoDB gets no automatic backfill — rewriting a real collection is a migration, not a boot step — so a pre-existing collection keeps whatever it holds until migrated
—— D-E3
而 mongo 读错形态时的表现是所有后端里最坏的:
a string comparand never matches a Date value, in either direction, for every operator —— $gte included, not just the upper bound #3777 was about. SQLite's affinity rules at least left one half reachable
—— D-E1
SQLite 至少还有一半能匹配上,mongo 是整段窗口静默返回错误行集(#4047 当时的表现是空集)。所以:唯一保证长期持有遗留形态的后端,恰好是唯一没有遗留形态测试的后端,且它的失败模式最安静。
driver-memory 的情况稍好——D-E3 说它在 schema 到达时会收敛已有行(覆盖 initialData 夹具和持久化恢复,两者都发生在 schema 声明之前)——但这个收敛本身也没有被矩阵测过。
为什么 Field.time 那一格必须补
#3994 的成因逐字就是这个:
INTEGER epoch rows fail >= '09:00' because INTEGER < TEXT
—— D-C 段
TEMPORAL_TIME_ROWS 里 a_midnight 的 why 也已经写着「epoch 0 on SQLite, which sorts before every TEXT row」。但 :107 的遗留扫描只跑 TEMPORAL_CASES,Field.time 的 describe(:150)没有对应的遗留版本。修复 #3994 的那条读取路径,今天没有任何遗留形态的回归锁。
建议形状
把 LegacyStorageDriver 的做法从「driver-sql 的一个 describe」提升为矩阵的一条正式轴:
- spec 侧给
TemporalRow / TemporalTimeRow 加 storage-form 的判定依据(形态本身是各驱动物理层的事,spec 只需声明「这一行在遗留形态下应该长什么样」的语义,不下沉方言拼写)。
- 每个消费者加一个绕过写入路径、直接注入存储层的 describe,跑同一批
TEMPORAL_CASES / TEMPORAL_TIME_CASES。各驱动的注入手法不同(sql 走 seedLegacyRows,mongo 直接 collection.insertMany 塞字符串/数字,memory 在 schema 声明前塞 initialData),但断言的行 id 集合必须完全一致——这正是矩阵的意义。
- 优先级:mongo > Field.time(全后端)> memory 的 schema-到达收敛 > sqlite-wasm / formula / analytics。
分两步做也可以,第 1 步只做 mongo 的 datetime 就能把最尖的那格封住。
验收
遗留形态下的行 id 集合与 canonical 形态逐格相同。任何一格不同,都说明该后端的读取修复路径与共识有偏差——这是 #3773 / #3994 / #4047 三次事故共同缺的那个信号。
顺带记一笔(不在本 issue 范围)
同一句 ADR 还点名了 server-timezone 轴。SQL 侧已部分就位:Temporal Conformance job 的 Postgres 服务容器跑在 Asia/Shanghai,进程侧自 #4180 起五个非 SQL 包也在 America/New_York 下运行且有非空转守卫。但「服务器时区 ≠ 进程时区 ≠ UTC」这个三方错开的组合没有被系统化成矩阵的一条轴。若要立项建议单开。
关联
#3773 / #3777 / #3994 / #4047 / #3928;#4081(D-A3 本体,已关)、#4098、#4109、#4147、#4175、#4180;ADR-0053 D-A3 / D-B3 / D-C / D-E1 / D-E3 / D-E4。
D-A3 的矩阵已经立起来了(#4081 已关),但 ADR 为它点名的 storage-form 轴只落在一个格子里。
现状(已逐个核对,不是推测)
sql-driver-temporal-conformance.test.ts:107有一个un-backfilled legacy storage的 describe:用LegacyStorageDriver.seedLegacyRows绕过写入路径,把同一张表按 writer-form 标签分别注入 INTEGER epoch ms 和 zone-naive TEXT,然后跑全套TEMPORAL_CASES。这个做法是对的,问题是它只有一份。driver-sql(datetime):107driver-sql(Field.time):150driver-sqlite-wasmdriver-memorydriver-mongodbformulaservice-analytics(native-sql / preview)其余六个消费者全部只做
driver.create(...)播种,即wire | native两种形态都是今天的写入路径会产出的东西。也就是说这些夹具在构造上就不可能含有遗留形态——矩阵测的是「我写进去的我能读回来」,而四次事故里有两次的机理是读到自己没写过的形态。为什么 mongo 是最尖的一格
其他后端的遗留数据是过渡态,mongo 的是永久态,这是 ADR 明写的设计决定:
而 mongo 读错形态时的表现是所有后端里最坏的:
SQLite 至少还有一半能匹配上,mongo 是整段窗口静默返回错误行集(#4047 当时的表现是空集)。所以:唯一保证长期持有遗留形态的后端,恰好是唯一没有遗留形态测试的后端,且它的失败模式最安静。
driver-memory的情况稍好——D-E3 说它在 schema 到达时会收敛已有行(覆盖initialData夹具和持久化恢复,两者都发生在 schema 声明之前)——但这个收敛本身也没有被矩阵测过。为什么 Field.time 那一格必须补
#3994 的成因逐字就是这个:
TEMPORAL_TIME_ROWS里a_midnight的why也已经写着「epoch 0 on SQLite, which sorts before every TEXT row」。但:107的遗留扫描只跑TEMPORAL_CASES,Field.time的 describe(:150)没有对应的遗留版本。修复 #3994 的那条读取路径,今天没有任何遗留形态的回归锁。建议形状
把
LegacyStorageDriver的做法从「driver-sql 的一个 describe」提升为矩阵的一条正式轴:TemporalRow/TemporalTimeRow加 storage-form 的判定依据(形态本身是各驱动物理层的事,spec 只需声明「这一行在遗留形态下应该长什么样」的语义,不下沉方言拼写)。TEMPORAL_CASES/TEMPORAL_TIME_CASES。各驱动的注入手法不同(sql 走seedLegacyRows,mongo 直接collection.insertMany塞字符串/数字,memory 在 schema 声明前塞initialData),但断言的行 id 集合必须完全一致——这正是矩阵的意义。分两步做也可以,第 1 步只做 mongo 的 datetime 就能把最尖的那格封住。
验收
遗留形态下的行 id 集合与 canonical 形态逐格相同。任何一格不同,都说明该后端的读取修复路径与共识有偏差——这是 #3773 / #3994 / #4047 三次事故共同缺的那个信号。
顺带记一笔(不在本 issue 范围)
同一句 ADR 还点名了 server-timezone 轴。SQL 侧已部分就位:
Temporal Conformancejob 的 Postgres 服务容器跑在Asia/Shanghai,进程侧自 #4180 起五个非 SQL 包也在America/New_York下运行且有非空转守卫。但「服务器时区 ≠ 进程时区 ≠ UTC」这个三方错开的组合没有被系统化成矩阵的一条轴。若要立项建议单开。关联
#3773 / #3777 / #3994 / #4047 / #3928;#4081(D-A3 本体,已关)、#4098、#4109、#4147、#4175、#4180;ADR-0053 D-A3 / D-B3 / D-C / D-E1 / D-E3 / D-E4。