numeric_bool_test.go 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. package migrate
  2. // 裸 NUMERIC (GORM sqlite 方言的 bool 声明) 的迁移映射与数据探测。
  3. // MySQL 的 NUMERIC 是 DECIMAL 别名, 不在此列 (见 TestBareNumericInfer 的 mysql 断言)。
  4. import (
  5. "fmt"
  6. "path/filepath"
  7. "strings"
  8. "testing"
  9. "git.ycl.pub/yl32940/TsDB2/DB/core"
  10. "git.ycl.pub/yl32940/TsDB2/DB/core/types"
  11. )
  12. // buildSQLiteNumeric 建一个带裸 NUMERIC 列的 sqlite 源文件 (写完即关)。
  13. func buildSQLiteNumeric(t *testing.T, path, ddl string, rows []string) {
  14. t.Helper()
  15. src, err := OpenSQLite(path, quiet())
  16. if err != nil {
  17. t.Fatal(err)
  18. }
  19. defer src.Close()
  20. if _, err := src.db.Exec(ddl); err != nil {
  21. t.Fatalf("建表: %v", err)
  22. }
  23. for _, r := range rows {
  24. if _, err := src.db.Exec(r); err != nil {
  25. t.Fatalf("插行 %q: %v", r, err)
  26. }
  27. }
  28. }
  29. // runSQLiteToVRTX sqlite 源文件 → 全新 VRTX 目标, 返回目标快照。
  30. func runSQLiteToVRTX(t *testing.T, srcPath, dstPath, table string) vrtxSnapshot {
  31. t.Helper()
  32. src, err := OpenSQLite(srcPath, quiet())
  33. if err != nil {
  34. t.Fatal(err)
  35. }
  36. defer src.Close()
  37. dst, err := OpenVRTX(dstPath, quiet())
  38. if err != nil {
  39. t.Fatal(err)
  40. }
  41. if _, err := Run(src, dst, quiet()); err != nil {
  42. _ = dst.Close()
  43. t.Fatal(err)
  44. }
  45. // 先关句柄释放文件锁, 快照才能重新打开同一文件
  46. if err := dst.Close(); err != nil {
  47. t.Fatal(err)
  48. }
  49. return snapshotVRTX(t, dstPath, table)
  50. }
  51. // hasColumn 快照列清单里是否含 "name:type" 前缀条目。
  52. func hasColumn(snap vrtxSnapshot, prefix string) bool {
  53. for _, c := range snap.columns {
  54. if strings.HasPrefix(c, prefix) {
  55. return true
  56. }
  57. }
  58. return false
  59. }
  60. // TestBareNumericBool 裸 NUMERIC 全 0/1 → Bool 列, 值往返, 引擎侧写 Go bool 不拒。
  61. func TestBareNumericBool(t *testing.T) {
  62. dir := t.TempDir()
  63. srcPath := filepath.Join(dir, "src.sqlite")
  64. buildSQLiteNumeric(t, srcPath,
  65. `CREATE TABLE "flags" ("id" INTEGER PRIMARY KEY, "is_error" NUMERIC, "note" TEXT)`,
  66. []string{
  67. `INSERT INTO "flags" (id, is_error, note) VALUES (1, 0, 'a')`,
  68. `INSERT INTO "flags" (id, is_error, note) VALUES (2, 1, 'b')`,
  69. })
  70. dstPath := filepath.Join(dir, "dst.db")
  71. snap := runSQLiteToVRTX(t, srcPath, dstPath, "flags")
  72. if !hasColumn(snap, "is_error:bool") {
  73. t.Fatalf("is_error 应收成 bool 列, 快照: %v", snap.columns)
  74. }
  75. if v := fmt.Sprint(snap.rows[2]["is_error"]); v != "true" {
  76. t.Fatalf("is_error=1 应读回 true, 实际 %v (rows=%v)", v, snap.rows)
  77. }
  78. // 复刻 GORM 写 Go bool 的场景: 引擎侧直插不拒
  79. db, err := core.Open(dstPath)
  80. if err != nil {
  81. t.Fatal(err)
  82. }
  83. defer db.Close()
  84. tbl, _ := db.FindTable("flags")
  85. if _, err := tbl.InsertRowWithID(3, []types.FieldValue{
  86. {FieldName: []byte("id"), Value: uint64(3)},
  87. {FieldName: []byte("is_error"), Value: true},
  88. {FieldName: []byte("note"), Value: "c"},
  89. }); err != nil {
  90. t.Fatalf("Bool 列写 bool 被拒: %v", err)
  91. }
  92. }
  93. // TestBareNumericOutOfDomain 裸 NUMERIC 含域外值 (1.5) → 数据探测改回 Decimal。
  94. func TestBareNumericOutOfDomain(t *testing.T) {
  95. dir := t.TempDir()
  96. srcPath := filepath.Join(dir, "src.sqlite")
  97. buildSQLiteNumeric(t, srcPath,
  98. `CREATE TABLE "m" ("id" INTEGER PRIMARY KEY, "ratio" NUMERIC)`,
  99. []string{
  100. `INSERT INTO "m" (id, ratio) VALUES (1, 0)`,
  101. `INSERT INTO "m" (id, ratio) VALUES (2, 1)`,
  102. `INSERT INTO "m" (id, ratio) VALUES (3, 1.5)`,
  103. })
  104. dstPath := filepath.Join(dir, "dst.db")
  105. snap := runSQLiteToVRTX(t, srcPath, dstPath, "m")
  106. if !hasColumn(snap, "ratio:float64") {
  107. t.Fatalf("域外值应探测降级 float64, 快照: %v", snap.columns)
  108. }
  109. }
  110. // TestBareNumericInfer 单元: 裸 NUMERIC 的分方言映射。
  111. func TestBareNumericInfer(t *testing.T) {
  112. if ft, _ := inferSQLType("NUMERIC", false); ft != types.FieldType_Bool {
  113. t.Fatalf("sqlite 裸 NUMERIC 应收 Bool, 实际 %v", ft)
  114. }
  115. if ft, _ := inferSQLType("NUMERIC", true); ft != types.FieldType_Decimal {
  116. t.Fatalf("mysql 裸 NUMERIC 应收 Decimal, 实际 %v", ft)
  117. }
  118. if ft, _ := inferSQLType("NUMERIC(10,2)", false); ft != types.FieldType_Decimal {
  119. t.Fatalf("NUMERIC(10,2) 应收 Decimal, 实际 %v", ft)
  120. }
  121. if ft, _ := inferSQLType("DECIMAL", false); ft != types.FieldType_Decimal {
  122. t.Fatalf("裸 DECIMAL 应收 Decimal, 实际 %v", ft)
  123. }
  124. }