sqltype_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. package migrate
  2. import (
  3. "TsDB2/DB/core/types"
  4. "fmt"
  5. "strconv"
  6. "strings"
  7. "testing"
  8. )
  9. // ============================ SQL 类型 ↔ VRTX 类型 ============================
  10. //
  11. // 这一层是迁移的"翻译表", 出错的后果不是报错而是【静默改变列的语义】:
  12. // - BLOB 认成 TEXT → 二进制内容进了文本列;
  13. // - FLOAT 在 SQLite/MySQL 上按同一档收 → 单精度列被当成双精度 (或反过来);
  14. // - DECIMAL(18,2) 只读类型名不读 (p,s) → 小数位被抹平, 金额变成整数。
  15. //
  16. // 所以这里既钉住逐条映射, 也钉住"声明出去再读回来还是同一个类型"这个往返性质。
  17. func TestInferSQLType(t *testing.T) {
  18. cases := []struct {
  19. decl string
  20. mysql bool
  21. ft types.FieldType
  22. param byte
  23. }{
  24. // 整数: 有符号 / 无符号分档, SQLite 的 INTEGER 一律按 8 字节收
  25. {"TINYINT", true, types.FieldType_Int8, 0},
  26. {"SMALLINT", true, types.FieldType_Int16, 0},
  27. {"INT", true, types.FieldType_Int32, 0},
  28. {"MEDIUMINT", true, types.FieldType_Int32, 0},
  29. {"BIGINT", true, types.FieldType_Int64, 0},
  30. {"INTEGER", false, types.FieldType_Int64, 0},
  31. {"TINYINT UNSIGNED", true, types.FieldType_1byte, 0},
  32. {"SMALLINT UNSIGNED", true, types.FieldType_2byte, 0},
  33. {"INT UNSIGNED", true, types.FieldType_4byte, 0},
  34. {"BIGINT UNSIGNED", true, types.FieldType_8byte, 0},
  35. // 浮点: MySQL 的 FLOAT 就是 4 字节, SQLite 只有 REAL 亲和性 (8 字节)
  36. {"FLOAT", true, types.FieldType_Float32, 0},
  37. {"FLOAT", false, types.FieldType_Float64, 0},
  38. {"DOUBLE", true, types.FieldType_Float64, 0},
  39. {"REAL", false, types.FieldType_Float64, 0},
  40. // 定点: (p,s) 必须读进来 —— 只读类型名会把小数位抹平
  41. {"DECIMAL(18,2)", true, types.FieldType_Decimal, 2},
  42. {"DECIMAL(10,0)", true, types.FieldType_Decimal, 0},
  43. {"NUMERIC(20,6)", false, types.FieldType_Decimal, 6},
  44. // p 与 s 是【两个不同的上界】: p ≤ 38 (128 位系数的十进制容量),
  45. // s ≤ 31 (Flag 低 5 位)。以前拿 31 去卡 p, 于是 DECIMAL(38,2) 被误判成
  46. // "表达不了"而退成 float64 —— 那是被写错的上限限制的, 不是被能力限制的。
  47. {"DECIMAL(38,2)", true, types.FieldType_Decimal, 2},
  48. {"DECIMAL(38,31)", true, types.FieldType_Decimal, 31},
  49. // 真的超出系数容量 → 退回 Float64 (与改动前一致), 不假装精确; 但要报出来 (degradeNote)
  50. {"DECIMAL(65,30)", true, types.FieldType_Float64, 0},
  51. {"DECIMAL(39,0)", true, types.FieldType_Float64, 0},
  52. // s 超 31: 系数装得下, 但小数位无处表达
  53. {"DECIMAL(38,35)", true, types.FieldType_Float64, 0},
  54. // 其余新类型
  55. {"BOOLEAN", false, types.FieldType_Bool, 0},
  56. {"BOOL", true, types.FieldType_Bool, 0},
  57. {"YEAR", true, types.FieldType_Year, 0},
  58. {"UUID", true, types.FieldType_UUID, 0},
  59. // 二进制大对象 vs 文本: 这是本次新增的区分
  60. {"BLOB", false, types.FieldType_Blob, 0},
  61. {"TINYBLOB", true, types.FieldType_Blob, 0},
  62. {"MEDIUMBLOB", true, types.FieldType_Blob, 0},
  63. {"LONGBLOB", true, types.FieldType_Blob, 0},
  64. {"VARBINARY(64)", true, types.FieldType_Blob, 0},
  65. {"TEXT", false, types.FieldType_Dynamicbyte, 0},
  66. {"LONGTEXT", true, types.FieldType_Dynamicbyte, 0},
  67. // 定长串: 按字节容量【向上】取宽度档, 档位号写进列级参数 (Flag 低 5 位)。
  68. // 返回的 param 不再是 0 —— 它就是"这一列多宽"的载体 (见 types.FieldSize)。
  69. {"VARCHAR(8)", true, types.FieldType_16byte, types.WidthCodeFor(8)},
  70. {"VARCHAR(200)", true, types.FieldType_256byte, types.WidthCodeFor(200)},
  71. {"CHAR(32)", true, types.FieldType_32byte, types.WidthCodeFor(32)},
  72. // 超过旧的 256 字节天花板仍可以定长: 2048 以内都保得住"可索引"
  73. {"VARCHAR(1000)", true, types.FieldType_256byte, types.WidthCodeFor(1000)},
  74. // 只有超过 MaxInlineColumnWidth 才落变长列
  75. {"VARCHAR(4000)", true, types.FieldType_Dynamicbyte, 0},
  76. // 时间列落 8byte (Unix 纳秒), 不是动态列
  77. {"DATETIME", true, types.FieldType_8byte, 0},
  78. {"DATE", false, types.FieldType_8byte, 0},
  79. }
  80. for _, c := range cases {
  81. ft, param := inferSQLType(c.decl, c.mysql)
  82. if ft != c.ft || param != c.param {
  83. t.Errorf("inferSQLType(%q, mysql=%v) = (%v, %d), 期望 (%v, %d)",
  84. c.decl, c.mysql, ft, param, c.ft, c.param)
  85. }
  86. }
  87. }
  88. // TestMySQLColumnDecl 钉住"字符列的容量按字节算"这条规则 (information_schema 里
  89. // CHARACTER_OCTET_LENGTH 那一列)。
  90. //
  91. // 这条是真实踩出来的: resource_info.resAlias 是 varchar(50) utf8mb4, 库里存了 73 字节
  92. // 的中文别名。老代码拿 50 (字符数) 去取档得到 64byte, 整表迁移失败 —— 而 50 个字符在
  93. // utf8mb4 下最多是 200 字节, 只有拿 200 去取档才装得下。
  94. func TestMySQLColumnDecl(t *testing.T) {
  95. cases := []struct {
  96. name string
  97. dataType string
  98. // information_schema 给的三组长度: 字节容量 / 精度 / 小数位
  99. charBytes, numPrec, numScale int64
  100. want types.FieldType
  101. wantParam byte
  102. }{
  103. // varchar(50) utf8mb4 → octet 200 → 224 字节档 (老算法 50 → 64byte 装不下中文;
  104. // 中间的过渡算法给 256 档, 现在能按声明精确到 224)
  105. {"utf8mb4 varchar(50)", "varchar", 200, 0, 0, types.FieldType_256byte, types.WidthCodeFor(200)},
  106. // varchar(12) utf8mb4 → octet 48 → 48 字节档
  107. {"utf8mb4 varchar(12)", "varchar", 48, 0, 0, types.FieldType_64byte, types.WidthCodeFor(48)},
  108. // 单字节字符集不背 4 倍: latin1 的 varchar(50) 就是 50 字节 (取 64 档)
  109. {"latin1 varchar(50)", "varchar", 50, 0, 0, types.FieldType_64byte, types.WidthCodeFor(50)},
  110. // 正好卡在旧天花板上: varchar(64) utf8mb4 = 256 字节
  111. {"utf8mb4 varchar(64)", "varchar", 256, 0, 0, types.FieldType_256byte, types.WidthCodeFor(256)},
  112. // 超过旧天花板不再落变长列: 320 字节仍在定长档内, 保住"可索引"
  113. {"utf8mb4 varchar(65)", "varchar", 260, 0, 0, types.FieldType_256byte, types.WidthCodeFor(260)},
  114. // 直到 MaxInlineColumnWidth (2048) 为止都还能定长
  115. {"utf8mb4 varchar(512)", "varchar", 2048, 0, 0, types.FieldType_256byte, types.WidthCodeFor(2048)},
  116. // 超出 2048 才落变长列 (变长只是没索引, 不会丢数据)
  117. {"utf8mb4 varchar(600)", "varchar", 2400, 0, 0, types.FieldType_Dynamicbyte, 0},
  118. {"utf8mb4 char(1)", "char", 4, 0, 0, types.FieldType_16byte, types.WidthCodeFor(4)},
  119. {"utf8mb4 text", "text", 65535, 0, 0, types.FieldType_Dynamicbyte, 0},
  120. {"blob", "blob", 65535, 0, 0, types.FieldType_Blob, 0},
  121. // 定点: (p,s) 要读进来, 小数位不能被抹平
  122. {"decimal(18,2)", "decimal", 20, 18, 2, types.FieldType_Decimal, 2},
  123. // 长度信息为 0 (数值/时间列没有 octet length) 时退回纯类型名
  124. {"int", "int", 0, 10, 0, types.FieldType_Int32, 0},
  125. {"datetime", "datetime", 0, 0, 0, types.FieldType_8byte, 0},
  126. }
  127. for _, c := range cases {
  128. decl := mysqlColumnDecl(c.dataType, false, c.charBytes, c.numPrec, c.numScale)
  129. ft, param := inferSQLType(decl, true)
  130. if ft != c.want || param != c.wantParam {
  131. t.Errorf("%s: %q → (%v, %d), 期望 (%v, %d)",
  132. c.name, decl, ft, param, c.want, c.wantParam)
  133. }
  134. }
  135. }
  136. // TestMySQLUnsigned 无符号标志只能从 COLUMN_TYPE 读。
  137. //
  138. // MySQL 对 "bigint unsigned" 报的 DATA_TYPE 就是 "bigint" —— 只看 DATA_TYPE
  139. // 等于把无符号这件事整个丢掉, 于是无符号列全被当有符号收:
  140. // - BIGINT UNSIGNED 的 2^63 读回来是负数;
  141. // - INT UNSIGNED 的上界只剩一半。
  142. //
  143. // EXTRA 也靠不住 (只在显式 zerofill 时才有值), COLUMN_TYPE 两处都覆盖。
  144. func TestMySQLUnsigned(t *testing.T) {
  145. cases := []struct {
  146. columnType string
  147. want bool
  148. }{
  149. {"bigint(20) unsigned", true},
  150. {"int(10) unsigned zerofill", true}, // zerofill 隐含 unsigned
  151. {"bigint unsigned", true},
  152. {"BIGINT UNSIGNED", true}, // information_schema 大小写随版本
  153. {"bigint(20)", false},
  154. {"int(11)", false},
  155. {"varchar(50)", false},
  156. {"decimal(18,2)", false},
  157. {"datetime", false},
  158. }
  159. for _, c := range cases {
  160. if got := mysqlUnsigned(c.columnType); got != c.want {
  161. t.Errorf("mysqlUnsigned(%q) = %v, 期望 %v", c.columnType, got, c.want)
  162. }
  163. }
  164. }
  165. // TestMySQLColumnDeclUnsigned 无符号后缀必须真的走到 inferSQLType,
  166. // 而且不能污染第一个括号里的容量 / 精度参数。
  167. func TestMySQLColumnDeclUnsigned(t *testing.T) {
  168. cases := []struct {
  169. name string
  170. dataType, columnType string
  171. charBytes, numPrec, numScale int64
  172. want types.FieldType
  173. wantParam byte
  174. }{
  175. // DATA_TYPE 丢了 unsigned, 补上 COLUMN_TYPE 才能收成无符号档
  176. {"bigint unsigned", "bigint", "bigint(20) unsigned", 0, 20, 0, types.FieldType_8byte, 0},
  177. {"bigint signed", "bigint", "bigint(20)", 0, 20, 0, types.FieldType_Int64, 0},
  178. {"int unsigned", "int", "int(10) unsigned", 0, 10, 0, types.FieldType_4byte, 0},
  179. {"int signed", "int", "int(11)", 0, 10, 0, types.FieldType_Int32, 0},
  180. {"smallint unsigned", "smallint", "smallint(5) unsigned", 0, 5, 0, types.FieldType_2byte, 0},
  181. {"tinyint unsigned", "tinyint", "tinyint(3) unsigned", 0, 3, 0, types.FieldType_1byte, 0},
  182. // 后缀在最后, 不能把 (n) / (p,s) 吃掉
  183. {"varchar(50) utf8mb4 unsigned", "varchar", "varchar(50) unsigned", 200, 0, 0,
  184. types.FieldType_256byte, types.WidthCodeFor(200)},
  185. {"decimal(18,2) unsigned", "decimal", "decimal(18,2) unsigned", 0, 18, 2,
  186. types.FieldType_Decimal, 2},
  187. }
  188. for _, c := range cases {
  189. decl := mysqlColumnDecl(c.dataType, mysqlUnsigned(c.columnType), c.charBytes, c.numPrec, c.numScale)
  190. ft, param := inferSQLType(decl, true)
  191. if ft != c.want || param != c.wantParam {
  192. t.Errorf("%s: COLUMN_TYPE=%q → decl %q → (%v, %d), 期望 (%v, %d)",
  193. c.name, c.columnType, decl, ft, param, c.want, c.wantParam)
  194. }
  195. }
  196. }
  197. // TestSQLTypeRoundTrip 声明出去的类型名必须能被自己读回同一个类型。
  198. //
  199. // 例外的几种都【不依赖 meta 就得靠宽度/语义兜底】, 这里显式列出来当文档:
  200. // - 窄整数在 SQLite 只有 INTEGER 一种写法, 读回一律 Int64;
  201. // - Decimal 在 SQLite 没有原生类型 (落 TEXT), 读回是 Dynamicbyte;
  202. // - UUID 在 MySQL 用 BINARY(16) 存, 读回被认成 Blob;
  203. // - Blob 与 Dynamicbyte 盘上同为动态列, 靠 meta 才能区分到具体哪一种;
  204. // - 定长串的**宽度码不是逐位往返的**: 声明成 VARCHAR(列宽) 再读回来会按宽度重新取档,
  205. // 档位相同, 但"用类型默认档 (param 0)"的列会变成"写明档位 (param≠0)"。
  206. // 所以定长串钉的不变量是【字节宽度】(见下面第二个循环), 不是 param 字面值。
  207. //
  208. // 也就是说: 需要"精确类型"的往返都依赖 __vrtx_meta (默认开启); 没有 meta 时
  209. // 至少保证["数据不丢、语义不离谱"]。
  210. func TestSQLTypeRoundTrip(t *testing.T) {
  211. type rt struct {
  212. ft types.FieldType
  213. param byte
  214. }
  215. for _, mysql := range []bool{true, false} {
  216. // 这些类型在两个方言下都能原样往返
  217. exact := []rt{
  218. {types.FieldType_Blob, 0},
  219. {types.FieldType_Bool, 0},
  220. {types.FieldType_Year, 0},
  221. {types.FieldType_Float64, 0},
  222. {types.FieldType_Int64, 0},
  223. }
  224. if mysql {
  225. exact = append(exact,
  226. rt{types.FieldType_Int8, 0},
  227. rt{types.FieldType_Int16, 0},
  228. rt{types.FieldType_Int32, 0},
  229. rt{types.FieldType_1byte, 0},
  230. rt{types.FieldType_2byte, 0},
  231. rt{types.FieldType_4byte, 0},
  232. rt{types.FieldType_8byte, 0},
  233. rt{types.FieldType_Float32, 0},
  234. rt{types.FieldType_Decimal, 2},
  235. )
  236. }
  237. // 定长串: 往返不变量是【字节宽度 + 类型族】。
  238. //
  239. // 声明成 VARCHAR(列宽) 之后再读回来会按宽度重新取档: 宽度码列往返后档位不变
  240. // (类型也稳, 因为基底类型由宽度推出); 用类型默认档 (param 0) 的列则会拿到一个
  241. // 显式宽度码 —— 宽度一样, 只是表示法从"默认档"变成"写明档位"。这正是本节
  242. // 要钉住的性质: 变的只是表示, 不是行内布局。
  243. stringCases := []rt{
  244. {types.FieldType_16byte, 0}, {types.FieldType_32byte, 0},
  245. {types.FieldType_64byte, 0}, {types.FieldType_128byte, 0},
  246. {types.FieldType_256byte, 0},
  247. }
  248. for _, size := range []int{4, 16, 32, 48, 100, 200, 256, 300, 1000, 2048} {
  249. ft, param := bucketColumn(size)
  250. stringCases = append(stringCases, rt{ft, param})
  251. }
  252. for _, w := range stringCases {
  253. decl := sqlTypeName(w.ft, w.param, mysql)
  254. ft, param := inferSQLType(decl, mysql)
  255. if ft != w.ft || types.FieldSize(ft, param) != types.FieldSize(w.ft, w.param) {
  256. t.Errorf("mysql=%v: %v/param%d → %q → (%v, param%d): 类型或行内宽度变了 (%d → %d 字节)",
  257. mysql, w.ft, w.param, decl, ft, param,
  258. types.FieldSize(w.ft, w.param), types.FieldSize(ft, param))
  259. }
  260. }
  261. for _, e := range exact {
  262. decl := sqlTypeName(e.ft, e.param, mysql)
  263. ft, param := inferSQLType(decl, mysql)
  264. if ft != e.ft || param != e.param {
  265. t.Errorf("mysql=%v: %v → %q → (%v, %d), 期望 (%v, %d)",
  266. mysql, e.ft, decl, ft, param, e.ft, e.param)
  267. }
  268. }
  269. }
  270. }
  271. // TestDeclaredCapacityNote 声明容量超过单值上限的列必须【报出来】, 且不许误报。
  272. //
  273. // 落动态列不等于"声明多大都行": 动态列的内容整体落在一个堆页里, 单值上限是
  274. // types.MaxStringSize。源库里 varchar(60000) 这种声明在 VRTX 端兑现不了, 而读端是唯一看得到
  275. // 声明原文的地方 —— 在这里漏掉, 用户只会看到一条"容量比声明小得多"的列而没有任何提示。
  276. //
  277. // 反过来误报同样有害: TEXT / BLOB 家族在 MySQL 的 information_schema 里带着【类型自身的】
  278. // 上界 (text → CHARACTER_OCTET_LENGTH = 65535), 那个数不是用户的声明, 按它报"兑现不了"会让
  279. // 每一张带 TEXT 列的表都挂上一条无意义的警告, 真正的容量问题反而被淹掉。
  280. //
  281. // 容量报告只影响提示, 不影响类型: 同一个入口推出来的类型必须与 inferSQLType 逐字节一致,
  282. // 否则"报了一句"就悄悄改了列的类型, 比不报更糟。
  283. func TestDeclaredCapacityNote(t *testing.T) {
  284. atCap := fmt.Sprintf("VARCHAR(%d)", types.MaxStringSize)
  285. overCap := fmt.Sprintf("VARCHAR(%d)", types.MaxStringSize+1)
  286. cases := []struct {
  287. decl string
  288. mysql bool
  289. ft types.FieldType
  290. param byte
  291. note bool // 是否应当报"容量封顶"
  292. }{
  293. // 还在定长档: 容量本来就远小于单值上限, 不报
  294. {"VARCHAR(2048)", true, types.FieldType_256byte, types.WidthCodeFor(2048), false},
  295. // 已落动态列但声明没超限: 声明多少就能装多少, 不报
  296. {atCap, false, types.FieldType_Dynamicbyte, 0, false},
  297. // 超过一个字节就兑现不了: 报
  298. {overCap, false, types.FieldType_Dynamicbyte, 0, true},
  299. {"VARCHAR(60000)", true, types.FieldType_Dynamicbyte, 0, true},
  300. // 二进制长度族同理 (BINARY / VARBINARY 也带容量声明, 盘上同样是动态列)
  301. {"BINARY(16)", true, types.FieldType_Blob, 0, false},
  302. {"VARBINARY(60000)", true, types.FieldType_Blob, 0, true},
  303. // TEXT / BLOB 家族: 括号里那个数是类型自身的上界, 不是用户的声明 → 不报
  304. {"TEXT(65535)", true, types.FieldType_Dynamicbyte, 0, false},
  305. {"LONGTEXT(4294967295)", true, types.FieldType_Dynamicbyte, 0, false},
  306. {"LONGBLOB(4294967295)", true, types.FieldType_Blob, 0, false},
  307. // 其它带括号的类型: 括号里是精度 / 小数位, 与容量无关
  308. {"DECIMAL(18,2)", true, types.FieldType_Decimal, 2, false},
  309. }
  310. for _, c := range cases {
  311. ft, param, note := inferSQLTypeWithNote(c.decl, c.mysql)
  312. if ft != c.ft || param != c.param {
  313. t.Errorf("inferSQLTypeWithNote(%q, mysql=%v) = (%v, %d), 期望 (%v, %d)",
  314. c.decl, c.mysql, ft, param, c.ft, c.param)
  315. }
  316. if got := note != ""; got != c.note {
  317. t.Errorf("%q: 报了容量=%v (%q), 期望 =%v", c.decl, got, note, c.note)
  318. }
  319. }
  320. // 报出来的话要能直接定位问题: 用户写下的数与引擎能给的上限, 两个都得有。
  321. _, _, note := inferSQLTypeWithNote(overCap, false)
  322. for _, want := range []string{strconv.Itoa(types.MaxStringSize + 1), strconv.Itoa(types.MaxStringSize)} {
  323. if !strings.Contains(note, want) {
  324. t.Errorf("容量报告里缺少 %q: %q", want, note)
  325. }
  326. }
  327. }