|
|
@@ -0,0 +1,135 @@
|
|
|
+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)
|
|
|
+ }
|
|
|
+}
|