签入版本
This commit is contained in:
@@ -0,0 +1,285 @@
|
||||
// +======================================================================
|
||||
// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2019-2026 半城风雨团队 版权所有
|
||||
// +----------------------------------------------------------------------
|
||||
// | 开源协议: Apache-2.0
|
||||
// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。
|
||||
// +----------------------------------------------------------------------
|
||||
// | 官方网站: https://www.easygoadmin.vip
|
||||
// +----------------------------------------------------------------------
|
||||
// | 研发团队: 小蚂蚁云团队 荣誉出品
|
||||
// +======================================================================
|
||||
// | 版权和免责声明:
|
||||
// | 一、知识产权声明
|
||||
// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、
|
||||
// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权,
|
||||
// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。
|
||||
// | 二、使用限制与禁止条款
|
||||
// | 您理解并同意,禁止将本产品用于以下任何行为:
|
||||
// | 1. 任何违反中华人民共和国法律法规的用途;
|
||||
// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为;
|
||||
// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。
|
||||
// | 三、责任豁免
|
||||
// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失
|
||||
// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三
|
||||
// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。
|
||||
// | 四、违约与法律责任
|
||||
// | 如发现任何违反本声明的行为,本团队有权:
|
||||
// | 1. 无条件配合公安机关及其他有权机关的调查取证工作;
|
||||
// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失;
|
||||
// | 3. 保留进一步采取刑事控告等法律措施的权利。
|
||||
// | 五、许可范围说明
|
||||
// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包
|
||||
// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通
|
||||
// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、
|
||||
// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造
|
||||
// | 成的一切经济损失及商誉损害。
|
||||
// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件
|
||||
// | 许可协议》附件为准。
|
||||
// +======================================================================
|
||||
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user