|
|
@@ -449,12 +449,13 @@ func TestSQLiteOriginToVRTX(t *testing.T) {
|
|
|
//
|
|
|
// 三条硬要求, 每一条都对应一个真实踩过的坑:
|
|
|
//
|
|
|
-// 1. 落 uint64 —— VRTX 没有日期类型, 时间就是 8 字节整数 (时序键 _ts 同款),
|
|
|
-// 只有这样时间列才能比较大小、做范围查询;
|
|
|
+// 1. 落引擎的专用时间类型 FieldType_DateTime (行内 8 字节 Unix 毫秒, 与 gormvrtx /
|
|
|
+// SQL driver 同一约定) —— 迁过来的表经 ORM 读 created_at 时列类型才是 datetime
|
|
|
+// (扫描类型 time.Time), 不会出现"列是 uint64、却要按时间读"的类型错误;
|
|
|
// 2. 建在时间列上的索引必须真的建出来。时间列以前落在 dynamic 上, 引擎会拒
|
|
|
// "动态列不能作为索引键", 整张表跟着废掉 (resource_online.idx_ctime 那个报错);
|
|
|
-// 3. 值要是时间戳本身, 且按 UTC 解释 —— 同一份数据在不同时区的机器上必须迁出
|
|
|
-// 同一个数。
|
|
|
+// 3. 值要是时间戳本身 (毫秒), 且按 UTC 解释 —— 同一份数据在不同时区的机器上
|
|
|
+// 必须迁出同一个数。
|
|
|
func TestSQLiteTimeColumnsToVRTX(t *testing.T) {
|
|
|
dir := t.TempDir()
|
|
|
srcPath := filepath.Join(dir, "src.sqlite")
|
|
|
@@ -471,7 +472,7 @@ func TestSQLiteTimeColumnsToVRTX(t *testing.T) {
|
|
|
day DATE NOT NULL
|
|
|
)`,
|
|
|
`CREATE INDEX idx_Events_ctime ON Events(ctime)`,
|
|
|
- `INSERT INTO Events (id, ctime, day) VALUES (1, '2024-03-05 12:34:56', '2024-03-05')`,
|
|
|
+ `INSERT INTO Events (id, ctime, day) VALUES (1, '2024-03-05 12:34:56.123', '2024-03-05')`,
|
|
|
} {
|
|
|
if _, err := db.Exec(s); err != nil {
|
|
|
t.Fatalf("执行 %s: %v", s, err)
|
|
|
@@ -499,30 +500,184 @@ func TestSQLiteTimeColumnsToVRTX(t *testing.T) {
|
|
|
|
|
|
snap := snapshotVRTX(t, midPath, "Events")
|
|
|
|
|
|
- // 1. 时间列是 uint64, 不是 dynamic
|
|
|
+ // 1. 时间列是引擎专用 datetime 类型, 不是 dynamic / 普通 uint64
|
|
|
cols := strings.Join(snap.columns, ",")
|
|
|
- for _, want := range []string{"ctime:uint64", "day:uint64"} {
|
|
|
+ for _, want := range []string{"ctime:datetime", "day:datetime"} {
|
|
|
if !strings.Contains(cols, want) {
|
|
|
t.Errorf("时间列应当落成 %s, 实际列结构 %v", want, snap.columns)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // 1b. 直接查引擎 meta: 列的 FieldType 必须是 FieldType_DateTime —— gormvrtx 的
|
|
|
+ // 扫描类型映射 (scanTypeOf) 正是按这个值给出 time.Time, 这是"ORM 读 created_at
|
|
|
+ // 不报类型错误"的确切依据, 不靠 migrate 自己的读端自证。
|
|
|
+ vdb, err := core.Open(midPath)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ vtbl, ok := vdb.FindTable("Events")
|
|
|
+ if !ok {
|
|
|
+ t.Fatal("VRTX 里没有 Events 表")
|
|
|
+ }
|
|
|
+ wantFT := map[string]types.FieldType{"ctime": types.FieldType_DateTime, "day": types.FieldType_DateTime}
|
|
|
+ for _, f := range vtbl.GetFields() {
|
|
|
+ name := util.BytesToString(f.FieldName)
|
|
|
+ if want, ok := wantFT[name]; ok && f.FieldType != want {
|
|
|
+ t.Errorf("列 %s 的 FieldType = %v, 期望 %v (gormvrtx 会因此给错扫描类型)", name, f.FieldType, want)
|
|
|
+ }
|
|
|
+ delete(wantFT, name)
|
|
|
+ }
|
|
|
+ if len(wantFT) != 0 {
|
|
|
+ t.Errorf("Events 表缺列: %v", wantFT)
|
|
|
+ }
|
|
|
+ _ = vdb.Close()
|
|
|
+
|
|
|
// 2. 索引建出来了 (以前这里整张表都废掉)
|
|
|
if got := snap.indexes["idx_Events_ctime"]; got != "btree(ctime)" {
|
|
|
t.Errorf("建在时间列上的索引没建出来, 实际 %v", snap.indexes)
|
|
|
}
|
|
|
|
|
|
- // 3. 值是 UTC 时间戳本身。数字写死是为了不让实现自证自己:
|
|
|
- // 2024-03-05 12:34:56 UTC = 1709642096 秒; 2024-03-05 = 1709596800 秒。
|
|
|
- for col, wantNS := range map[string]uint64{
|
|
|
- "ctime": 1709642096000000000,
|
|
|
- "day": 1709596800000000000,
|
|
|
+ // 3. 值是 UTC 毫秒时间戳本身。数字写死是为了不让实现自证自己:
|
|
|
+ // 2024-03-05 12:34:56.123 UTC = 1709642096123 毫秒; 2024-03-05 = 1709596800000 毫秒。
|
|
|
+ for col, wantMS := range map[string]uint64{
|
|
|
+ "ctime": 1709642096123,
|
|
|
+ "day": 1709596800000,
|
|
|
} {
|
|
|
got := snap.rows[1][col]
|
|
|
- if s := fmt.Sprintf("%v", got); s != strconv.FormatUint(wantNS, 10) {
|
|
|
- t.Errorf("%s 的 Unix 纳秒应为 %d, 实际 %v (%T)", col, wantNS, got, got)
|
|
|
+ if s := fmt.Sprintf("%v", got); s != strconv.FormatUint(wantMS, 10) {
|
|
|
+ t.Errorf("%s 的 Unix 毫秒应为 %d, 实际 %v (%T)", col, wantMS, got, got)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 再迁回 SQLite, 时间文本必须逐字符回来 (毫秒无损)。
|
|
|
+ backPath := filepath.Join(dir, "back.sqlite")
|
|
|
+ v2, err := OpenVRTX(midPath, quiet())
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ s2, err := OpenSQLite(backPath, quiet())
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ if _, err := Run(v2, s2, quiet()); err != nil {
|
|
|
+ t.Fatalf("VRTX→SQLite: %v", err)
|
|
|
+ }
|
|
|
+ _ = v2.Close()
|
|
|
+ _ = s2.Close()
|
|
|
+
|
|
|
+ bdb, err := sql.Open("sqlite", backPath)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ var ctime, day string
|
|
|
+ // CAST 成 TEXT 读原样存储值: modernc 驱动会把 DATETIME 声明列里的时间文本
|
|
|
+ // 读成 time.Time, 再 Scan 进 string 时又格式化回 RFC3339 —— 那是驱动的往返
|
|
|
+ // 转换, 不是迁移写错了文本。这里要验的是"落库的文本逐字符无损"。
|
|
|
+ if err := bdb.QueryRow(`SELECT CAST(ctime AS TEXT), CAST(day AS TEXT) FROM Events WHERE id = 1`).
|
|
|
+ Scan(&ctime, &day); err != nil {
|
|
|
+ t.Fatalf("读回 SQLite: %v", err)
|
|
|
+ }
|
|
|
+ _ = bdb.Close()
|
|
|
+ if ctime != "2024-03-05 12:34:56.123" {
|
|
|
+ t.Errorf("ctime 应逐字符回来, 实际 %q", ctime)
|
|
|
+ }
|
|
|
+ // day 走到 VRTX 再直出, DATE 的细分声明丢失 (VRTX schema 没有放 TimeKind 的位置,
|
|
|
+ // 导出只能按 DATETIME 兜底声明, 见 types.go 的 TimeKind 注释) —— 但值必须无损:
|
|
|
+ // 仍是那一天的零点, 不是日期被砍或时间被挪。
|
|
|
+ if day != "2024-03-05 00:00:00" {
|
|
|
+ t.Errorf("day 的值应无损回来 (DATETIME 兜底声明), 实际 %q", day)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// TestLegacyUint64NanoTimeColumns 兼容路径: 存量旧表 (Uint64 列存 Unix 纳秒) 经 migrate
|
|
|
+// 读出仍按【纳秒】解释, 不能被新逻辑当成毫秒。
|
|
|
+//
|
|
|
+// 场景还原: 更早版本的 migrate 把 SQL 时间列迁成 Uint64 存纳秒, 并在 __vrtx_meta 里
|
|
|
+// 给该列打上 TimeKind 标记。这样的中间库今天再迁, 结构按 meta 还原成 Uint64+TimeKind
|
|
|
+// (写路径照旧换算成纳秒), 值必须是原来的那个纳秒数 —— 走错路会静默给出 1970 年附近
|
|
|
+// 的垃圾时间 (毫秒当纳秒解释) 或天文数字 (纳秒当毫秒解释)。
|
|
|
+func TestLegacyUint64NanoTimeColumns(t *testing.T) {
|
|
|
+ dir := t.TempDir()
|
|
|
+ srcPath := filepath.Join(dir, "src.sqlite")
|
|
|
+ dstPath := filepath.Join(dir, "dst.db")
|
|
|
+
|
|
|
+ db, err := sql.Open("sqlite", srcPath)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ for _, s := range []string{
|
|
|
+ `CREATE TABLE Events (
|
|
|
+ id INTEGER PRIMARY KEY,
|
|
|
+ ctime DATETIME NOT NULL
|
|
|
+ )`,
|
|
|
+ `INSERT INTO Events (id, ctime) VALUES (1, '2024-03-05 12:34:56.123456')`,
|
|
|
+ // 旧版 migrate 写出的保真表: ctime 的类型记成 Uint64 (10) + TimeKind 标记。
|
|
|
+ // 这正是存量中间库的样子 —— 新实现读到它, 列必须还原成 Uint64+TimeKind。
|
|
|
+ `CREATE TABLE __vrtx_meta (tbl TEXT PRIMARY KEY, payload TEXT NOT NULL)`,
|
|
|
+ `INSERT INTO __vrtx_meta (tbl, payload) VALUES ('Events',
|
|
|
+ '{"v":1,"pk":"id","pkt":2,"cols":[{"n":"id","t":11},{"n":"ctime","t":10,"tm":"datetime"}]}')`,
|
|
|
+ } {
|
|
|
+ if _, err := db.Exec(s); err != nil {
|
|
|
+ t.Fatalf("执行 %s: %v", s, err)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ _ = db.Close()
|
|
|
+
|
|
|
+ // 结构按旧 meta 还原: ctime 是 Uint64 + TimeKind, 不是 DateTime。
|
|
|
+ src, err := OpenSQLite(srcPath, quiet())
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ sc, err := src.Schema("Events")
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("读结构: %v", err)
|
|
|
+ }
|
|
|
+ var ctimeCol *Column
|
|
|
+ for i := range sc.Columns {
|
|
|
+ if sc.Columns[i].Name == "ctime" {
|
|
|
+ ctimeCol = &sc.Columns[i]
|
|
|
}
|
|
|
}
|
|
|
+ if ctimeCol == nil {
|
|
|
+ t.Fatal("Events 表没有 ctime 列")
|
|
|
+ }
|
|
|
+ if ctimeCol.Type != types.FieldType_Uint64 || ctimeCol.TimeKind != "datetime" {
|
|
|
+ t.Fatalf("旧 meta 应把 ctime 还原成 Uint64+TimeKind, 实际 %v (tm=%q)", ctimeCol.Type, ctimeCol.TimeKind)
|
|
|
+ }
|
|
|
+
|
|
|
+ dst, err := OpenVRTX(dstPath, quiet())
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ if _, err := Run(src, dst, quiet()); err != nil {
|
|
|
+ t.Fatalf("SQLite→VRTX: %v", err)
|
|
|
+ }
|
|
|
+ _ = src.Close()
|
|
|
+ _ = dst.Close()
|
|
|
+
|
|
|
+ // 写路径按旧盘面换算: 时间文本 → Unix 纳秒, 落进 Uint64 列。
|
|
|
+ // 1709642096123456000 纳秒 = 2024-03-05 12:34:56.123456 UTC。
|
|
|
+ snap := snapshotVRTX(t, dstPath, "Events")
|
|
|
+ if got := fmt.Sprintf("%v", snap.rows[1]["ctime"]); got != "1709642096123456000" {
|
|
|
+ t.Errorf("旧 Uint64 时间列应按纳秒落值, 实际 %v (%T)",
|
|
|
+ snap.rows[1]["ctime"], snap.rows[1]["ctime"])
|
|
|
+ }
|
|
|
+ cols := strings.Join(snap.columns, ",")
|
|
|
+ if !strings.Contains(cols, "ctime:uint64") {
|
|
|
+ t.Errorf("按旧 meta 建的列应是 uint64, 实际列结构 %v", snap.columns)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 读路径 (SQL 侧) 同样按纳秒还原: 从这张旧 VRTX 表经 SQL 导出到带旧 meta 的库,
|
|
|
+ // 时间文本必须原样回来。这里直接钉 coerceForSQL 的 Uint64+TimeKind 分支:
|
|
|
+ // 纳秒进、微秒文本出 (MySQL DATETIME(6) 的上限), 日期语义不丢。
|
|
|
+ legacyCol := Column{Name: "ctime", Type: types.FieldType_Uint64, TimeKind: "datetime"}
|
|
|
+ got, err := coerceForSQL(legacyCol, snap.rows[1]["ctime"], false)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("旧列导出 SQL 不该报错: %v", err)
|
|
|
+ }
|
|
|
+ if s, ok := got.(string); !ok || s != "2024-03-05 12:34:56.123456" {
|
|
|
+ t.Errorf("旧 Uint64 时间列应按纳秒还原成时间文本, 实际 %v (%T)", got, got)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// TestSkippedIndexDoesNotKillTable 目标端建不出来的索引应当只跳它自己。
|