package migrate import ( "context" "sort" "strings" "github.com/gogf/gf/v2/database/gdb" ) // ============================== 源库结构内省 ============================== // listTables 查询源库表清单 // // 受白名单过滤,按表名排序以保证结果可复现 func listTables(ctx context.Context, db gdb.DB, whitelist []string) ([]string, error) { tables, err := db.Tables(ctx) if err != nil { return nil, err } if len(whitelist) > 0 { allow := make(map[string]bool, len(whitelist)) for _, t := range whitelist { allow[strings.TrimSpace(t)] = true } filtered := make([]string, 0, len(tables)) for _, t := range tables { if allow[t] { filtered = append(filtered, t) } } tables = filtered } sort.Strings(tables) return tables, nil } // introspectTable 单表全量内省 // // 采集列信息(含注释、默认值、自增、主键)、二级索引与表注释 func introspectTable(ctx context.Context, db gdb.DB, dialect, table string) (*TableMeta, error) { fields, err := db.TableFields(ctx, table) if err != nil { return nil, err } meta := &TableMeta{Name: table} // TableFields 返回 map,按 Index 排序还原物理列序 sorted := make([]*gdb.TableField, 0, len(fields)) for _, f := range fields { sorted = append(sorted, f) } sort.Slice(sorted, func(i, j int) bool { return sorted[i].Index < sorted[j].Index }) for _, f := range sorted { typ, length, scale, enumVals := normalizeType(dialect, f.Type) meta.Columns = append(meta.Columns, ColumnMeta{ Name: f.Name, Type: typ, Length: length, Scale: scale, EnumVals: enumVals, Nullable: f.Null, Default: cleanDefault(dialect, f.Default), AutoInc: isAutoInc(f.Extra), Comment: f.Comment, IsPK: strings.EqualFold(f.Key, "PRI") || strings.EqualFold(f.Key, "pri"), }) } // 索引与表注释(方言相关) switch dialect { case "mysql": if err = introspectIndexesMySQL(ctx, db, table, meta); err != nil { return nil, err } meta.Comment = tableCommentMySQL(ctx, db, table) case "pgsql": if err = introspectIndexesPgsql(ctx, db, table, meta); err != nil { return nil, err } meta.Comment = tableCommentPgsql(ctx, db, table) default: // 其余方言:主键经列元数据已识别,二级索引降级跳过(首期实测链路 mysql→pgsql) } // 统一索引命名(迁移后全库一种规则,见 normalizeIndexName) normalizeIndexes(meta) return meta, nil } // normalizeIndexes 归一化二级索引名 // // 主键随建表落地,不包含在索引集合中 func normalizeIndexes(meta *TableMeta) { for i := range meta.Indexes { idx := &meta.Indexes[i] idx.Name = normalizeIndexName(meta.Name, idx.Unique, idx.Columns) } } // normalizeIndexName 统一索引命名 // // 唯一索引使用 uk_ 前缀,普通索引使用 idx_ 前缀,后接表名与列名; // 名称超过 63 字符时截断,兼容多数数据库的索引名长度上限 func normalizeIndexName(table string, unique bool, cols []string) string { prefix := "idx" if unique { prefix = "uk" } name := prefix + "_" + table + "_" + strings.Join(cols, "_") if len(name) > 63 { name = name[:63] } return name } // isAutoInc 判断是否自增列 // // 兼容 MySQL 的 auto_increment 以及 PostgreSQL、SQL Server 的 identity func isAutoInc(extra string) bool { e := strings.ToLower(extra) return strings.Contains(e, "auto_increment") || strings.Contains(e, "identity") } // introspectIndexesMySQL 采集 MySQL 索引 // // 主键写入列元数据,其余二级索引写入索引集合 func introspectIndexesMySQL(ctx context.Context, db gdb.DB, table string, meta *TableMeta) error { rows, err := db.Ctx(ctx).Raw("SHOW INDEX FROM `" + table + "`").All() if err != nil { return err } type idxHolder struct { unique bool cols []string } holders := map[string]*idxHolder{} order := make([]string, 0) for _, r := range rows { name := r["Key_name"].String() h, ok := holders[name] if !ok { h = &idxHolder{unique: r["Non_unique"].Int() == 0} holders[name] = h order = append(order, name) } h.cols = append(h.cols, r["Column_name"].String()) } for _, name := range order { h := holders[name] if name == "PRIMARY" { for i := range meta.Columns { for _, c := range h.cols { if meta.Columns[i].Name == c { meta.Columns[i].IsPK = true } } } continue } meta.Indexes = append(meta.Indexes, IndexMeta{Unique: h.unique, Name: name, Columns: h.cols}) } return nil } // tableCommentMySQL 查询 MySQL 表注释 func tableCommentMySQL(ctx context.Context, db gdb.DB, table string) string { row, err := db.Ctx(ctx).Raw( "SELECT TABLE_COMMENT FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?", table, ).One() if err != nil || len(row) == 0 { return "" } return row["TABLE_COMMENT"].String() } // introspectIndexesPgsql 采集 PostgreSQL 索引 // // 主键写入列元数据,其余二级索引写入索引集合 func introspectIndexesPgsql(ctx context.Context, db gdb.DB, table string, meta *TableMeta) error { rows, err := db.Ctx(ctx).Raw( "SELECT indexname, indexdef FROM pg_indexes WHERE schemaname = current_schema() AND tablename = ?", table, ).All() if err != nil { return err } for _, r := range rows { name := r["indexname"].String() def := r["indexdef"].String() cols := parsePgIndexCols(def) if len(cols) == 0 { continue } if name == table+"_pkey" { for i := range meta.Columns { for _, c := range cols { if meta.Columns[i].Name == c { meta.Columns[i].IsPK = true } } } continue } meta.Indexes = append(meta.Indexes, IndexMeta{ Unique: strings.Contains(strings.ToUpper(def), "UNIQUE"), Name: name, Columns: cols, }) } return nil } // parsePgIndexCols 从建索引语句中提取列清单 // // 取括号内内容按逗号切分,跳过表达式部分 func parsePgIndexCols(def string) []string { start := strings.Index(def, "(") end := strings.LastIndex(def, ")") if start < 0 || end <= start { return nil } parts := strings.Split(def[start+1:end], ",") cols := make([]string, 0, len(parts)) for _, p := range parts { p = strings.TrimSpace(p) // 表达式索引(如 lower(name))取首标识符,跳过复杂表达式 if i := strings.IndexAny(p, " ("); i > 0 { p = p[:i] } p = strings.Trim(p, `"`) if p != "" { cols = append(cols, p) } } return cols } // tableCommentPgsql 查询 PostgreSQL 表注释 func tableCommentPgsql(ctx context.Context, db gdb.DB, table string) string { row, err := db.Ctx(ctx).Raw( "SELECT obj_description(c.oid) AS comment FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = current_schema() AND c.relname = ?", table, ).One() if err != nil || len(row) == 0 { return "" } return row["comment"].String() }