mysql.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. package migrate
  2. import (
  3. "git.ycl.pub/yl32940/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/.../256 字节) 是按【字节】算容量的。
  124. // 拿 50 去取档只会得到 64 字节 —— 一个 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. // mysqlBool 判断 COLUMN_TYPE 是不是 BOOLEAN 列。
  159. //
  160. // 与 unsigned 同理, 这件事 DATA_TYPE 看不出来: MySQL 的 BOOLEAN 就是 tinyint(1),
  161. // information_schema 报的 DATA_TYPE 是 "tinyint"、COLUMN_TYPE 是 "tinyint(1)" ——
  162. // 只看 DATA_TYPE 会走 inferSQLType 的 TINYINT 分支收成 Int8, 而 sqltype.go 的
  163. // "BOOLEAN"/"BOOL" 分支对 MySQL 源永远不可达。宽度不是 1 的 tinyint 是真·小整数,
  164. // 仍按 Int8 收; tinyint(1) unsigned 能装 0..255, 也不是真假语义, 不特判。
  165. func mysqlBool(columnType string) bool {
  166. return strings.EqualFold(strings.TrimSpace(columnType), "tinyint(1)")
  167. }
  168. // columns 读列 + 单列主键名。
  169. //
  170. // 主键的那点信息这里最全, 所以两件"VRTX 承载不了"的事在这里记下 (见 sourceNotes):
  171. // - 复合主键: VRTX 的行存储索引只支持单列。以前直接把主键名丢成空串, 下游只会报
  172. // "源表没有主键"—— 而源表明明有主键, 只是多列, 提示是假的。
  173. // - 列级的容量/精度差异: 只有这里看得到 information_schema 给的声明原文。
  174. func (d mysqlDial) columns(db *sql.DB, table string) ([]Column, string, sourceNotes, error) {
  175. rows, err := db.Query(`SELECT COLUMN_NAME, DATA_TYPE, COLUMN_TYPE,
  176. COALESCE(CHARACTER_OCTET_LENGTH, 0),
  177. COALESCE(NUMERIC_PRECISION, 0), COALESCE(NUMERIC_SCALE, 0),
  178. COLUMN_KEY, IS_NULLABLE
  179. FROM information_schema.COLUMNS
  180. WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?
  181. ORDER BY ORDINAL_POSITION`, table)
  182. if err != nil {
  183. return nil, "", sourceNotes{}, err
  184. }
  185. defer rows.Close()
  186. var cols []Column
  187. var pkCols []string
  188. var notes sourceNotes
  189. for rows.Next() {
  190. // COLUMN_TYPE 只用来判无符号 (DATA_TYPE 把它丢了), 其余长度信息仍走各自那一列。
  191. var name, dataType, columnType, key, nullable string
  192. var charBytes, numPrec, numScale int64
  193. if err := rows.Scan(&name, &dataType, &columnType, &charBytes, &numPrec, &numScale, &key, &nullable); err != nil {
  194. return nil, "", sourceNotes{}, err
  195. }
  196. decl := mysqlColumnDecl(dataType, mysqlUnsigned(columnType), charBytes, numPrec, numScale)
  197. if mysqlBool(columnType) {
  198. // MySQL 的 BOOLEAN 就是 tinyint(1), DATA_TYPE 只报 "tinyint": 不特判只会
  199. // 落到 Int8, 等值/索引给出的是整数语义而不是真假。换成 boolean 走
  200. // inferSQLType 的 BOOL 分支, 先于常规 tinyint→Int8 路径生效。
  201. decl = "boolean"
  202. }
  203. ft, param, note := inferSQLTypeWithNote(decl, true)
  204. if note != "" {
  205. notes.Capped = append(notes.Capped, name+": "+note)
  206. }
  207. if d := degradeNote(decl); d != "" {
  208. notes.Degraded = append(notes.Degraded, name+": "+d)
  209. }
  210. cols = append(cols, Column{
  211. Name: name,
  212. Type: ft,
  213. Param: param,
  214. Decl: decl,
  215. TimeKind: sqlTimeKind(dataType),
  216. Nullable: nullable == "YES", // information_schema 的 IS_NULLABLE
  217. })
  218. if key == "PRI" {
  219. pkCols = append(pkCols, name)
  220. }
  221. }
  222. if err := rows.Err(); err != nil {
  223. return nil, "", sourceNotes{}, err
  224. }
  225. if len(pkCols) == 1 {
  226. return cols, pkCols[0], notes, nil
  227. }
  228. if len(pkCols) > 1 {
  229. notes.PK = compositePKNote(table, pkCols)
  230. }
  231. // 复合主键 / 无主键: 都当作"没有单列主键", 由调用方决定补 _id 还是报错。
  232. return cols, "", notes, nil
  233. }
  234. // indexes 读二级索引, 唯一性取 NON_UNIQUE=0。
  235. //
  236. // STATISTICS 里同一个索引每列一行, NON_UNIQUE 各行相同, 用 MAX 收成一行;
  237. // 主键索引 (INDEX_NAME='PRIMARY') 由 PKColumn 表达, 排除在外。
  238. func (d mysqlDial) indexes(db *sql.DB, table string) ([]IndexSchema, error) {
  239. rows, err := db.Query(`SELECT INDEX_NAME, MAX(NON_UNIQUE),
  240. GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX)
  241. FROM information_schema.STATISTICS
  242. WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND INDEX_NAME <> 'PRIMARY'
  243. GROUP BY INDEX_NAME`, table)
  244. if err != nil {
  245. return nil, err
  246. }
  247. defer rows.Close()
  248. var out []IndexSchema
  249. for rows.Next() {
  250. var name, cols string
  251. var nonUnique int
  252. if err := rows.Scan(&name, &nonUnique, &cols); err != nil {
  253. return nil, err
  254. }
  255. out = append(out, IndexSchema{
  256. Name: name,
  257. // SQL 侧分不出 Hash / BTree: 默认按 B+ 树建; 真正的类型由 __vrtx_meta
  258. // 带回来 (见 applyMeta)
  259. Type: page.IndexTypeBTree,
  260. Columns: strings.Split(cols, ","),
  261. Unique: nonUnique == 0,
  262. })
  263. }
  264. return out, rows.Err()
  265. }
  266. func (d mysqlDial) metaDDL() string {
  267. return fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s (
  268. tbl VARCHAR(191) NOT NULL,
  269. payload LONGTEXT NOT NULL,
  270. PRIMARY KEY (tbl)
  271. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`, d.quote(MetaTable))
  272. }
  273. func (d mysqlDial) metaUpsert() string {
  274. return fmt.Sprintf("INSERT INTO %s (tbl, payload) VALUES (?, ?) "+
  275. "ON DUPLICATE KEY UPDATE payload = VALUES(payload)", d.quote(MetaTable))
  276. }
  277. func (d mysqlDial) metaSelect() string {
  278. return fmt.Sprintf("SELECT payload FROM %s WHERE tbl = ? LIMIT 1", d.quote(MetaTable))
  279. }
  280. // maxPlaceholders MySQL 单语句占位符上限是 65535, 留一点余量。
  281. func (d mysqlDial) maxPlaceholders() int { return 60000 }
  282. func (d mysqlDial) textLenExpr(col string) string {
  283. // MySQL 的 LENGTH() 返回【字节】数 (CHAR_LENGTH 才是字符数), 与按字节计容量的
  284. // 宽度码同口径。
  285. return "LENGTH(" + d.quote(col) + ")"
  286. }