签入脚本

This commit is contained in:
小蚂蚁云
2026-10-20 11:37:25 +08:00
commit 7f5e8470af
909 changed files with 4045080 additions and 0 deletions
+244
View File
@@ -0,0 +1,244 @@
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()
}