245 lines
6.8 KiB
Go
245 lines
6.8 KiB
Go
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()
|
||
}
|