package migrate import ( "git.ycl.pub/yl32940/TsDB2/DB/core" "fmt" "strings" ) // ============================ 迁移侧的结构校验 ============================ // // 为什么单独有一层: 源库 (MySQL / SQLite) 的结构能力与 VRTX 不是子集关系。源侧完全合法、 // 到 VRTX 侧却无处安放的东西不少 —— // // 源库 (常见上限) VRTX // MySQL 列数 1017 (InnoDB 硬限) core.MaxFieldsPerTable = 254 // MySQL/SQLite 表名 64 / 不限 core.TableNameSize = 48 // MySQL 列名 64, SQLite 几乎不限 core.FieldNameSize = 255 // MySQL 索引名 64, SQLite 几乎不限 core.IndexNameSize = 64 // MySQL 索引分量最多 16, SQLite 更多 core.MaxIndexFields = 16 // MySQL 主键可以是复合键 VRTX 的行存储索引只能单列 // MySQL DECIMAL(65,30) p ≤ 38 且 s ≤ 31 (否则退 float64) // // 不在这里拦, 这些差异就会以 VRTX【内部约束】的样子冒出来: // // 建 VRTX 表 t: 字段数量非法: 一张表最多 254 列, 本次 900 列 // // 用户看不出 "900" 是源库的事实、"254" 是引擎的边界, 更不知道该改哪一边。所以这里在读 // 完源结构、建表之前跑一遍, 把源库的实际值与 VRTX 的上限摆在一起说。 // // 两类处理: // - 硬错误: 目标表【建不出来】, 只能拒绝 (改源结构, 或用 Options.Tables 跳过); // - 报告: 建得出来, 但容量/语义与原库不同 —— 写进 sourceNotes, 由 runTable 记进 // 迁移报告。与 SkippedIndexes 同一条原则: 静默改变语义比报错更糟, 但也不该让整张表 // 的数据陪葬 (索引能补、容量是小概率、精度问题用户知情后可自行取舍)。 // // 行宽 ("一页两行", types.MaxRowDataSize) 是这一类里唯一【能自动救】的: 建表前先把宽度 // 超过 256 字节的定长字符串列降为动态列, 降完还超才拒 —— 见 fitRowWidthForVRTX // (widerow.go), 由 runTable 在本文件之后调用。 // // 只在目标端是 VRTX 时跑: SQL 一侧的约束比这些宽松得多 (MySQL/SQLite 都收得下 VRTX 的 // 任何结构), 往 SQL 迁移时这层是空的。 // sourceNotes 读端在源库结构里发现的、VRTX 端承载不了的东西。 // // 三类分开存, 因为下游处理不同: // - Capped / Degraded: 只报, 不影响建表; // - PK: 复合主键 —— 有 AutoPK 时补 _id 继续迁 (报), 没有时表根本建不出来 (硬错误), // 所以它单独一格, 两种下场都能说清楚。 // // 合成一句话会让"装不下"和"类型变了"混在一起, 用户没法据此决定要不要继续。 type sourceNotes struct { // Capped 列装不下声明的容量 (类型不变, 上限变小), 见 capacityNote。 Capped []string // Degraded 列只能降级收编, 类型都换了 (如 DECIMAL → float64), 见 degradeNote。 Degraded []string // PK 源表主键表达了、但 VRTX 承载不了 (复合主键) 的说明; 空表示没这问题。 PK string } // validateForVRTX 核对"这张表能不能在 VRTX 里立起来"。 // // 必须在 prepareForVRTX 之后调用: 那时列清单才是最终形态 (可能已经补了 _id 列、补了索引), // 校验的才是"真正要落下去的那张表"。反过来在之前校验, 补出来的列就不在检查范围内。 func validateForVRTX(s *TableSchema) error { // 表名: MySQL 给到 64 字节, VRTX 只到 core.TableNameSize(48)。core 也会拒, 但那里的 // 报错只说"表名过长", 看不出这是"源库比目标库宽"的问题, 也拿不到源名的长度。 if n := len(s.Name); n > core.TableNameSize { return fmt.Errorf("表 %q 的名字有 %d 字节, 超过 VRTX 上限 %d 字节 "+ "(源库的表名通常允许到 64 字节): 请把源表名改短, 或用 Options.Tables 跳过它", s.Name, n, core.TableNameSize) } // 列数: 迁移里最容易被源库【合法地】撞到的一条 —— MySQL (InnoDB) 允许 1017 列, // 而 VRTX 的 FieldCount 与 schema 字段索引都只有 1 字节, 上限 core.MaxFieldsPerTable。 // // 注意含已被 DropField 软删除的列: 那些列仍占 master registry 的位置, 删过的表能加的 // 新列更少。这里按最终列清单算, 与 core 的 checkFieldCount 同口径。 if n := len(s.Columns); n > core.MaxFieldsPerTable { return fmt.Errorf("表 %s 有 %d 列, 超过 VRTX 上限 %d 列 "+ "(源库通常允许到 1017 列): 请拆分源表, 或只迁其中的一部分列", s.Name, n, core.MaxFieldsPerTable) } // 列名: schema 里是 [nameLen(1B)][name...]。超过 255 字节会让 nameLen 截断, 之后所有 // 列从错位偏移解码 —— 不是"名字被截短"这么轻, 是整表错乱。SQLite 的列名几乎没有长度限制。 for _, c := range s.Columns { if len(c.Name) == 0 { return fmt.Errorf("表 %s 有一列的名字为空: VRTX 的 schema 里列名长度占 1 字节, "+ "空名字会让解析侧把下一个字段的类型字节当名字", s.Name) } if n := len(c.Name); n > core.FieldNameSize { return fmt.Errorf("表 %s 的列名 %q 有 %d 字节, 超过 VRTX 上限 %d 字节 "+ "(schema 里列名长度占 1 字节, 超长会让后续列整体错位)", s.Name, c.Name, n, core.FieldNameSize) } } // 行存储索引的分量数: VRTX 的行数据寄存在行存储索引的条目里, 而它只能是单列 // (RowStore() 已经按"未显式指定就是 [PKColumn]"取好, 这里兜住显式指定多列的情况)。 if rs := s.RowStore(); len(rs) > 1 { return fmt.Errorf("表 %s 的行存储索引指定了 %d 列 (%v), VRTX 只支持单列行存储索引", s.Name, len(rs), rs) } return nil } // auditIndexForVRTX 检查一个二级索引能不能在 VRTX 里建出来, 不能则返回原因 (可读的短语)。 // // 与 validateForVRTX 的区别是【处理方式】: 表和列建不出来就没有行可以放, 只能拒绝; // 而索引建不出来不影响数据 —— 与 ErrIndexDynamicField 一样跳过并记进 SkippedIndexes, // 让用户事后自行补 (索引丢了还能补, 数据没迁才是真丢)。 // // 空返回表示可以建。 func auditIndexForVRTX(idx IndexSchema) string { // 索引名: VRTX 上限 core.IndexNameSize。SQLite 的索引名几乎没有长度限制, MySQL 给到 // 64 字节 (刚好等于 VRTX 的上限, 不多不少)。 if err := checkIndexName(idx.Name); err != nil { return err.Error() } // 分量数: IndexMeta.FieldIDs 是定长 [16]byte, 多出来的分量无处落盘。 // // SQLite 的索引能引用的列数几乎没有限制 (上限跟着表的列数走), 所以这条不是理论值。 if n := len(idx.Columns); n > core.MaxIndexFields { return fmt.Sprintf("索引有 %d 个分量列, 超过 VRTX 上限 %d 个", n, core.MaxIndexFields) } return "" } // compositePKNote 复合主键的说明。 // // VRTX 的行数据寄存在行存储索引的条目里, 而那个索引只能单列 —— 复合主键承载不了。 // 措辞里必须带上列清单: 用户据此判断"拆表 / 改成单列主键"还是"接受降级"。 func compositePKNote(table string, cols []string) string { return fmt.Sprintf("源表 %s 的主键是复合键 (%s): VRTX 的行存储索引只支持单列, "+ "该主键无法原样承载 (复合唯一性不再由主键保证)", table, strings.Join(cols, ", ")) }