validate_test.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. package migrate
  2. import (
  3. "git.ycl.pub/yl32940/TsDB2/DB/core"
  4. "git.ycl.pub/yl32940/TsDB2/DB/core/types"
  5. "database/sql"
  6. "path/filepath"
  7. "strconv"
  8. "strings"
  9. "testing"
  10. )
  11. // ============================ 迁移侧的结构闸门 ============================
  12. //
  13. // 这一层存在的理由: 源库 (MySQL/SQLite) 的结构能力比 VRTX 宽, 源侧合法的东西到 VRTX 侧
  14. // 无处安放。测的重点不只是"报错了", 而是【报错里有没有用户能据以行动的信息】——
  15. // 源库的实际值 (900 列 / 65 字节表名) 和 VRTX 的上限 (254 / 48) 都要在里面。
  16. // 只说一句"字段数量非法", 用户无从判断该改源表还是该拆表。
  17. func atMostColumns(n int) []Column {
  18. cols := make([]Column, n)
  19. for i := range cols {
  20. cols[i] = Column{Name: "c" + strconv.Itoa(i), Type: types.FieldType_Uint8}
  21. }
  22. return cols
  23. }
  24. func TestValidateForVRTX(t *testing.T) {
  25. // ---- 列数: MySQL 允许 1017 列, VRTX 只到 core.MaxFieldsPerTable ----
  26. over := &TableSchema{Name: "wide", Columns: atMostColumns(core.MaxFieldsPerTable + 1)}
  27. err := validateForVRTX(over)
  28. if err == nil {
  29. t.Fatal("列数超限必须报错")
  30. }
  31. // 源库的事实 + 引擎的边界, 两个数都要在报错里
  32. for _, want := range []string{
  33. strconv.Itoa(core.MaxFieldsPerTable + 1), // 源表有多少列
  34. strconv.Itoa(core.MaxFieldsPerTable), // VRTX 上限
  35. "wide",
  36. } {
  37. if !strings.Contains(err.Error(), want) {
  38. t.Errorf("列数超限的报错应包含 %q: %v", want, err)
  39. }
  40. }
  41. // 正好到上限: 放行 (闸门是 >, 不是 >=)
  42. exact := &TableSchema{Name: "wide", Columns: atMostColumns(core.MaxFieldsPerTable)}
  43. if err := validateForVRTX(exact); err != nil {
  44. t.Errorf("正好 %d 列应当放行: %v", core.MaxFieldsPerTable, err)
  45. }
  46. // ---- 表名: MySQL 给到 64 字节, VRTX 只到 core.TableNameSize(48) ----
  47. longName := strings.Repeat("t", core.TableNameSize+1)
  48. err = validateForVRTX(&TableSchema{Name: longName, Columns: atMostColumns(2)})
  49. if err == nil {
  50. t.Fatal("表名超长必须报错")
  51. }
  52. for _, want := range []string{
  53. strconv.Itoa(len(longName)), // 源表名多长
  54. strconv.Itoa(core.TableNameSize), // VRTX 上限
  55. } {
  56. if !strings.Contains(err.Error(), want) {
  57. t.Errorf("表名超长的报错应包含 %q: %v", want, err)
  58. }
  59. }
  60. // ---- 列名: schema 里是 [nameLen(1B)][name...] ----
  61. err = validateForVRTX(&TableSchema{Name: "t", Columns: []Column{{Name: ""}}})
  62. if err == nil {
  63. t.Fatal("空列名必须报错 (会让解析侧把下一个字段的类型字节当名字)")
  64. }
  65. longCol := strings.Repeat("x", core.FieldNameSize+1)
  66. err = validateForVRTX(&TableSchema{Name: "t", Columns: []Column{{Name: longCol}}})
  67. if err == nil {
  68. t.Fatal("超长列名必须报错")
  69. }
  70. if !strings.Contains(err.Error(), strconv.Itoa(core.FieldNameSize)) {
  71. t.Errorf("超长列名的报错应给出上限 %d: %v", core.FieldNameSize, err)
  72. }
  73. // ---- 行存储索引: VRTX 只支持单列 ----
  74. err = validateForVRTX(&TableSchema{
  75. Name: "t",
  76. Columns: atMostColumns(3),
  77. RowStoreColumns: []string{"a", "b"},
  78. })
  79. if err == nil {
  80. t.Fatal("多列行存储索引必须报错")
  81. }
  82. if !strings.Contains(err.Error(), "a") || !strings.Contains(err.Error(), "b") {
  83. t.Errorf("行存储索引报错应点出是哪几列: %v", err)
  84. }
  85. // ---- 正常表: 一条都不该报 ----
  86. ok := &TableSchema{Name: "t", Columns: atMostColumns(3), PKColumn: "c0"}
  87. if err := validateForVRTX(ok); err != nil {
  88. t.Errorf("合法表不该被拦: %v", err)
  89. }
  90. }
  91. func TestAuditIndexForVRTX(t *testing.T) {
  92. // 索引名: SQLite 的索引名几乎没有长度限制, VRTX 只到 core.IndexNameSize
  93. longName := strings.Repeat("i", core.IndexNameSize+1)
  94. if got := auditIndexForVRTX(IndexSchema{Name: longName, Columns: []string{"a"}}); got == "" {
  95. t.Error("超长索引名应当被判为建不了")
  96. } else if !strings.Contains(got, strconv.Itoa(core.IndexNameSize)) {
  97. t.Errorf("超长索引名应给出上限 %d: %s", core.IndexNameSize, got)
  98. }
  99. // 分量数: IndexMeta.FieldIDs 是定长 [16]byte
  100. many := make([]string, core.MaxIndexFields+1)
  101. for i := range many {
  102. many[i] = "c" + strconv.Itoa(i)
  103. }
  104. got := auditIndexForVRTX(IndexSchema{Name: "idx_many", Columns: many})
  105. if got == "" {
  106. t.Fatal("分量过多的索引应当被判为建不了")
  107. }
  108. if !strings.Contains(got, strconv.Itoa(core.MaxIndexFields)) {
  109. t.Errorf("分量过多应给出上限 %d: %s", core.MaxIndexFields, got)
  110. }
  111. // 正常索引: 空返回 = 可以建
  112. if got := auditIndexForVRTX(IndexSchema{Name: "idx_ok", Columns: []string{"a", "b"}}); got != "" {
  113. t.Errorf("正常索引不该被判为建不了: %s", got)
  114. }
  115. }
  116. // TestDegradeNoteAndInferAgree 钉住"报的"与"做的"逐条一致。
  117. //
  118. // degradeNote 只负责【说】, 真正决定类型的是 inferSQLType —— 两者判定条件一旦分叉,
  119. // 报告就成了假消息: 要么列退成了 float64 却不吭声, 要么没退却吓唬用户。所以这里对每个
  120. // 声明同时问两边, 要求它们同进同退。
  121. func TestDegradeNoteAndInferAgree(t *testing.T) {
  122. cases := []struct {
  123. decl string
  124. // wantDegrade = 期望"确实降级了"; wantWhich = 报错里该出现的关键词
  125. wantDegrade bool
  126. wantWhich string
  127. }{
  128. {"DECIMAL(18,2)", false, ""},
  129. {"DECIMAL(38,2)", false, ""}, // p 到 38 仍可精确表达
  130. {"DECIMAL(38,31)", false, ""}, // s 到 31 仍可表达
  131. {"DECIMAL(39,0)", true, "39"}, // p 超 38
  132. {"DECIMAL(65,30)", true, "65"}, // 同上, 这是 MySQL 能建出来、VRTX 装不下的真实例子
  133. {"DECIMAL(38,35)", true, "35"}, // s 超 31: 系数装得下, 小数位无处表达
  134. {"NUMERIC(20,6)", false, ""},
  135. // 与 decimal 无关的声明一律不吭声 (这个函数不该对别的类型有意见)
  136. {"INT", false, ""},
  137. {"VARCHAR(4000)", false, ""},
  138. {"LONGTEXT", false, ""},
  139. {"", false, ""},
  140. }
  141. for _, c := range cases {
  142. note := degradeNote(c.decl)
  143. ft, _ := inferSQLType(c.decl, true)
  144. if c.wantDegrade {
  145. if note == "" {
  146. t.Errorf("%q: 退成了 float64 却没报出来 (静默丢精度)", c.decl)
  147. continue
  148. }
  149. if !strings.Contains(note, c.wantWhich) {
  150. t.Errorf("%q: 报错应指明是哪一条超限 (%s): %s", c.decl, c.wantWhich, note)
  151. }
  152. if ft != types.FieldType_Float64 {
  153. t.Errorf("%q: 说降级了, inferSQLType 却给了 %v", c.decl, ft)
  154. }
  155. continue
  156. }
  157. if note != "" {
  158. t.Errorf("%q: 没降级却报了降级: %s", c.decl, note)
  159. }
  160. }
  161. }
  162. // TestSQLTypeNameDecimalWidth VRTX 的 Decimal 声明出去必须装得下 VRTX 里最大的系数。
  163. //
  164. // 以前按 DECIMAL(MaxDecimalScale, s) = DECIMAL(31,s) 声明, 于是 scale=30 的列只剩 1 位
  165. // 整数位, 而 VRTX 的系数上限是 10^38-1 —— 导出 8 位整数的值时会被 MySQL 判成超范围。
  166. func TestSQLTypeNameDecimalWidth(t *testing.T) {
  167. for _, scale := range []byte{0, 2, 20, 30, 31} {
  168. decl := sqlTypeName(types.FieldType_Decimal, scale, true)
  169. if decl != "DECIMAL("+strconv.Itoa(types.MaxDecimalPrecision)+","+strconv.Itoa(int(scale))+")" {
  170. t.Errorf("Decimal(%d) 的 MySQL 声明 = %q, 应为 DECIMAL(%d,%d)",
  171. scale, decl, types.MaxDecimalPrecision, scale)
  172. }
  173. // 声明出去再读回来, 类型与小数位都不能变 (否则往返一次就换了语义)
  174. ft, param := inferSQLType(decl, true)
  175. if ft != types.FieldType_Decimal || param != scale {
  176. t.Errorf("Decimal(%d) 往返后变成 (%v,%d)", scale, ft, param)
  177. }
  178. }
  179. }
  180. // TestCompositePKNote 复合主键的报告必须点出"源表有主键, 只是多列"。
  181. //
  182. // 以前读端直接把主键丢成空串, 下游报的是"源表没有主键"—— 用户会去源库找一个不存在的
  183. // 问题 (源表的主键好好的), 真正的原因是 VRTX 的行存储索引只能单列。
  184. func TestCompositePKNote(t *testing.T) {
  185. note := compositePKNote("orders", []string{"tenant_id", "order_no"})
  186. for _, want := range []string{"orders", "tenant_id", "order_no", "复合键"} {
  187. if !strings.Contains(note, want) {
  188. t.Errorf("复合主键的报告应包含 %q: %s", want, note)
  189. }
  190. }
  191. }
  192. // ============================ 端到端: 闸门真的接在迁移路径上 ============================
  193. //
  194. // 上面测的是校验函数本身。一个"从来没被调用过"的校验函数同样能过那些用例, 所以这里从
  195. // Run 走一遍: 源库 (SQLite, 与 MySQL 在"结构比 VRTX 宽"这点上同类) 真有这样的表时,
  196. // 迁移是不是按预期拒绝 / 跳过。
  197. // TestWideTableRejectedBeforeBuild 源库列数超过 VRTX 上限 → 建表之前就报错, 不留半成品。
  198. //
  199. // MySQL (InnoDB) 允许 1017 列而 VRTX 只到 core.MaxFieldsPerTable。不前置检查, 这条路会
  200. // 一直走到 core 写 FieldCount 时才炸, 报出一句与源库无关的"字段数量非法"—— 用户不知道
  201. // 该改源表还是该拆表。
  202. func TestWideTableRejectedBeforeBuild(t *testing.T) {
  203. dir := t.TempDir()
  204. srcPath := filepath.Join(dir, "src.sqlite")
  205. dstPath := filepath.Join(dir, "dst.db")
  206. // 比上限多 32 列: 源库(SQLite)建得出来, VRTX 建不出来
  207. const n = core.MaxFieldsPerTable + 32
  208. cols := make([]string, 0, n)
  209. for i := 0; i < n; i++ {
  210. cols = append(cols, "c"+strconv.Itoa(i)+" INTEGER")
  211. }
  212. db, err := sql.Open("sqlite", srcPath)
  213. if err != nil {
  214. t.Fatal(err)
  215. }
  216. for _, s := range []string{
  217. `CREATE TABLE Wide (id INTEGER PRIMARY KEY, ` + strings.Join(cols, ", ") + `)`,
  218. `INSERT INTO Wide (id, c0) VALUES (1, 7)`,
  219. } {
  220. if _, err := db.Exec(s); err != nil {
  221. t.Fatalf("建源表: %v", err)
  222. }
  223. }
  224. _ = db.Close()
  225. s1, err := OpenSQLite(srcPath, quiet())
  226. if err != nil {
  227. t.Fatal(err)
  228. }
  229. defer s1.Close()
  230. v1, err := OpenVRTX(dstPath, quiet())
  231. if err != nil {
  232. t.Fatal(err)
  233. }
  234. defer v1.Close()
  235. _, err = Run(s1, v1, quiet())
  236. if err == nil {
  237. t.Fatal("列数超过 VRTX 上限的表必须被拒")
  238. }
  239. for _, want := range []string{
  240. "Wide",
  241. strconv.Itoa(n + 1), // 源表实际有多少列 (n 个 + 主键 id)
  242. strconv.Itoa(core.MaxFieldsPerTable), // VRTX 上限
  243. } {
  244. if !strings.Contains(err.Error(), want) {
  245. t.Errorf("列数超限的报错应包含 %q: %v", want, err)
  246. }
  247. }
  248. // 闸门在【建表之前】: 目标库里不该留下一张半成品 Wide
  249. names, err := v1.Tables()
  250. if err != nil {
  251. t.Fatal(err)
  252. }
  253. for _, nm := range names {
  254. if nm == "Wide" {
  255. t.Error("被拒的表不该在目标库里留下半成品")
  256. }
  257. }
  258. }
  259. // TestLongIndexNameSkippedNotFatal 索引名超长只该跳那一条索引, 不该让整张表陪葬。
  260. //
  261. // SQLite 的索引名几乎没有长度限制, VRTX 只到 core.IndexNameSize(64)。这类"源库合法、
  262. // 目标装不下"的索引以前会直接 return err 把整张表拦下来 —— 数据本来一条都不丢, 却因为
  263. // 一个名字迁不过去。跳过的同时必须写进报告 (静默丢索引等于悄悄改了结构)。
  264. func TestLongIndexNameSkippedNotFatal(t *testing.T) {
  265. dir := t.TempDir()
  266. srcPath := filepath.Join(dir, "src.sqlite")
  267. dstPath := filepath.Join(dir, "dst.db")
  268. longIdx := "idx_" + strings.Repeat("x", core.IndexNameSize) // 比上限长
  269. db, err := sql.Open("sqlite", srcPath)
  270. if err != nil {
  271. t.Fatal(err)
  272. }
  273. for _, s := range []string{
  274. `CREATE TABLE T (id INTEGER PRIMARY KEY, name VARCHAR(32) NOT NULL)`,
  275. `CREATE INDEX ` + longIdx + ` ON T(name)`,
  276. `INSERT INTO T (id, name) VALUES (1, 'a')`,
  277. } {
  278. if _, err := db.Exec(s); err != nil {
  279. t.Fatalf("执行 %s: %v", s, err)
  280. }
  281. }
  282. _ = db.Close()
  283. s1, err := OpenSQLite(srcPath, quiet())
  284. if err != nil {
  285. t.Fatal(err)
  286. }
  287. defer s1.Close()
  288. v1, err := OpenVRTX(dstPath, quiet())
  289. if err != nil {
  290. t.Fatal(err)
  291. }
  292. defer v1.Close()
  293. rep, err := Run(s1, v1, quiet())
  294. if err != nil {
  295. t.Fatalf("一条索引建不出来不该让整张表失败: %v", err)
  296. }
  297. if rep.Rows != 1 {
  298. t.Errorf("数据应当照常迁过去, 实际 %d 行", rep.Rows)
  299. }
  300. if len(rep.Tables) != 1 {
  301. t.Fatalf("应当只有一张表的报告, 实际 %d", len(rep.Tables))
  302. }
  303. note := rep.Tables[0].Note
  304. if !strings.Contains(note, "跳过索引") {
  305. t.Errorf("跳过的索引必须报出来: %q", note)
  306. }
  307. // 报错要能让用户判断该怎么办: 得说清是"名字超过 64 字节"
  308. if !strings.Contains(note, strconv.Itoa(core.IndexNameSize)) {
  309. t.Errorf("跳过的索引报告应给出上限 %d: %q", core.IndexNameSize, note)
  310. }
  311. }