package migrate // 裸 NUMERIC (GORM sqlite 方言的 bool 声明) 的迁移映射与数据探测。 // MySQL 的 NUMERIC 是 DECIMAL 别名, 不在此列 (见 TestBareNumericInfer 的 mysql 断言)。 import ( "fmt" "path/filepath" "strings" "testing" "git.ycl.pub/yl32940/TsDB2/DB/core" "git.ycl.pub/yl32940/TsDB2/DB/core/types" ) // buildSQLiteNumeric 建一个带裸 NUMERIC 列的 sqlite 源文件 (写完即关)。 func buildSQLiteNumeric(t *testing.T, path, ddl string, rows []string) { t.Helper() src, err := OpenSQLite(path, quiet()) if err != nil { t.Fatal(err) } defer src.Close() if _, err := src.db.Exec(ddl); err != nil { t.Fatalf("建表: %v", err) } for _, r := range rows { if _, err := src.db.Exec(r); err != nil { t.Fatalf("插行 %q: %v", r, err) } } } // runSQLiteToVRTX sqlite 源文件 → 全新 VRTX 目标, 返回目标快照。 func runSQLiteToVRTX(t *testing.T, srcPath, dstPath, table string) vrtxSnapshot { t.Helper() src, err := OpenSQLite(srcPath, quiet()) if err != nil { t.Fatal(err) } defer src.Close() dst, err := OpenVRTX(dstPath, quiet()) if err != nil { t.Fatal(err) } if _, err := Run(src, dst, quiet()); err != nil { _ = dst.Close() t.Fatal(err) } // 先关句柄释放文件锁, 快照才能重新打开同一文件 if err := dst.Close(); err != nil { t.Fatal(err) } return snapshotVRTX(t, dstPath, table) } // hasColumn 快照列清单里是否含 "name:type" 前缀条目。 func hasColumn(snap vrtxSnapshot, prefix string) bool { for _, c := range snap.columns { if strings.HasPrefix(c, prefix) { return true } } return false } // TestBareNumericBool 裸 NUMERIC 全 0/1 → Bool 列, 值往返, 引擎侧写 Go bool 不拒。 func TestBareNumericBool(t *testing.T) { dir := t.TempDir() srcPath := filepath.Join(dir, "src.sqlite") buildSQLiteNumeric(t, srcPath, `CREATE TABLE "flags" ("id" INTEGER PRIMARY KEY, "is_error" NUMERIC, "note" TEXT)`, []string{ `INSERT INTO "flags" (id, is_error, note) VALUES (1, 0, 'a')`, `INSERT INTO "flags" (id, is_error, note) VALUES (2, 1, 'b')`, }) dstPath := filepath.Join(dir, "dst.db") snap := runSQLiteToVRTX(t, srcPath, dstPath, "flags") if !hasColumn(snap, "is_error:bool") { t.Fatalf("is_error 应收成 bool 列, 快照: %v", snap.columns) } if v := fmt.Sprint(snap.rows[2]["is_error"]); v != "true" { t.Fatalf("is_error=1 应读回 true, 实际 %v (rows=%v)", v, snap.rows) } // 复刻 GORM 写 Go bool 的场景: 引擎侧直插不拒 db, err := core.Open(dstPath) if err != nil { t.Fatal(err) } defer db.Close() tbl, _ := db.FindTable("flags") if _, err := tbl.InsertRowWithID(3, []types.FieldValue{ {FieldName: []byte("id"), Value: uint64(3)}, {FieldName: []byte("is_error"), Value: true}, {FieldName: []byte("note"), Value: "c"}, }); err != nil { t.Fatalf("Bool 列写 bool 被拒: %v", err) } } // TestBareNumericOutOfDomain 裸 NUMERIC 含域外值 (1.5) → 数据探测改回 Decimal。 func TestBareNumericOutOfDomain(t *testing.T) { dir := t.TempDir() srcPath := filepath.Join(dir, "src.sqlite") buildSQLiteNumeric(t, srcPath, `CREATE TABLE "m" ("id" INTEGER PRIMARY KEY, "ratio" NUMERIC)`, []string{ `INSERT INTO "m" (id, ratio) VALUES (1, 0)`, `INSERT INTO "m" (id, ratio) VALUES (2, 1)`, `INSERT INTO "m" (id, ratio) VALUES (3, 1.5)`, }) dstPath := filepath.Join(dir, "dst.db") snap := runSQLiteToVRTX(t, srcPath, dstPath, "m") if !hasColumn(snap, "ratio:float64") { t.Fatalf("域外值应探测降级 float64, 快照: %v", snap.columns) } } // TestBareNumericInfer 单元: 裸 NUMERIC 的分方言映射。 func TestBareNumericInfer(t *testing.T) { if ft, _ := inferSQLType("NUMERIC", false); ft != types.FieldType_Bool { t.Fatalf("sqlite 裸 NUMERIC 应收 Bool, 实际 %v", ft) } if ft, _ := inferSQLType("NUMERIC", true); ft != types.FieldType_Decimal { t.Fatalf("mysql 裸 NUMERIC 应收 Decimal, 实际 %v", ft) } if ft, _ := inferSQLType("NUMERIC(10,2)", false); ft != types.FieldType_Decimal { t.Fatalf("NUMERIC(10,2) 应收 Decimal, 实际 %v", ft) } if ft, _ := inferSQLType("DECIMAL", false); ft != types.FieldType_Decimal { t.Fatalf("裸 DECIMAL 应收 Decimal, 实际 %v", ft) } }