mysql.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. package migrate
  2. import (
  3. "TsDB2/DB/core/page"
  4. "context"
  5. "database/sql"
  6. "fmt"
  7. "strings"
  8. "time"
  9. _ "github.com/go-sql-driver/mysql" // database/sql 驱动注册
  10. )
  11. // OpenMySQL 连接 MySQL。
  12. //
  13. // dsn 就是 go-sql-driver/mysql 的 DSN, 例如:
  14. //
  15. // root:123456@tcp(127.0.0.1:3306)/vrtx_demo?charset=utf8mb4&parseTime=false
  16. //
  17. // 建议保持 parseTime=false (默认): 时间列按字符串原样搬。开了 parseTime 会引入
  18. // 时区换算, 往返一次可能就变了几小时。
  19. func OpenMySQL(dsn string, opts *Options) (*SQL, error) {
  20. db, err := sql.Open("mysql", dsn)
  21. if err != nil {
  22. return nil, fmt.Errorf("打开 MySQL: %w", err)
  23. }
  24. db.SetMaxOpenConns(4)
  25. if err := db.Ping(); err != nil {
  26. _ = db.Close()
  27. return nil, fmt.Errorf("连接 MySQL 失败: %w", err)
  28. }
  29. return newSQL(db, KindMySQL, dsn, mysqlDial{}, opts), nil
  30. }
  31. type mysqlDial struct{}
  32. func (mysqlDial) kind() Kind { return KindMySQL }
  33. // mysqlListTimeout 列库最多等这么久 —— 主机写错、端口不通时界面不该一直转圈。
  34. const mysqlListTimeout = 10 * time.Second
  35. // MySQLDatabases 问一个 MySQL 服务器上有哪些库。
  36. //
  37. // dsn 和 OpenMySQL 同样格式, 但库名可以留空: 能不能列出库只取决于账号权限,
  38. // 不取决于你已经知道哪个库 —— 连接界面正是靠它把库名做成候选, 免得手打错一个
  39. // 字母换来一句"连接失败"。
  40. //
  41. // 系统库 (mysql / information_schema 等) 不做过滤, 那是调用方的口味。
  42. func MySQLDatabases(dsn string) ([]string, error) {
  43. db, err := sql.Open("mysql", dsn)
  44. if err != nil {
  45. return nil, fmt.Errorf("打开 MySQL: %w", err)
  46. }
  47. defer db.Close()
  48. db.SetMaxOpenConns(1)
  49. ctx, cancel := context.WithTimeout(context.Background(), mysqlListTimeout)
  50. defer cancel()
  51. rows, err := db.QueryContext(ctx,
  52. `SELECT SCHEMA_NAME FROM information_schema.SCHEMATA ORDER BY SCHEMA_NAME`)
  53. if err != nil {
  54. return nil, fmt.Errorf("列出数据库失败: %w", err)
  55. }
  56. defer rows.Close()
  57. var out []string
  58. for rows.Next() {
  59. var name string
  60. if err := rows.Scan(&name); err != nil {
  61. return nil, err
  62. }
  63. out = append(out, name)
  64. }
  65. if err := rows.Err(); err != nil {
  66. return nil, fmt.Errorf("列出数据库失败: %w", err)
  67. }
  68. return out, nil
  69. }
  70. func (mysqlDial) quote(name string) string {
  71. return "`" + strings.ReplaceAll(name, "`", "``") + "`"
  72. }
  73. func (d mysqlDial) columnDDL(c Column) string {
  74. return d.quote(c.Name) + " " + columnTypeName(c, true) + nullClause(c)
  75. }
  76. // createTable 建表。主键写成表级约束 PRIMARY KEY (col): VRTX 的行存储索引是单列,
  77. // 这里也只可能是单列。
  78. func (d mysqlDial) createTable(s *TableSchema) string {
  79. parts := make([]string, 0, len(s.Columns)+1)
  80. for _, c := range s.Columns {
  81. parts = append(parts, " "+d.columnDDL(c))
  82. }
  83. if s.PKColumn != "" {
  84. parts = append(parts, " PRIMARY KEY ("+d.quote(s.PKColumn)+")")
  85. }
  86. return fmt.Sprintf("CREATE TABLE %s (\n%s\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
  87. d.quote(s.Name), strings.Join(parts, ",\n"))
  88. }
  89. func (d mysqlDial) dropTable(table string) string {
  90. return "DROP TABLE IF EXISTS " + d.quote(table)
  91. }
  92. func (d mysqlDial) addColumn(table string, c Column) string {
  93. return fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s", d.quote(table), d.columnDDL(c))
  94. }
  95. func (d mysqlDial) createIndex(table string, idx IndexSchema) string {
  96. cols := make([]string, len(idx.Columns))
  97. for i, c := range idx.Columns {
  98. cols[i] = d.quote(c)
  99. }
  100. return fmt.Sprintf("%s %s ON %s (%s)",
  101. createIndexKind(idx), d.quote(idx.Name), d.quote(table), strings.Join(cols, ", "))
  102. }
  103. func (d mysqlDial) listTables(db *sql.DB) ([]string, error) {
  104. rows, err := db.Query(`SELECT TABLE_NAME FROM information_schema.TABLES
  105. WHERE TABLE_SCHEMA = DATABASE() AND TABLE_TYPE = 'BASE TABLE' ORDER BY TABLE_NAME`)
  106. if err != nil {
  107. return nil, fmt.Errorf("列 MySQL 表: %w", err)
  108. }
  109. defer rows.Close()
  110. var out []string
  111. for rows.Next() {
  112. var name string
  113. if err := rows.Scan(&name); err != nil {
  114. return nil, err
  115. }
  116. out = append(out, name)
  117. }
  118. return out, rows.Err()
  119. }
  120. // mysqlColumnDecl 把 information_schema 的长度信息收成 inferSQLType 认得的声明文本。
  121. //
  122. // 字符列取【字节】上界, 不能取字符数: MySQL 的 varchar(50) 是 50 个【字符】, 在
  123. // utf8mb4 下最多 200 字节; 而 VRTX 的定长档 (16/32/.../256byte) 是按【字节】算容量的。
  124. // 拿 50 去取档只会得到 64byte —— 一个 25 字的中文别名就装不下 (真实踩到:
  125. // resource_info.resAlias 是 varchar(50), 值 73 字节, 整表迁移失败)。
  126. // CHARACTER_OCTET_LENGTH 正是"这列最多多少字节", 而且按该列自己的字符集算
  127. // (latin1 仍是 1 字节/字符), 所以直接拿它当容量, 既够用又不至于无谓放大。
  128. //
  129. // unsigned 必须单独传进来, 因为无符号这件事【不在 DATA_TYPE 里】: MySQL 对
  130. // "bigint unsigned" 报的 DATA_TYPE 就是 "bigint", 只有 COLUMN_TYPE
  131. // ("bigint(20) unsigned") 保留 unsigned; 而 inferSQLType 正是靠声明原文里有没有
  132. // "UNSIGNED" 来判无符号档的。少了这个后缀, 所有 MySQL 无符号列都会被当有符号收:
  133. // BIGINT UNSIGNED 的 2^63 读出来是负数, INT UNSIGNED 的上界只剩一半。
  134. func mysqlColumnDecl(dataType string, unsigned bool, charBytes, numPrec, numScale int64) string {
  135. decl := dataType
  136. switch {
  137. case numScale > 0 || dataType == "decimal":
  138. // DECIMAL 必须带上 (p,s): 只给 "decimal" 会按 scale=0 收, 小数位被抹平。
  139. decl = fmt.Sprintf("%s(%d,%d)", dataType, numPrec, numScale)
  140. case charBytes > 0:
  141. decl = fmt.Sprintf("%s(%d)", dataType, charBytes)
  142. }
  143. if unsigned {
  144. // 后缀放在最后: inferSQLType 用 strings.Contains 找 "UNSIGNED", 位置无所谓;
  145. // splitTypeArgs 只取第一个 "(" 里的参数, 后缀不会污染 (p,s) / (n)。
  146. decl += " unsigned"
  147. }
  148. return decl
  149. }
  150. // mysqlUnsigned 判断 COLUMN_TYPE 是不是无符号列。
  151. //
  152. // 只看 COLUMN_TYPE: DATA_TYPE 把 unsigned 丢了; EXTRA 只在显式写了 zerofill 时才
  153. // 出现 "zerofill", 而 zerofill 本身也隐含 unsigned —— COLUMN_TYPE 两种都覆盖
  154. // ("int(10) unsigned" / "int(10) unsigned zerofill")。
  155. func mysqlUnsigned(columnType string) bool {
  156. return strings.Contains(strings.ToLower(columnType), "unsigned")
  157. }
  158. // columns 读列 + 单列主键名。
  159. //
  160. // 主键的那点信息这里最全, 所以两件"VRTX 承载不了"的事在这里记下 (见 sourceNotes):
  161. // - 复合主键: VRTX 的行存储索引只支持单列。以前直接把主键名丢成空串, 下游只会报
  162. // "源表没有主键"—— 而源表明明有主键, 只是多列, 提示是假的。
  163. // - 列级的容量/精度差异: 只有这里看得到 information_schema 给的声明原文。
  164. func (d mysqlDial) columns(db *sql.DB, table string) ([]Column, string, sourceNotes, error) {
  165. rows, err := db.Query(`SELECT COLUMN_NAME, DATA_TYPE, COLUMN_TYPE,
  166. COALESCE(CHARACTER_OCTET_LENGTH, 0),
  167. COALESCE(NUMERIC_PRECISION, 0), COALESCE(NUMERIC_SCALE, 0),
  168. COLUMN_KEY, IS_NULLABLE
  169. FROM information_schema.COLUMNS
  170. WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?
  171. ORDER BY ORDINAL_POSITION`, table)
  172. if err != nil {
  173. return nil, "", sourceNotes{}, err
  174. }
  175. defer rows.Close()
  176. var cols []Column
  177. var pkCols []string
  178. var notes sourceNotes
  179. for rows.Next() {
  180. // COLUMN_TYPE 只用来判无符号 (DATA_TYPE 把它丢了), 其余长度信息仍走各自那一列。
  181. var name, dataType, columnType, key, nullable string
  182. var charBytes, numPrec, numScale int64
  183. if err := rows.Scan(&name, &dataType, &columnType, &charBytes, &numPrec, &numScale, &key, &nullable); err != nil {
  184. return nil, "", sourceNotes{}, err
  185. }
  186. decl := mysqlColumnDecl(dataType, mysqlUnsigned(columnType), charBytes, numPrec, numScale)
  187. ft, param, note := inferSQLTypeWithNote(decl, true)
  188. if note != "" {
  189. notes.Capped = append(notes.Capped, name+": "+note)
  190. }
  191. if d := degradeNote(decl); d != "" {
  192. notes.Degraded = append(notes.Degraded, name+": "+d)
  193. }
  194. cols = append(cols, Column{
  195. Name: name,
  196. Type: ft,
  197. Param: param,
  198. TimeKind: sqlTimeKind(dataType),
  199. Nullable: nullable == "YES", // information_schema 的 IS_NULLABLE
  200. })
  201. if key == "PRI" {
  202. pkCols = append(pkCols, name)
  203. }
  204. }
  205. if err := rows.Err(); err != nil {
  206. return nil, "", sourceNotes{}, err
  207. }
  208. if len(pkCols) == 1 {
  209. return cols, pkCols[0], notes, nil
  210. }
  211. if len(pkCols) > 1 {
  212. notes.PK = compositePKNote(table, pkCols)
  213. }
  214. // 复合主键 / 无主键: 都当作"没有单列主键", 由调用方决定补 _id 还是报错。
  215. return cols, "", notes, nil
  216. }
  217. // indexes 读二级索引, 唯一性取 NON_UNIQUE=0。
  218. //
  219. // STATISTICS 里同一个索引每列一行, NON_UNIQUE 各行相同, 用 MAX 收成一行;
  220. // 主键索引 (INDEX_NAME='PRIMARY') 由 PKColumn 表达, 排除在外。
  221. func (d mysqlDial) indexes(db *sql.DB, table string) ([]IndexSchema, error) {
  222. rows, err := db.Query(`SELECT INDEX_NAME, MAX(NON_UNIQUE),
  223. GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX)
  224. FROM information_schema.STATISTICS
  225. WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND INDEX_NAME <> 'PRIMARY'
  226. GROUP BY INDEX_NAME`, table)
  227. if err != nil {
  228. return nil, err
  229. }
  230. defer rows.Close()
  231. var out []IndexSchema
  232. for rows.Next() {
  233. var name, cols string
  234. var nonUnique int
  235. if err := rows.Scan(&name, &nonUnique, &cols); err != nil {
  236. return nil, err
  237. }
  238. out = append(out, IndexSchema{
  239. Name: name,
  240. // SQL 侧分不出 Hash / BTree: 默认按 B+ 树建; 真正的类型由 __vrtx_meta
  241. // 带回来 (见 applyMeta)
  242. Type: page.IndexTypeBTree,
  243. Columns: strings.Split(cols, ","),
  244. Unique: nonUnique == 0,
  245. })
  246. }
  247. return out, rows.Err()
  248. }
  249. func (d mysqlDial) metaDDL() string {
  250. return fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s (
  251. tbl VARCHAR(191) NOT NULL,
  252. payload LONGTEXT NOT NULL,
  253. PRIMARY KEY (tbl)
  254. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`, d.quote(MetaTable))
  255. }
  256. func (d mysqlDial) metaUpsert() string {
  257. return fmt.Sprintf("INSERT INTO %s (tbl, payload) VALUES (?, ?) "+
  258. "ON DUPLICATE KEY UPDATE payload = VALUES(payload)", d.quote(MetaTable))
  259. }
  260. func (d mysqlDial) metaSelect() string {
  261. return fmt.Sprintf("SELECT payload FROM %s WHERE tbl = ? LIMIT 1", d.quote(MetaTable))
  262. }
  263. // maxPlaceholders MySQL 单语句占位符上限是 65535, 留一点余量。
  264. func (d mysqlDial) maxPlaceholders() int { return 60000 }