Files
EasyGoAdmin_GoFrame_EleVue/internal/migrate/introspect.go
T
2026-10-20 11:37:25 +08:00

245 lines
6.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()
}