sqltype_test.go 17 KB

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