harden(migrate): 抄送回填取切换时刻改 CAST(TEXT) —— PG 的 timestamptz 会被扫成 time.Time

判据只在 SQLite 上跑过:把 MIN(read_at) 扫进 sql.NullString 在 PG 上依赖驱动返回类型,
不可靠。改成 CAST(... AS TEXT) 再解析,并把 PG 的文本形态(+00 时区后缀)加进可识别的
layout 列表。认不出来时退回现在= 把当下已有的已读邮件全算作迁移前 —— 首次升级时
正是对的,且有一次标记守着不会反复跑。
This commit is contained in:
2026-09-13 16:22:00 +08:00
parent 2e5d84330b
commit 773acd079f

View File

@ -120,13 +120,21 @@ func switchoverAt(ctx context.Context) (time.Time, error) {
return time.Time{}, err
}
var raw sql.NullString
if err := DB.QueryRowContext(ctx, `SELECT MIN(read_at) FROM mail_reads`).Scan(&raw); err != nil {
// CAST 成文本再解析:两种方言的驱动对 timestamptz 返回的类型不一样
// PG 给 time.TimeSQLite 给字符串),文本形态才是共通的。
if err := DB.QueryRowContext(ctx, `SELECT CAST(MIN(read_at) AS TEXT) FROM mail_reads`).Scan(&raw); err != nil {
return time.Time{}, err
}
if !raw.Valid {
return time.Now().UTC(), nil // 表还是空的:本机没有"老邮件"要照顾
}
for _, layout := range []string{"2006-01-02 15:04:05.000", "2006-01-02 15:04:05", time.RFC3339} {
// 认不出来的形态退回"现在":那等于把当下已有的已读邮件全算作迁移前 ——
// 首次升级时正是对的;而且这条路有一次性标记守着,不会反复跑。
for _, layout := range []string{
"2006-01-02 15:04:05.000", "2006-01-02 15:04:05",
"2006-01-02 15:04:05.999999-07", "2006-01-02 15:04:05-07",
time.RFC3339,
} {
if t, perr := time.Parse(layout, raw.String); perr == nil {
return t.UTC(), nil
}