validate.go 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. package migrate
  2. import (
  3. "git.ycl.pub/yl32940/TsDB2/DB/core"
  4. "fmt"
  5. "strings"
  6. )
  7. // ============================ 迁移侧的结构校验 ============================
  8. //
  9. // 为什么单独有一层: 源库 (MySQL / SQLite) 的结构能力与 VRTX 不是子集关系。源侧完全合法、
  10. // 到 VRTX 侧却无处安放的东西不少 ——
  11. //
  12. // 源库 (常见上限) VRTX
  13. // MySQL 列数 1017 (InnoDB 硬限) core.MaxFieldsPerTable = 254
  14. // MySQL/SQLite 表名 64 / 不限 core.TableNameSize = 48
  15. // MySQL 列名 64, SQLite 几乎不限 core.FieldNameSize = 255
  16. // MySQL 索引名 64, SQLite 几乎不限 core.IndexNameSize = 64
  17. // MySQL 索引分量最多 16, SQLite 更多 core.MaxIndexFields = 16
  18. // MySQL 主键可以是复合键 VRTX 的行存储索引只能单列
  19. // MySQL DECIMAL(65,30) p ≤ 38 且 s ≤ 31 (否则退 float64)
  20. //
  21. // 不在这里拦, 这些差异就会以 VRTX【内部约束】的样子冒出来:
  22. //
  23. // 建 VRTX 表 t: 字段数量非法: 一张表最多 254 列, 本次 900 列
  24. //
  25. // 用户看不出 "900" 是源库的事实、"254" 是引擎的边界, 更不知道该改哪一边。所以这里在读
  26. // 完源结构、建表之前跑一遍, 把源库的实际值与 VRTX 的上限摆在一起说。
  27. //
  28. // 两类处理:
  29. // - 硬错误: 目标表【建不出来】, 只能拒绝 (改源结构, 或用 Options.Tables 跳过);
  30. // - 报告: 建得出来, 但容量/语义与原库不同 —— 写进 sourceNotes, 由 runTable 记进
  31. // 迁移报告。与 SkippedIndexes 同一条原则: 静默改变语义比报错更糟, 但也不该让整张表
  32. // 的数据陪葬 (索引能补、容量是小概率、精度问题用户知情后可自行取舍)。
  33. //
  34. // 行宽 ("一页两行", types.MaxRowDataSize) 是这一类里唯一【能自动救】的: 建表前先把宽度
  35. // 超过 256 字节的定长字符串列降为动态列, 降完还超才拒 —— 见 fitRowWidthForVRTX
  36. // (widerow.go), 由 runTable 在本文件之后调用。
  37. //
  38. // 只在目标端是 VRTX 时跑: SQL 一侧的约束比这些宽松得多 (MySQL/SQLite 都收得下 VRTX 的
  39. // 任何结构), 往 SQL 迁移时这层是空的。
  40. // sourceNotes 读端在源库结构里发现的、VRTX 端承载不了的东西。
  41. //
  42. // 三类分开存, 因为下游处理不同:
  43. // - Capped / Degraded: 只报, 不影响建表;
  44. // - PK: 复合主键 —— 有 AutoPK 时补 _id 继续迁 (报), 没有时表根本建不出来 (硬错误),
  45. // 所以它单独一格, 两种下场都能说清楚。
  46. //
  47. // 合成一句话会让"装不下"和"类型变了"混在一起, 用户没法据此决定要不要继续。
  48. type sourceNotes struct {
  49. // Capped 列装不下声明的容量 (类型不变, 上限变小), 见 capacityNote。
  50. Capped []string
  51. // Degraded 列只能降级收编, 类型都换了 (如 DECIMAL → float64), 见 degradeNote。
  52. Degraded []string
  53. // PK 源表主键表达了、但 VRTX 承载不了 (复合主键) 的说明; 空表示没这问题。
  54. PK string
  55. }
  56. // validateForVRTX 核对"这张表能不能在 VRTX 里立起来"。
  57. //
  58. // 必须在 prepareForVRTX 之后调用: 那时列清单才是最终形态 (可能已经补了 _id 列、补了索引),
  59. // 校验的才是"真正要落下去的那张表"。反过来在之前校验, 补出来的列就不在检查范围内。
  60. func validateForVRTX(s *TableSchema) error {
  61. // 表名: MySQL 给到 64 字节, VRTX 只到 core.TableNameSize(48)。core 也会拒, 但那里的
  62. // 报错只说"表名过长", 看不出这是"源库比目标库宽"的问题, 也拿不到源名的长度。
  63. if n := len(s.Name); n > core.TableNameSize {
  64. return fmt.Errorf("表 %q 的名字有 %d 字节, 超过 VRTX 上限 %d 字节 "+
  65. "(源库的表名通常允许到 64 字节): 请把源表名改短, 或用 Options.Tables 跳过它",
  66. s.Name, n, core.TableNameSize)
  67. }
  68. // 列数: 迁移里最容易被源库【合法地】撞到的一条 —— MySQL (InnoDB) 允许 1017 列,
  69. // 而 VRTX 的 FieldCount 与 schema 字段索引都只有 1 字节, 上限 core.MaxFieldsPerTable。
  70. //
  71. // 注意含已被 DropField 软删除的列: 那些列仍占 master registry 的位置, 删过的表能加的
  72. // 新列更少。这里按最终列清单算, 与 core 的 checkFieldCount 同口径。
  73. if n := len(s.Columns); n > core.MaxFieldsPerTable {
  74. return fmt.Errorf("表 %s 有 %d 列, 超过 VRTX 上限 %d 列 "+
  75. "(源库通常允许到 1017 列): 请拆分源表, 或只迁其中的一部分列",
  76. s.Name, n, core.MaxFieldsPerTable)
  77. }
  78. // 列名: schema 里是 [nameLen(1B)][name...]。超过 255 字节会让 nameLen 截断, 之后所有
  79. // 列从错位偏移解码 —— 不是"名字被截短"这么轻, 是整表错乱。SQLite 的列名几乎没有长度限制。
  80. for _, c := range s.Columns {
  81. if len(c.Name) == 0 {
  82. return fmt.Errorf("表 %s 有一列的名字为空: VRTX 的 schema 里列名长度占 1 字节, "+
  83. "空名字会让解析侧把下一个字段的类型字节当名字", s.Name)
  84. }
  85. if n := len(c.Name); n > core.FieldNameSize {
  86. return fmt.Errorf("表 %s 的列名 %q 有 %d 字节, 超过 VRTX 上限 %d 字节 "+
  87. "(schema 里列名长度占 1 字节, 超长会让后续列整体错位)",
  88. s.Name, c.Name, n, core.FieldNameSize)
  89. }
  90. }
  91. // 行存储索引的分量数: VRTX 的行数据寄存在行存储索引的条目里, 而它只能是单列
  92. // (RowStore() 已经按"未显式指定就是 [PKColumn]"取好, 这里兜住显式指定多列的情况)。
  93. if rs := s.RowStore(); len(rs) > 1 {
  94. return fmt.Errorf("表 %s 的行存储索引指定了 %d 列 (%v), VRTX 只支持单列行存储索引",
  95. s.Name, len(rs), rs)
  96. }
  97. return nil
  98. }
  99. // auditIndexForVRTX 检查一个二级索引能不能在 VRTX 里建出来, 不能则返回原因 (可读的短语)。
  100. //
  101. // 与 validateForVRTX 的区别是【处理方式】: 表和列建不出来就没有行可以放, 只能拒绝;
  102. // 而索引建不出来不影响数据 —— 与 ErrIndexDynamicField 一样跳过并记进 SkippedIndexes,
  103. // 让用户事后自行补 (索引丢了还能补, 数据没迁才是真丢)。
  104. //
  105. // 空返回表示可以建。
  106. func auditIndexForVRTX(idx IndexSchema) string {
  107. // 索引名: VRTX 上限 core.IndexNameSize。SQLite 的索引名几乎没有长度限制, MySQL 给到
  108. // 64 字节 (刚好等于 VRTX 的上限, 不多不少)。
  109. if err := checkIndexName(idx.Name); err != nil {
  110. return err.Error()
  111. }
  112. // 分量数: IndexMeta.FieldIDs 是定长 [16]byte, 多出来的分量无处落盘。
  113. //
  114. // SQLite 的索引能引用的列数几乎没有限制 (上限跟着表的列数走), 所以这条不是理论值。
  115. if n := len(idx.Columns); n > core.MaxIndexFields {
  116. return fmt.Sprintf("索引有 %d 个分量列, 超过 VRTX 上限 %d 个",
  117. n, core.MaxIndexFields)
  118. }
  119. return ""
  120. }
  121. // compositePKNote 复合主键的说明。
  122. //
  123. // VRTX 的行数据寄存在行存储索引的条目里, 而那个索引只能单列 —— 复合主键承载不了。
  124. // 措辞里必须带上列清单: 用户据此判断"拆表 / 改成单列主键"还是"接受降级"。
  125. func compositePKNote(table string, cols []string) string {
  126. return fmt.Sprintf("源表 %s 的主键是复合键 (%s): VRTX 的行存储索引只支持单列, "+
  127. "该主键无法原样承载 (复合唯一性不再由主键保证)", table, strings.Join(cols, ", "))
  128. }