签入版本
This commit is contained in:
@@ -0,0 +1,846 @@
|
||||
// +======================================================================
|
||||
// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2019-2026 半城风雨团队 版权所有
|
||||
// +----------------------------------------------------------------------
|
||||
// | 开源协议: Apache-2.0
|
||||
// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。
|
||||
// +----------------------------------------------------------------------
|
||||
// | 官方网站: https://www.easygoadmin.vip
|
||||
// +----------------------------------------------------------------------
|
||||
// | 研发团队: 小蚂蚁云团队 荣誉出品
|
||||
// +======================================================================
|
||||
// | 版权和免责声明:
|
||||
// | 一、知识产权声明
|
||||
// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、
|
||||
// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权,
|
||||
// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。
|
||||
// | 二、使用限制与禁止条款
|
||||
// | 您理解并同意,禁止将本产品用于以下任何行为:
|
||||
// | 1. 任何违反中华人民共和国法律法规的用途;
|
||||
// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为;
|
||||
// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。
|
||||
// | 三、责任豁免
|
||||
// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失
|
||||
// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三
|
||||
// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。
|
||||
// | 四、违约与法律责任
|
||||
// | 如发现任何违反本声明的行为,本团队有权:
|
||||
// | 1. 无条件配合公安机关及其他有权机关的调查取证工作;
|
||||
// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失;
|
||||
// | 3. 保留进一步采取刑事控告等法律措施的权利。
|
||||
// | 五、许可范围说明
|
||||
// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包
|
||||
// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通
|
||||
// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、
|
||||
// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造
|
||||
// | 成的一切经济损失及商誉损害。
|
||||
// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件
|
||||
// | 许可协议》附件为准。
|
||||
// +======================================================================
|
||||
|
||||
package migrate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
)
|
||||
|
||||
// ============================== 目标方言 DDL 渲染 ==============================
|
||||
|
||||
// Renderer 目标方言 DDL 渲染器
|
||||
// 负责标识符引用、列定义、建表、建索引、清表与自增序列修正
|
||||
type Renderer interface {
|
||||
Quote(name string) string
|
||||
Column(col ColumnMeta) string
|
||||
CreateTable(t *TableMeta) string
|
||||
CreateIndex(t *TableMeta, idx IndexMeta) string
|
||||
DropAllTables(ctx context.Context, db gdb.DB) error
|
||||
FixSequence(ctx context.Context, db gdb.DB, t *TableMeta) error
|
||||
}
|
||||
|
||||
// NewRenderer 按目标方言创建渲染器
|
||||
func NewRenderer(dialect string) (Renderer, error) {
|
||||
switch dialect {
|
||||
case "mysql":
|
||||
return &mysqlRenderer{dialect: "mysql"}, nil
|
||||
case "pgsql":
|
||||
return &pgsqlRenderer{dialect: "pgsql"}, nil
|
||||
case "sqlite":
|
||||
return &sqliteRenderer{dialect: "sqlite"}, nil
|
||||
case "mssql":
|
||||
return &mssqlRenderer{dialect: "mssql"}, nil
|
||||
case "oracle":
|
||||
return &oracleRenderer{dialect: "oracle"}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("不支持的目标数据库类型: %s", dialect)
|
||||
}
|
||||
|
||||
// quoteStd 双引号引用
|
||||
// 适用于 PostgreSQL、Oracle、SQLite
|
||||
func quoteStd(name string) string {
|
||||
return `"` + strings.ReplaceAll(name, `"`, `""`) + `"`
|
||||
}
|
||||
|
||||
// quoteTick 反引号引用
|
||||
// 适用于 MySQL
|
||||
func quoteTick(name string) string {
|
||||
return "`" + strings.ReplaceAll(name, "`", "``") + "`"
|
||||
}
|
||||
|
||||
// quoteBracket 方括号引用
|
||||
// 适用于 SQL Server
|
||||
func quoteBracket(name string) string {
|
||||
return "[" + strings.ReplaceAll(name, "]", "]]") + "]"
|
||||
}
|
||||
|
||||
// defaultLiteral 渲染默认值字面量
|
||||
//
|
||||
// 数值原样输出,其余加引号;
|
||||
// 时间函数按目标方言归一转换
|
||||
func defaultLiteral(dialect, v string) string {
|
||||
if v == "" {
|
||||
return ""
|
||||
}
|
||||
u := strings.ToUpper(v)
|
||||
// 时间函数按目标方言归一(NOW/CURRENT_TIMESTAMP → 各库原生)
|
||||
if u == "CURRENT_TIMESTAMP" || u == "NOW()" || u == "NOW" {
|
||||
switch dialect {
|
||||
case "mssql":
|
||||
return "GETDATE()"
|
||||
case "oracle":
|
||||
return "SYSDATE"
|
||||
default:
|
||||
return "CURRENT_TIMESTAMP"
|
||||
}
|
||||
}
|
||||
if u == "NULL" {
|
||||
return "NULL"
|
||||
}
|
||||
// 纯数值/负数原样
|
||||
if _, err := fmt.Sscanf(v, "%f", new(float64)); err == nil {
|
||||
return v
|
||||
}
|
||||
return "'" + strings.ReplaceAll(v, "'", "''") + "'"
|
||||
}
|
||||
|
||||
// hasAutoIncCol 判断表中是否存在自增列
|
||||
func hasAutoIncCol(t *TableMeta) bool {
|
||||
for _, c := range t.Columns {
|
||||
if c.AutoInc {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// pkColumns 返回表的主键列清单
|
||||
func pkColumns(t *TableMeta) []string {
|
||||
var cols []string
|
||||
for _, c := range t.Columns {
|
||||
if c.IsPK {
|
||||
cols = append(cols, c.Name)
|
||||
}
|
||||
}
|
||||
return cols
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// MySQL
|
||||
// ============================================================
|
||||
|
||||
// mysqlRenderer MySQL 方言渲染器
|
||||
type mysqlRenderer struct{ dialect string }
|
||||
|
||||
// Quote 引用标识符
|
||||
func (r *mysqlRenderer) Quote(name string) string { return quoteTick(name) }
|
||||
|
||||
// Column 渲染列定义
|
||||
func (r *mysqlRenderer) Column(col ColumnMeta) string {
|
||||
var b strings.Builder
|
||||
b.WriteString(quoteTick(col.Name) + " " + r.colType(col))
|
||||
if col.AutoInc {
|
||||
b.WriteString(" NOT NULL AUTO_INCREMENT")
|
||||
} else {
|
||||
if !col.Nullable {
|
||||
b.WriteString(" NOT NULL")
|
||||
}
|
||||
if d := defaultLiteral(r.dialect, col.Default); d != "" {
|
||||
b.WriteString(" DEFAULT " + d)
|
||||
}
|
||||
}
|
||||
if col.Comment != "" {
|
||||
b.WriteString(" COMMENT '" + strings.ReplaceAll(col.Comment, "'", "''") + "'")
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// colType 列类型映射
|
||||
func (r *mysqlRenderer) colType(col ColumnMeta) string {
|
||||
switch col.Type {
|
||||
case TypeBool:
|
||||
return "tinyint(1)"
|
||||
case TypeTinyint:
|
||||
return "tinyint"
|
||||
case TypeSmallint:
|
||||
return "smallint"
|
||||
case TypeInt:
|
||||
return "int"
|
||||
case TypeBigint:
|
||||
return "bigint"
|
||||
case TypeFloat:
|
||||
return "float"
|
||||
case TypeDouble:
|
||||
return "double"
|
||||
case TypeDecimal:
|
||||
if col.Length > 0 {
|
||||
return fmt.Sprintf("decimal(%d,%d)", col.Length, col.Scale)
|
||||
}
|
||||
return "decimal"
|
||||
case TypeVarchar:
|
||||
if col.Length > 0 {
|
||||
return fmt.Sprintf("varchar(%d)", col.Length)
|
||||
}
|
||||
return "varchar(255)"
|
||||
case TypeChar:
|
||||
if col.Length > 0 {
|
||||
return fmt.Sprintf("char(%d)", col.Length)
|
||||
}
|
||||
return "char(36)"
|
||||
case TypeTinytext:
|
||||
return "tinytext"
|
||||
case TypeMediumText:
|
||||
return "mediumtext"
|
||||
case TypeLongText:
|
||||
return "longtext"
|
||||
case TypeText:
|
||||
return "text"
|
||||
case TypeDate:
|
||||
return "date"
|
||||
case TypeTime:
|
||||
return "time"
|
||||
case TypeDateTime:
|
||||
return "datetime"
|
||||
case TypeBlob:
|
||||
return "longblob"
|
||||
case TypeJson:
|
||||
return "json"
|
||||
case TypeEnum:
|
||||
return "enum('" + strings.Join(col.EnumVals, "','") + "')"
|
||||
case TypeSet:
|
||||
return "set('" + strings.Join(col.EnumVals, "','") + "')"
|
||||
}
|
||||
return "text"
|
||||
}
|
||||
|
||||
// CreateTable 渲染建表语句
|
||||
func (r *mysqlRenderer) CreateTable(t *TableMeta) string {
|
||||
var lines []string
|
||||
for _, c := range t.Columns {
|
||||
lines = append(lines, " "+r.Column(c))
|
||||
}
|
||||
if pk := pkColumns(t); len(pk) > 0 {
|
||||
quoted := make([]string, len(pk))
|
||||
for i, c := range pk {
|
||||
quoted[i] = quoteTick(c)
|
||||
}
|
||||
lines = append(lines, " PRIMARY KEY ("+strings.Join(quoted, ",")+")")
|
||||
}
|
||||
sql := "CREATE TABLE IF NOT EXISTS " + quoteTick(t.Name) + " (\n" + strings.Join(lines, ",\n") + "\n)"
|
||||
if t.Comment != "" {
|
||||
sql += " COMMENT='" + strings.ReplaceAll(t.Comment, "'", "''") + "'"
|
||||
}
|
||||
return sql + ";"
|
||||
}
|
||||
|
||||
// CreateIndex 渲染索引语句
|
||||
func (r *mysqlRenderer) CreateIndex(t *TableMeta, idx IndexMeta) string {
|
||||
unique := ""
|
||||
if idx.Unique {
|
||||
unique = "UNIQUE "
|
||||
}
|
||||
cols := make([]string, len(idx.Columns))
|
||||
for i, c := range idx.Columns {
|
||||
cols[i] = quoteTick(c)
|
||||
}
|
||||
return fmt.Sprintf("CREATE %sINDEX %s ON %s (%s);",
|
||||
unique, quoteTick(idx.Name), quoteTick(t.Name), strings.Join(cols, ","))
|
||||
}
|
||||
|
||||
// DropAllTables 清空全部表
|
||||
func (r *mysqlRenderer) DropAllTables(ctx context.Context, db gdb.DB) error {
|
||||
if _, err := db.Exec(ctx, "SET FOREIGN_KEY_CHECKS=0"); err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Exec(ctx, "SET FOREIGN_KEY_CHECKS=1")
|
||||
tables, err := db.Tables(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, t := range tables {
|
||||
if _, err = db.Exec(ctx, "DROP TABLE IF EXISTS "+quoteTick(t)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FixSequence 修正自增序列
|
||||
// MySQL 自增自动续接,无需处理
|
||||
func (r *mysqlRenderer) FixSequence(ctx context.Context, db gdb.DB, t *TableMeta) error { return nil }
|
||||
|
||||
// ============================================================
|
||||
// PostgreSQL
|
||||
// ============================================================
|
||||
|
||||
// pgsqlRenderer PostgreSQL 方言渲染器
|
||||
type pgsqlRenderer struct{ dialect string }
|
||||
|
||||
// Quote 引用标识符
|
||||
func (r *pgsqlRenderer) Quote(name string) string { return quoteStd(name) }
|
||||
|
||||
// Column 渲染列定义
|
||||
func (r *pgsqlRenderer) Column(col ColumnMeta) string {
|
||||
var b strings.Builder
|
||||
b.WriteString(quoteStd(col.Name) + " " + r.colType(col))
|
||||
if col.AutoInc {
|
||||
b.WriteString(" GENERATED BY DEFAULT AS IDENTITY")
|
||||
}
|
||||
if !col.Nullable && !col.AutoInc {
|
||||
b.WriteString(" NOT NULL")
|
||||
}
|
||||
if !col.AutoInc {
|
||||
if d := defaultLiteral(r.dialect, col.Default); d != "" {
|
||||
b.WriteString(" DEFAULT " + d)
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// colType 列类型映射
|
||||
func (r *pgsqlRenderer) colType(col ColumnMeta) string {
|
||||
switch col.Type {
|
||||
case TypeBool:
|
||||
return "BOOLEAN"
|
||||
case TypeTinyint, TypeSmallint:
|
||||
return "SMALLINT"
|
||||
case TypeInt:
|
||||
return "INTEGER"
|
||||
case TypeBigint:
|
||||
return "BIGINT"
|
||||
case TypeFloat:
|
||||
return "REAL"
|
||||
case TypeDouble:
|
||||
return "DOUBLE PRECISION"
|
||||
case TypeDecimal:
|
||||
if col.Length > 0 {
|
||||
return fmt.Sprintf("NUMERIC(%d,%d)", col.Length, col.Scale)
|
||||
}
|
||||
return "NUMERIC"
|
||||
case TypeVarchar:
|
||||
if col.Length > 0 {
|
||||
return fmt.Sprintf("VARCHAR(%d)", col.Length)
|
||||
}
|
||||
return "VARCHAR"
|
||||
case TypeChar:
|
||||
if col.Length > 0 {
|
||||
return fmt.Sprintf("CHAR(%d)", col.Length)
|
||||
}
|
||||
return "CHAR(36)"
|
||||
case TypeText, TypeTinytext, TypeMediumText, TypeLongText:
|
||||
return "TEXT"
|
||||
case TypeDate:
|
||||
return "DATE"
|
||||
case TypeTime:
|
||||
return "TIME"
|
||||
case TypeDateTime:
|
||||
return "TIMESTAMP"
|
||||
case TypeBlob:
|
||||
return "BYTEA"
|
||||
case TypeJson:
|
||||
return "JSONB"
|
||||
case TypeEnum, TypeSet:
|
||||
return "VARCHAR(255)" // pgsql 无内建 enum/set,降级为 varchar(值域由应用层保证)
|
||||
}
|
||||
return "TEXT"
|
||||
}
|
||||
|
||||
// CreateTable 渲染建表语句
|
||||
func (r *pgsqlRenderer) CreateTable(t *TableMeta) string {
|
||||
var lines []string
|
||||
for _, c := range t.Columns {
|
||||
lines = append(lines, " "+r.Column(c))
|
||||
}
|
||||
if pk := pkColumns(t); len(pk) > 0 {
|
||||
quoted := make([]string, len(pk))
|
||||
for i, c := range pk {
|
||||
quoted[i] = quoteStd(c)
|
||||
}
|
||||
lines = append(lines, " PRIMARY KEY ("+strings.Join(quoted, ",")+")")
|
||||
}
|
||||
sql := "CREATE TABLE IF NOT EXISTS " + quoteStd(t.Name) + " (\n" + strings.Join(lines, ",\n") + "\n);"
|
||||
if t.Comment != "" {
|
||||
sql += "\nCOMMENT ON TABLE " + quoteStd(t.Name) + " IS '" + strings.ReplaceAll(t.Comment, "'", "''") + "';"
|
||||
}
|
||||
// 列注释随建表语句一并输出
|
||||
for _, c := range t.Columns {
|
||||
if c.Comment != "" {
|
||||
sql += "\nCOMMENT ON COLUMN " + quoteStd(t.Name) + "." + quoteStd(c.Name) +
|
||||
" IS '" + strings.ReplaceAll(c.Comment, "'", "''") + "';"
|
||||
}
|
||||
}
|
||||
return sql
|
||||
}
|
||||
|
||||
// CreateIndex 渲染索引语句
|
||||
func (r *pgsqlRenderer) CreateIndex(t *TableMeta, idx IndexMeta) string {
|
||||
unique := ""
|
||||
if idx.Unique {
|
||||
unique = "UNIQUE "
|
||||
}
|
||||
cols := make([]string, len(idx.Columns))
|
||||
for i, c := range idx.Columns {
|
||||
cols[i] = quoteStd(c)
|
||||
}
|
||||
return fmt.Sprintf("CREATE %sINDEX IF NOT EXISTS %s ON %s (%s);",
|
||||
unique, quoteStd(idx.Name), quoteStd(t.Name), strings.Join(cols, ","))
|
||||
}
|
||||
|
||||
// DropAllTables 清空全部表
|
||||
func (r *pgsqlRenderer) DropAllTables(ctx context.Context, db gdb.DB) error {
|
||||
rows, err := db.Ctx(ctx).Raw(
|
||||
"SELECT tablename FROM pg_tables WHERE schemaname = current_schema()").All()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, row := range rows {
|
||||
if _, err = db.Exec(ctx, "DROP TABLE IF EXISTS "+quoteStd(row["tablename"].String())+" CASCADE"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FixSequence 修正自增序列
|
||||
//
|
||||
// 将序列设置到当前 max(id),下次插入取 max+1;
|
||||
// 空表不处理,IDENTITY 从 1 开始
|
||||
func (r *pgsqlRenderer) FixSequence(ctx context.Context, db gdb.DB, t *TableMeta) error {
|
||||
for _, c := range t.Columns {
|
||||
if !c.AutoInc {
|
||||
continue
|
||||
}
|
||||
// setval 到当前 max(id),下次插入取 max+1;空表不动(IDENTITY 从1开始)
|
||||
_, err := db.Exec(ctx, fmt.Sprintf(
|
||||
`SELECT setval(pg_get_serial_sequence('%s', '%s'), COALESCE((SELECT MAX(%s) FROM %s), 0) + 1, false)`,
|
||||
t.Name, c.Name, quoteStd(c.Name), quoteStd(t.Name)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// SQLite
|
||||
// ============================================================
|
||||
|
||||
// sqliteRenderer SQLite 方言渲染器
|
||||
type sqliteRenderer struct{ dialect string }
|
||||
|
||||
// Quote 引用标识符
|
||||
func (r *sqliteRenderer) Quote(name string) string { return quoteStd(name) }
|
||||
|
||||
// Column 渲染列定义
|
||||
func (r *sqliteRenderer) Column(col ColumnMeta) string {
|
||||
var b strings.Builder
|
||||
b.WriteString(quoteStd(col.Name) + " " + r.colType(col))
|
||||
if col.IsPK && col.AutoInc {
|
||||
b.WriteString(" PRIMARY KEY AUTOINCREMENT")
|
||||
} else {
|
||||
if col.IsPK {
|
||||
b.WriteString(" PRIMARY KEY")
|
||||
}
|
||||
if !col.Nullable {
|
||||
b.WriteString(" NOT NULL")
|
||||
}
|
||||
if d := defaultLiteral(r.dialect, col.Default); d != "" {
|
||||
b.WriteString(" DEFAULT " + d)
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// colType 列类型映射
|
||||
func (r *sqliteRenderer) colType(col ColumnMeta) string {
|
||||
switch col.Type {
|
||||
case TypeBool, TypeTinyint, TypeSmallint, TypeInt, TypeBigint, TypeDate, TypeTime, TypeDateTime:
|
||||
return "INTEGER"
|
||||
case TypeFloat, TypeDouble, TypeDecimal:
|
||||
return "REAL"
|
||||
case TypeBlob:
|
||||
return "BLOB"
|
||||
default:
|
||||
return "TEXT"
|
||||
}
|
||||
}
|
||||
|
||||
// CreateTable 渲染建表语句
|
||||
func (r *sqliteRenderer) CreateTable(t *TableMeta) string {
|
||||
var lines []string
|
||||
for _, c := range t.Columns {
|
||||
lines = append(lines, " "+r.Column(c))
|
||||
}
|
||||
// 复合主键走表级约束(列级 PRIMARY KEY 仅单列生效)
|
||||
if pk := pkColumns(t); len(pk) > 1 || !hasAutoIncCol(t) {
|
||||
if pk := pkColumns(t); len(pk) > 0 {
|
||||
quoted := make([]string, len(pk))
|
||||
for i, c := range pk {
|
||||
quoted[i] = quoteStd(c)
|
||||
}
|
||||
lines = append(lines, " PRIMARY KEY ("+strings.Join(quoted, ",")+")")
|
||||
}
|
||||
}
|
||||
// sqlite 无表/列注释
|
||||
return "CREATE TABLE IF NOT EXISTS " + quoteStd(t.Name) + " (\n" + strings.Join(lines, ",\n") + "\n);"
|
||||
}
|
||||
|
||||
// CreateIndex 渲染索引语句
|
||||
func (r *sqliteRenderer) CreateIndex(t *TableMeta, idx IndexMeta) string {
|
||||
unique := ""
|
||||
if idx.Unique {
|
||||
unique = "UNIQUE "
|
||||
}
|
||||
cols := make([]string, len(idx.Columns))
|
||||
for i, c := range idx.Columns {
|
||||
cols[i] = quoteStd(c)
|
||||
}
|
||||
return fmt.Sprintf("CREATE %sINDEX IF NOT EXISTS %s ON %s (%s);",
|
||||
unique, quoteStd(idx.Name), quoteStd(t.Name), strings.Join(cols, ","))
|
||||
}
|
||||
|
||||
// DropAllTables 清空全部表
|
||||
func (r *sqliteRenderer) DropAllTables(ctx context.Context, db gdb.DB) error {
|
||||
rows, err := db.Ctx(ctx).Raw(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").All()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = db.Exec(ctx, "PRAGMA foreign_keys = OFF"); err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Exec(ctx, "PRAGMA foreign_keys = ON")
|
||||
for _, row := range rows {
|
||||
if _, err = db.Exec(ctx, "DROP TABLE IF EXISTS "+quoteStd(row["name"].String())); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FixSequence 修正自增序列
|
||||
// AUTOINCREMENT 自动续接,无需处理
|
||||
func (r *sqliteRenderer) FixSequence(ctx context.Context, db gdb.DB, t *TableMeta) error { return nil }
|
||||
|
||||
// ============================================================
|
||||
// SQL Server(首期实测链路为 mysql→pgsql,mssql/oracle 按定稿方言约定实现,待人工校对)
|
||||
// ============================================================
|
||||
|
||||
// mssqlRenderer SQL Server 方言渲染器
|
||||
type mssqlRenderer struct{ dialect string }
|
||||
|
||||
// Quote 引用标识符
|
||||
func (r *mssqlRenderer) Quote(name string) string { return quoteBracket(name) }
|
||||
|
||||
// Column 渲染列定义
|
||||
func (r *mssqlRenderer) Column(col ColumnMeta) string {
|
||||
var b strings.Builder
|
||||
b.WriteString(quoteBracket(col.Name) + " " + r.colType(col))
|
||||
if col.AutoInc {
|
||||
b.WriteString(" IDENTITY(1,1)")
|
||||
}
|
||||
if !col.Nullable {
|
||||
b.WriteString(" NOT NULL")
|
||||
}
|
||||
if !col.AutoInc {
|
||||
if d := defaultLiteral(r.dialect, col.Default); d != "" {
|
||||
b.WriteString(" DEFAULT " + d)
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// colType 列类型映射
|
||||
func (r *mssqlRenderer) colType(col ColumnMeta) string {
|
||||
switch col.Type {
|
||||
case TypeBool:
|
||||
return "BIT"
|
||||
case TypeTinyint:
|
||||
return "TINYINT"
|
||||
case TypeSmallint:
|
||||
return "SMALLINT"
|
||||
case TypeInt:
|
||||
return "INT"
|
||||
case TypeBigint:
|
||||
return "BIGINT"
|
||||
case TypeFloat:
|
||||
return "FLOAT"
|
||||
case TypeDouble:
|
||||
return "FLOAT"
|
||||
case TypeDecimal:
|
||||
if col.Length > 0 {
|
||||
return fmt.Sprintf("DECIMAL(%d,%d)", col.Length, col.Scale)
|
||||
}
|
||||
return "DECIMAL"
|
||||
case TypeVarchar:
|
||||
if col.Length > 0 {
|
||||
return fmt.Sprintf("NVARCHAR(%d)", col.Length)
|
||||
}
|
||||
return "NVARCHAR(255)"
|
||||
case TypeChar:
|
||||
if col.Length > 0 {
|
||||
return fmt.Sprintf("NCHAR(%d)", col.Length)
|
||||
}
|
||||
return "NCHAR(36)"
|
||||
case TypeText, TypeTinytext, TypeMediumText, TypeLongText, TypeJson, TypeEnum, TypeSet:
|
||||
return "NVARCHAR(MAX)"
|
||||
case TypeDate:
|
||||
return "DATE"
|
||||
case TypeTime:
|
||||
return "TIME"
|
||||
case TypeDateTime:
|
||||
return "DATETIME2"
|
||||
case TypeBlob:
|
||||
return "VARBINARY(MAX)"
|
||||
}
|
||||
return "NVARCHAR(MAX)"
|
||||
}
|
||||
|
||||
// CreateTable 渲染建表语句
|
||||
func (r *mssqlRenderer) CreateTable(t *TableMeta) string {
|
||||
var lines []string
|
||||
for _, c := range t.Columns {
|
||||
lines = append(lines, " "+r.Column(c))
|
||||
}
|
||||
if pk := pkColumns(t); len(pk) > 0 {
|
||||
quoted := make([]string, len(pk))
|
||||
for i, c := range pk {
|
||||
quoted[i] = quoteBracket(c)
|
||||
}
|
||||
lines = append(lines, " PRIMARY KEY ("+strings.Join(quoted, ",")+")")
|
||||
}
|
||||
sql := "IF OBJECT_ID(N'" + t.Name + "', N'U') IS NULL CREATE TABLE " + quoteBracket(t.Name) +
|
||||
" (\n" + strings.Join(lines, ",\n") + "\n);"
|
||||
// 表注释(扩展属性 MS_Description)
|
||||
if t.Comment != "" {
|
||||
sql += "\nEXEC sp_addextendedproperty N'MS_Description', N'" +
|
||||
strings.ReplaceAll(t.Comment, "'", "''") +
|
||||
"', N'SCHEMA', N'dbo', N'TABLE', N'" + t.Name + "';"
|
||||
}
|
||||
// 列注释(扩展属性 MS_Description)
|
||||
for _, c := range t.Columns {
|
||||
if c.Comment != "" {
|
||||
sql += "\nEXEC sp_addextendedproperty N'MS_Description', N'" +
|
||||
strings.ReplaceAll(c.Comment, "'", "''") +
|
||||
"', N'SCHEMA', N'dbo', N'TABLE', N'" + t.Name + "', N'COLUMN', N'" + c.Name + "';"
|
||||
}
|
||||
}
|
||||
return sql
|
||||
}
|
||||
|
||||
// CreateIndex 渲染索引语句
|
||||
func (r *mssqlRenderer) CreateIndex(t *TableMeta, idx IndexMeta) string {
|
||||
unique := ""
|
||||
if idx.Unique {
|
||||
unique = "UNIQUE "
|
||||
}
|
||||
cols := make([]string, len(idx.Columns))
|
||||
for i, c := range idx.Columns {
|
||||
cols[i] = quoteBracket(c)
|
||||
}
|
||||
// IF NOT EXISTS 保护:重复建索引跳过
|
||||
return fmt.Sprintf("IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = N'%s' AND object_id = OBJECT_ID(N'%s')) "+
|
||||
"CREATE %sINDEX %s ON %s (%s);",
|
||||
idx.Name, t.Name, unique, quoteBracket(idx.Name), quoteBracket(t.Name), strings.Join(cols, ","))
|
||||
}
|
||||
|
||||
// DropAllTables 清空全部表
|
||||
func (r *mssqlRenderer) DropAllTables(ctx context.Context, db gdb.DB) error {
|
||||
// 先禁用全部外键约束,再逐表 DROP
|
||||
fks, err := db.Ctx(ctx).Raw(
|
||||
`SELECT OBJECT_NAME(parent_object_id) AS tbl, name AS fk FROM sys.foreign_keys`).All()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, fk := range fks {
|
||||
if _, err = db.Exec(ctx, fmt.Sprintf("ALTER TABLE %s DROP CONSTRAINT %s",
|
||||
quoteBracket(fk["tbl"].String()), quoteBracket(fk["fk"].String()))); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
tables, err := db.Tables(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, t := range tables {
|
||||
if _, err = db.Exec(ctx, "DROP TABLE "+quoteBracket(t)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FixSequence 修正自增序列
|
||||
func (r *mssqlRenderer) FixSequence(ctx context.Context, db gdb.DB, t *TableMeta) error {
|
||||
for _, c := range t.Columns {
|
||||
if !c.AutoInc {
|
||||
continue
|
||||
}
|
||||
if _, err := db.Exec(ctx, fmt.Sprintf(
|
||||
"DBCC CHECKIDENT ('%s', RESEED)", t.Name)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Oracle
|
||||
// ============================================================
|
||||
|
||||
// oracleRenderer Oracle 方言渲染器
|
||||
type oracleRenderer struct{ dialect string }
|
||||
|
||||
// Quote 引用标识符
|
||||
func (r *oracleRenderer) Quote(name string) string { return quoteStd(name) }
|
||||
|
||||
// Column 渲染列定义
|
||||
func (r *oracleRenderer) Column(col ColumnMeta) string {
|
||||
var b strings.Builder
|
||||
b.WriteString(quoteStd(col.Name) + " " + r.colType(col))
|
||||
if col.AutoInc {
|
||||
b.WriteString(" GENERATED BY DEFAULT AS IDENTITY")
|
||||
}
|
||||
if !col.Nullable && !col.AutoInc {
|
||||
b.WriteString(" NOT NULL")
|
||||
}
|
||||
if !col.AutoInc {
|
||||
if d := defaultLiteral(r.dialect, col.Default); d != "" {
|
||||
b.WriteString(" DEFAULT " + d)
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// colType 列类型映射
|
||||
func (r *oracleRenderer) colType(col ColumnMeta) string {
|
||||
switch col.Type {
|
||||
case TypeBool, TypeTinyint, TypeSmallint:
|
||||
return "NUMBER(3)"
|
||||
case TypeInt:
|
||||
return "NUMBER(10)"
|
||||
case TypeBigint:
|
||||
return "NUMBER(19)"
|
||||
case TypeFloat:
|
||||
return "BINARY_FLOAT"
|
||||
case TypeDouble:
|
||||
return "BINARY_DOUBLE"
|
||||
case TypeDecimal:
|
||||
if col.Length > 0 {
|
||||
return fmt.Sprintf("NUMBER(%d,%d)", col.Length, col.Scale)
|
||||
}
|
||||
return "NUMBER"
|
||||
case TypeVarchar:
|
||||
if col.Length > 0 {
|
||||
return fmt.Sprintf("NVARCHAR2(%d)", col.Length)
|
||||
}
|
||||
return "NVARCHAR2(255)"
|
||||
case TypeChar:
|
||||
if col.Length > 0 {
|
||||
return fmt.Sprintf("NCHAR(%d)", col.Length)
|
||||
}
|
||||
return "NCHAR(36)"
|
||||
case TypeText, TypeTinytext, TypeMediumText, TypeLongText, TypeJson, TypeEnum, TypeSet:
|
||||
return "NCLOB"
|
||||
case TypeDate:
|
||||
return "DATE"
|
||||
case TypeTime, TypeDateTime:
|
||||
return "TIMESTAMP"
|
||||
case TypeBlob:
|
||||
return "BLOB"
|
||||
}
|
||||
return "NCLOB"
|
||||
}
|
||||
|
||||
// CreateTable 渲染建表语句
|
||||
func (r *oracleRenderer) CreateTable(t *TableMeta) string {
|
||||
var lines []string
|
||||
for _, c := range t.Columns {
|
||||
lines = append(lines, " "+r.Column(c))
|
||||
}
|
||||
if pk := pkColumns(t); len(pk) > 0 {
|
||||
quoted := make([]string, len(pk))
|
||||
for i, c := range pk {
|
||||
quoted[i] = quoteStd(c)
|
||||
}
|
||||
lines = append(lines, " PRIMARY KEY ("+strings.Join(quoted, ",")+")")
|
||||
}
|
||||
sql := "CREATE TABLE " + quoteStd(t.Name) + " (\n" + strings.Join(lines, ",\n") + "\n)"
|
||||
if t.Comment != "" {
|
||||
sql += "\nCOMMENT ON TABLE " + quoteStd(t.Name) + " IS '" + strings.ReplaceAll(t.Comment, "'", "''") + "'"
|
||||
}
|
||||
for _, c := range t.Columns {
|
||||
if c.Comment != "" {
|
||||
sql += "\nCOMMENT ON COLUMN " + quoteStd(t.Name) + "." + quoteStd(c.Name) +
|
||||
" IS '" + strings.ReplaceAll(c.Comment, "'", "''") + "'"
|
||||
}
|
||||
}
|
||||
return sql + ";"
|
||||
}
|
||||
|
||||
// CreateIndex 渲染索引语句
|
||||
//
|
||||
// Oracle 无 IF NOT EXISTS,由编排层预检索引存在性后决定是否执行
|
||||
func (r *oracleRenderer) CreateIndex(t *TableMeta, idx IndexMeta) string {
|
||||
unique := ""
|
||||
if idx.Unique {
|
||||
unique = "UNIQUE "
|
||||
}
|
||||
cols := make([]string, len(idx.Columns))
|
||||
for i, c := range idx.Columns {
|
||||
cols[i] = quoteStd(c)
|
||||
}
|
||||
// oracle 无 IF NOT EXISTS,由编排层预检索引存在性后决定是否执行
|
||||
return fmt.Sprintf("CREATE %sINDEX %s ON %s (%s);",
|
||||
unique, quoteStd(idx.Name), quoteStd(t.Name), strings.Join(cols, ","))
|
||||
}
|
||||
|
||||
// DropAllTables 清空全部表
|
||||
func (r *oracleRenderer) DropAllTables(ctx context.Context, db gdb.DB) error {
|
||||
rows, err := db.Ctx(ctx).Raw("SELECT table_name FROM user_tables").All()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, row := range rows {
|
||||
if _, err = db.Exec(ctx, "DROP TABLE "+quoteStd(row["table_name"].String())+" CASCADE CONSTRAINTS PURGE"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FixSequence 修正自增序列
|
||||
//
|
||||
// Oracle 的 IDENTITY 由缓存失效自动续接,无需显式修序
|
||||
func (r *oracleRenderer) FixSequence(ctx context.Context, db gdb.DB, t *TableMeta) error {
|
||||
for _, c := range t.Columns {
|
||||
if !c.AutoInc {
|
||||
continue
|
||||
}
|
||||
// oracle identity 由缓存失效自动续接,无需显式修序
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user