sqltype_test.go 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. package migrate
  2. import (
  3. "TsDB2/DB/core/types"
  4. "testing"
  5. )
  6. // ============================ SQL 类型 ↔ VRTX 类型 ============================
  7. //
  8. // 这一层是迁移的"翻译表", 出错的后果不是报错而是【静默改变列的语义】:
  9. // - BLOB 认成 TEXT → 二进制内容进了文本列;
  10. // - FLOAT 在 SQLite/MySQL 上按同一档收 → 单精度列被当成双精度 (或反过来);
  11. // - DECIMAL(18,2) 只读类型名不读 (p,s) → 小数位被抹平, 金额变成整数。
  12. //
  13. // 所以这里既钉住逐条映射, 也钉住"声明出去再读回来还是同一个类型"这个往返性质。
  14. func TestInferSQLType(t *testing.T) {
  15. cases := []struct {
  16. decl string
  17. mysql bool
  18. ft types.FieldType
  19. param byte
  20. }{
  21. // 整数: 有符号 / 无符号分档, SQLite 的 INTEGER 一律按 8 字节收
  22. {"TINYINT", true, types.FieldType_Int8, 0},
  23. {"SMALLINT", true, types.FieldType_Int16, 0},
  24. {"INT", true, types.FieldType_Int32, 0},
  25. {"MEDIUMINT", true, types.FieldType_Int32, 0},
  26. {"BIGINT", true, types.FieldType_Int64, 0},
  27. {"INTEGER", false, types.FieldType_Int64, 0},
  28. {"TINYINT UNSIGNED", true, types.FieldType_1byte, 0},
  29. {"SMALLINT UNSIGNED", true, types.FieldType_2byte, 0},
  30. {"INT UNSIGNED", true, types.FieldType_4byte, 0},
  31. {"BIGINT UNSIGNED", true, types.FieldType_8byte, 0},
  32. // 浮点: MySQL 的 FLOAT 就是 4 字节, SQLite 只有 REAL 亲和性 (8 字节)
  33. {"FLOAT", true, types.FieldType_Float32, 0},
  34. {"FLOAT", false, types.FieldType_Float64, 0},
  35. {"DOUBLE", true, types.FieldType_Float64, 0},
  36. {"REAL", false, types.FieldType_Float64, 0},
  37. // 定点: (p,s) 必须读进来 —— 只读类型名会把小数位抹平
  38. {"DECIMAL(18,2)", true, types.FieldType_Decimal, 2},
  39. {"DECIMAL(10,0)", true, types.FieldType_Decimal, 0},
  40. {"NUMERIC(20,6)", false, types.FieldType_Decimal, 6},
  41. // 超出 128 位系数容量 → 退回 Float64 (与改动前一致), 不假装精确
  42. {"DECIMAL(65,30)", true, types.FieldType_Float64, 0},
  43. // 其余新类型
  44. {"BOOLEAN", false, types.FieldType_Bool, 0},
  45. {"BOOL", true, types.FieldType_Bool, 0},
  46. {"YEAR", true, types.FieldType_Year, 0},
  47. {"UUID", true, types.FieldType_UUID, 0},
  48. // 二进制大对象 vs 文本: 这是本次新增的区分
  49. {"BLOB", false, types.FieldType_Blob, 0},
  50. {"TINYBLOB", true, types.FieldType_Blob, 0},
  51. {"MEDIUMBLOB", true, types.FieldType_Blob, 0},
  52. {"LONGBLOB", true, types.FieldType_Blob, 0},
  53. {"VARBINARY(64)", true, types.FieldType_Blob, 0},
  54. {"TEXT", false, types.FieldType_Dynamicbyte, 0},
  55. {"LONGTEXT", true, types.FieldType_Dynamicbyte, 0},
  56. // 定长串按长度就近取档
  57. {"VARCHAR(8)", true, types.FieldType_16byte, 0},
  58. {"VARCHAR(200)", true, types.FieldType_256byte, 0},
  59. {"CHAR(32)", true, types.FieldType_32byte, 0},
  60. // 时间列落 8byte (Unix 纳秒), 不是动态列
  61. {"DATETIME", true, types.FieldType_8byte, 0},
  62. {"DATE", false, types.FieldType_8byte, 0},
  63. }
  64. for _, c := range cases {
  65. ft, param := inferSQLType(c.decl, c.mysql)
  66. if ft != c.ft || param != c.param {
  67. t.Errorf("inferSQLType(%q, mysql=%v) = (%v, %d), 期望 (%v, %d)",
  68. c.decl, c.mysql, ft, param, c.ft, c.param)
  69. }
  70. }
  71. }
  72. // TestMySQLColumnDecl 钉住"字符列的容量按字节算"这条规则 (information_schema 里
  73. // CHARACTER_OCTET_LENGTH 那一列)。
  74. //
  75. // 这条是真实踩出来的: resource_info.resAlias 是 varchar(50) utf8mb4, 库里存了 73 字节
  76. // 的中文别名。老代码拿 50 (字符数) 去取档得到 64byte, 整表迁移失败 —— 而 50 个字符在
  77. // utf8mb4 下最多是 200 字节, 只有拿 200 去取档才装得下。
  78. func TestMySQLColumnDecl(t *testing.T) {
  79. cases := []struct {
  80. name string
  81. dataType string
  82. // information_schema 给的三组长度: 字节容量 / 精度 / 小数位
  83. charBytes, numPrec, numScale int64
  84. want types.FieldType
  85. wantParam byte
  86. }{
  87. // varchar(50) utf8mb4 → octet 200 → 256byte (老算法 50 → 64byte, 装不下中文)
  88. {"utf8mb4 varchar(50)", "varchar", 200, 0, 0, types.FieldType_256byte, 0},
  89. // varchar(12) utf8mb4 → octet 48 → 64byte
  90. {"utf8mb4 varchar(12)", "varchar", 48, 0, 0, types.FieldType_64byte, 0},
  91. // 单字节字符集不背 4 倍: latin1 的 varchar(50) 就是 50 字节
  92. {"latin1 varchar(50)", "varchar", 50, 0, 0, types.FieldType_64byte, 0},
  93. // 正好卡在档位边界上: varchar(64) utf8mb4 = 256 字节, 用满最后一档
  94. {"utf8mb4 varchar(64)", "varchar", 256, 0, 0, types.FieldType_256byte, 0},
  95. // 再多一个字符就超出所有定长档: 宁可变长也不开窄列 (变长只是没索引, 不会丢数据)
  96. {"utf8mb4 varchar(65)", "varchar", 260, 0, 0, types.FieldType_Dynamicbyte, 0},
  97. {"utf8mb4 char(1)", "char", 4, 0, 0, types.FieldType_16byte, 0},
  98. {"utf8mb4 text", "text", 65535, 0, 0, types.FieldType_Dynamicbyte, 0},
  99. {"blob", "blob", 65535, 0, 0, types.FieldType_Blob, 0},
  100. // 定点: (p,s) 要读进来, 小数位不能被抹平
  101. {"decimal(18,2)", "decimal", 20, 18, 2, types.FieldType_Decimal, 2},
  102. // 长度信息为 0 (数值/时间列没有 octet length) 时退回纯类型名
  103. {"int", "int", 0, 10, 0, types.FieldType_Int32, 0},
  104. {"datetime", "datetime", 0, 0, 0, types.FieldType_8byte, 0},
  105. }
  106. for _, c := range cases {
  107. decl := mysqlColumnDecl(c.dataType, c.charBytes, c.numPrec, c.numScale)
  108. ft, param := inferSQLType(decl, true)
  109. if ft != c.want || param != c.wantParam {
  110. t.Errorf("%s: %q → (%v, %d), 期望 (%v, %d)",
  111. c.name, decl, ft, param, c.want, c.wantParam)
  112. }
  113. }
  114. }
  115. // TestSQLTypeRoundTrip 声明出去的类型名必须能被自己读回同一个类型。
  116. //
  117. // 例外的几种都【不依赖 meta 就得靠宽度/语义兜底】, 这里显式列出来当文档:
  118. // - 窄整数在 SQLite 只有 INTEGER 一种写法, 读回一律 Int64;
  119. // - Decimal 在 SQLite 没有原生类型 (落 TEXT), 读回是 Dynamicbyte;
  120. // - UUID 在 MySQL 用 BINARY(16) 存, 读回被认成 Blob;
  121. // - Blob 与 Dynamicbyte 盘上同为动态列, 靠 meta 才能区分到具体哪一种。
  122. //
  123. // 也就是说: 需要"精确类型"的往返都依赖 __vrtx_meta (默认开启); 没有 meta 时
  124. // 至少保证["数据不丢、语义不离谱"]。
  125. func TestSQLTypeRoundTrip(t *testing.T) {
  126. type rt struct {
  127. ft types.FieldType
  128. param byte
  129. }
  130. for _, mysql := range []bool{true, false} {
  131. // 这些类型在两个方言下都能原样往返
  132. exact := []rt{
  133. {types.FieldType_Blob, 0},
  134. {types.FieldType_Bool, 0},
  135. {types.FieldType_Year, 0},
  136. {types.FieldType_Float64, 0},
  137. {types.FieldType_Int64, 0},
  138. }
  139. if mysql {
  140. exact = append(exact,
  141. rt{types.FieldType_Int8, 0},
  142. rt{types.FieldType_Int16, 0},
  143. rt{types.FieldType_Int32, 0},
  144. rt{types.FieldType_1byte, 0},
  145. rt{types.FieldType_2byte, 0},
  146. rt{types.FieldType_4byte, 0},
  147. rt{types.FieldType_8byte, 0},
  148. rt{types.FieldType_Float32, 0},
  149. rt{types.FieldType_Decimal, 2},
  150. )
  151. }
  152. // 定长串按长度取档, 档位本身就是 16/32/64/128/256, 所以能精确往返
  153. for _, ft := range []types.FieldType{
  154. types.FieldType_16byte, types.FieldType_32byte,
  155. types.FieldType_64byte, types.FieldType_128byte, types.FieldType_256byte,
  156. } {
  157. exact = append(exact, rt{ft, 0})
  158. }
  159. for _, e := range exact {
  160. decl := sqlTypeName(e.ft, e.param, mysql)
  161. ft, param := inferSQLType(decl, mysql)
  162. if ft != e.ft || param != e.param {
  163. t.Errorf("mysql=%v: %v → %q → (%v, %d), 期望 (%v, %d)",
  164. mysql, e.ft, decl, ft, param, e.ft, e.param)
  165. }
  166. }
  167. }
  168. }