From bd1e83989d39465bac2c28fdef85129049d30c26 Mon Sep 17 00:00:00 2001 From: zongyangleo Date: Fri, 15 Mar 2024 09:01:51 +0000 Subject: [PATCH] =?UTF-8?q?!108=20feat=EF=BC=9A=E6=94=AF=E6=8C=81=E4=B8=8D?= =?UTF-8?q?=E5=90=8C=E6=BA=90=E6=95=B0=E6=8D=AE=E5=BA=93=E8=BF=81=E7=A7=BB?= =?UTF-8?q?=20*=20feat=EF=BC=9A=E6=94=AF=E6=8C=81=E4=B8=8D=E5=90=8C?= =?UTF-8?q?=E6=BA=90=E6=95=B0=E6=8D=AE=E5=BA=93=E8=BF=81=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/views/ops/db/DbTransferEdit.vue | 331 ++++++++++++++++++ .../src/views/ops/db/DbTransferList.vue | 180 ++++++++++ .../src/views/ops/db/DbTransferLog.vue | 117 +++++++ .../src/views/ops/db/SyncTaskEdit.vue | 4 +- mayfly_go_web/src/views/ops/db/api.ts | 8 + mayfly_go_web/src/views/ops/db/db.ts | 2 +- .../src/views/ops/db/dialect/dm_dialect.ts | 2 +- .../views/ops/db/dialect/oracle_dialect.ts | 11 +- server/internal/db/api/db_data_sync.go | 8 +- server/internal/db/api/db_transfer.go | 85 +++++ server/internal/db/api/form/db_transfer.go | 24 ++ server/internal/db/api/vo/db_transfer.go | 37 ++ server/internal/db/application/application.go | 5 + server/internal/db/application/db_transfer.go | 302 ++++++++++++++++ server/internal/db/dbm/dbi/conn.go | 2 +- server/internal/db/dbm/dbi/dialect.go | 37 ++ server/internal/db/dbm/dbi/metadata.go | 2 +- .../internal/db/dbm/dbi/metasql/dm_meta.sql | 29 +- .../db/dbm/dbi/metasql/mssql_meta.sql | 15 + .../db/dbm/dbi/metasql/mysql_meta.sql | 19 + .../db/dbm/dbi/metasql/pgsql_meta.sql | 20 ++ .../db/dbm/dbi/metasql/sqlite_meta.sql | 13 + server/internal/db/dbm/dm/dialect.go | 174 ++++++++- server/internal/db/dbm/dm/metadata.go | 84 ++++- server/internal/db/dbm/mssql/dialect.go | 255 +++++++++++++- server/internal/db/dbm/mssql/metadata.go | 106 +++++- server/internal/db/dbm/mysql/dialect.go | 145 ++++++++ server/internal/db/dbm/mysql/metadata.go | 87 ++++- server/internal/db/dbm/oracle/dialect.go | 215 ++++++++++++ server/internal/db/dbm/oracle/metadata.go | 82 ++++- server/internal/db/dbm/postgres/dialect.go | 234 ++++++++++++- server/internal/db/dbm/postgres/metadata.go | 84 ++++- server/internal/db/dbm/sqlite/dialect.go | 139 +++++++- server/internal/db/dbm/sqlite/meta.go | 8 +- server/internal/db/dbm/sqlite/metadata.go | 80 ++++- .../internal/db/domain/entity/db_transfer.go | 60 ++++ server/internal/db/domain/entity/query.go | 7 + .../db/domain/repository/db_transfer.go | 21 ++ .../infrastructure/persistence/db_transfer.go | 40 +++ .../infrastructure/persistence/persistence.go | 2 + server/internal/db/router/db_transfer.go | 38 ++ server/internal/db/router/router.go | 1 + server/pkg/utils/collx/array.go | 12 + server/resources/script/sql/v1.7/v1.7.5.sql | 41 +++ 44 files changed, 3064 insertions(+), 104 deletions(-) create mode 100644 mayfly_go_web/src/views/ops/db/DbTransferEdit.vue create mode 100644 mayfly_go_web/src/views/ops/db/DbTransferList.vue create mode 100644 mayfly_go_web/src/views/ops/db/DbTransferLog.vue create mode 100644 server/internal/db/api/db_transfer.go create mode 100644 server/internal/db/api/form/db_transfer.go create mode 100644 server/internal/db/api/vo/db_transfer.go create mode 100644 server/internal/db/application/db_transfer.go create mode 100644 server/internal/db/domain/entity/db_transfer.go create mode 100644 server/internal/db/domain/repository/db_transfer.go create mode 100644 server/internal/db/infrastructure/persistence/db_transfer.go create mode 100644 server/internal/db/router/db_transfer.go diff --git a/mayfly_go_web/src/views/ops/db/DbTransferEdit.vue b/mayfly_go_web/src/views/ops/db/DbTransferEdit.vue new file mode 100644 index 00000000..de5c2fc1 --- /dev/null +++ b/mayfly_go_web/src/views/ops/db/DbTransferEdit.vue @@ -0,0 +1,331 @@ + + + + diff --git a/mayfly_go_web/src/views/ops/db/DbTransferList.vue b/mayfly_go_web/src/views/ops/db/DbTransferList.vue new file mode 100644 index 00000000..332fd397 --- /dev/null +++ b/mayfly_go_web/src/views/ops/db/DbTransferList.vue @@ -0,0 +1,180 @@ + + + + diff --git a/mayfly_go_web/src/views/ops/db/DbTransferLog.vue b/mayfly_go_web/src/views/ops/db/DbTransferLog.vue new file mode 100644 index 00000000..833b271a --- /dev/null +++ b/mayfly_go_web/src/views/ops/db/DbTransferLog.vue @@ -0,0 +1,117 @@ + + + diff --git a/mayfly_go_web/src/views/ops/db/SyncTaskEdit.vue b/mayfly_go_web/src/views/ops/db/SyncTaskEdit.vue index 9725fa42..950428cb 100644 --- a/mayfly_go_web/src/views/ops/db/SyncTaskEdit.vue +++ b/mayfly_go_web/src/views/ops/db/SyncTaskEdit.vue @@ -301,7 +301,9 @@ watch(dialogVisible, async (newValue: boolean) => { state.tabActiveName = 'basic'; const propsData = props.data as any; if (!propsData?.id) { - state.form = basicFormData; + let d = {} as FormData; + Object.assign(d, basicFormData); + state.form = d; return; } diff --git a/mayfly_go_web/src/views/ops/db/api.ts b/mayfly_go_web/src/views/ops/db/api.ts index 8b5888f7..a9cce0ea 100644 --- a/mayfly_go_web/src/views/ops/db/api.ts +++ b/mayfly_go_web/src/views/ops/db/api.ts @@ -83,6 +83,14 @@ export const dbApi = { runDatasyncTask: Api.newPost('/datasync/tasks/{taskId}/run'), stopDatasyncTask: Api.newPost('/datasync/tasks/{taskId}/stop'), datasyncLogs: Api.newGet('/datasync/tasks/{taskId}/logs'), + + // 数据库迁移相关 + dbTransferTasks: Api.newGet('/dbTransfer'), + saveDbTransferTask: Api.newPost('/dbTransfer/save'), + deleteDbTransferTask: Api.newDelete('/dbTransfer/{taskId}/del'), + runDbTransferTask: Api.newPost('/dbTransfer/{taskId}/run'), + stopDbTransferTask: Api.newPost('/dbTransfer/{taskId}/stop'), + dbTransferTaskLogs: Api.newGet('/dbTransfer/{taskId}/logs'), }; export const dbSqlExecApi = { diff --git a/mayfly_go_web/src/views/ops/db/db.ts b/mayfly_go_web/src/views/ops/db/db.ts index 5734d574..aef43169 100644 --- a/mayfly_go_web/src/views/ops/db/db.ts +++ b/mayfly_go_web/src/views/ops/db/db.ts @@ -426,7 +426,7 @@ export class DbInst { * @returns */ static isNumber(columnType: string) { - return columnType.match(/int|double|float|number|decimal|byte|bit/gi); + return columnType && columnType.match(/int|double|float|number|decimal|byte|bit/gi); } /** diff --git a/mayfly_go_web/src/views/ops/db/dialect/dm_dialect.ts b/mayfly_go_web/src/views/ops/db/dialect/dm_dialect.ts index 1f84edcd..b2f19932 100644 --- a/mayfly_go_web/src/views/ops/db/dialect/dm_dialect.ts +++ b/mayfly_go_web/src/views/ops/db/dialect/dm_dialect.ts @@ -35,7 +35,6 @@ const DM_TYPE_LIST: sqlColumnType[] = [ // 位串数据类型 BIT 用于存储整数数据 1、0 或 NULL,只有 0 才转换为假,其他非空、非 0 值都会自动转换为真 { udtName: 'BIT', dataType: 'BIT', desc: '用于存储整数数据 1、0 或 NULL', space: '1', range: '1' }, // 一般日期时间数据类型 DATE TIME TIMESTAMP 默认精度 6 - // 多媒体数据类型 TEXT/LONG/LONGVARCHAR 类型:变长字符串类型 IMAGE/LONGVARBINARY 类型 BLOB CLOB BFILE 100G-1 { udtName: 'DATE', dataType: 'DATE', desc: '年、月、日', space: '', range: '' }, { udtName: 'TIME', dataType: 'TIME', desc: '时、分、秒', space: '', range: '' }, { @@ -45,6 +44,7 @@ const DM_TYPE_LIST: sqlColumnType[] = [ space: '', range: '-4712-01-01 00:00:00.000000000 ~ 9999-12-31 23:59:59.999999999', }, + // 多媒体数据类型 TEXT/LONG/LONGVARCHAR 类型:变长字符串类型 IMAGE/LONGVARBINARY 类型 BLOB CLOB BFILE 100G-1 { udtName: 'TEXT', dataType: 'TEXT', desc: '变长字符串', space: '', range: '100G-1' }, { udtName: 'LONG', dataType: 'LONG', desc: '同TEXT', space: '', range: '100G-1' }, { udtName: 'LONGVARCHAR', dataType: 'LONGVARCHAR', desc: '同TEXT', space: '', range: '100G-1' }, diff --git a/mayfly_go_web/src/views/ops/db/dialect/oracle_dialect.ts b/mayfly_go_web/src/views/ops/db/dialect/oracle_dialect.ts index 9e06dc5a..4f18bc44 100644 --- a/mayfly_go_web/src/views/ops/db/dialect/oracle_dialect.ts +++ b/mayfly_go_web/src/views/ops/db/dialect/oracle_dialect.ts @@ -296,7 +296,12 @@ class OracleDialect implements DbDialect { let length = this.getTypeLengthSql(cl); // 默认值 let defVal = this.getDefaultValueSql(cl); - let incr = cl.auto_increment && create ? 'generated by default as IDENTITY' : ''; + let incr = ''; + if (cl.auto_increment && create) { + cl.type = 'number'; + length = ''; + incr = 'generated by default as IDENTITY'; + } // 如果有原名以原名为准 let name = cl.oldName && cl.name !== cl.oldName ? cl.oldName : cl.name; let baseSql = ` ${this.quoteIdentifier(name)} ${cl.type}${length} ${incr}`; @@ -329,10 +334,10 @@ class OracleDialect implements DbDialect { // 主键语句 let prisql = ''; if (pris.length > 0) { - prisql = ` CONSTRAINT "PK_${data.tableName}" PRIMARY KEY (${pris.join(',')});`; + prisql = ` PRIMARY KEY (${pris.join(',')})`; } // 建表 - createSql = `CREATE TABLE ${dbTable} ( ${fields.join(',')} ) ${prisql ? ',' + prisql : ''};`; + createSql = `CREATE TABLE ${dbTable} ( ${fields.join(',')} ${prisql ? ',' + prisql : ''} ) ;`; // 表注释 if (data.tableComment) { tableCommentSql = ` COMMENT ON TABLE ${dbTable} is '${data.tableComment}'; `; diff --git a/server/internal/db/api/db_data_sync.go b/server/internal/db/api/db_data_sync.go index 11463ced..bc11db45 100644 --- a/server/internal/db/api/db_data_sync.go +++ b/server/internal/db/api/db_data_sync.go @@ -75,13 +75,13 @@ func (d *DataSyncTask) ChangeStatus(rc *req.Ctx) { } func (d *DataSyncTask) Run(rc *req.Ctx) { - taskId := getTaskId(rc) + taskId := d.getTaskId(rc) rc.ReqParam = taskId _ = d.DataSyncTaskApp.RunCronJob(taskId) } func (d *DataSyncTask) Stop(rc *req.Ctx) { - taskId := getTaskId(rc) + taskId := d.getTaskId(rc) rc.ReqParam = taskId task := new(entity.DataSyncTask) @@ -91,12 +91,12 @@ func (d *DataSyncTask) Stop(rc *req.Ctx) { } func (d *DataSyncTask) GetTask(rc *req.Ctx) { - taskId := getTaskId(rc) + taskId := d.getTaskId(rc) dbEntity, _ := d.DataSyncTaskApp.GetById(new(entity.DataSyncTask), taskId) rc.ResData = dbEntity } -func getTaskId(rc *req.Ctx) uint64 { +func (d *DataSyncTask) getTaskId(rc *req.Ctx) uint64 { instanceId := rc.PathParamInt("taskId") biz.IsTrue(instanceId > 0, "instanceId 错误") return uint64(instanceId) diff --git a/server/internal/db/api/db_transfer.go b/server/internal/db/api/db_transfer.go new file mode 100644 index 00000000..3c6cacce --- /dev/null +++ b/server/internal/db/api/db_transfer.go @@ -0,0 +1,85 @@ +package api + +import ( + "context" + "fmt" + "mayfly-go/internal/db/api/form" + "mayfly-go/internal/db/api/vo" + "mayfly-go/internal/db/application" + "mayfly-go/internal/db/domain/entity" + "mayfly-go/pkg/biz" + "mayfly-go/pkg/logx" + "mayfly-go/pkg/req" + "strconv" + "strings" +) + +type DbTransferTask struct { + DbTransferTask application.DbTransferTask `inject:"DbTransferTaskApp"` +} + +func (d *DbTransferTask) Tasks(rc *req.Ctx) { + queryCond, page := req.BindQueryAndPage[*entity.DbTransferTaskQuery](rc, new(entity.DbTransferTaskQuery)) + res, err := d.DbTransferTask.GetPageList(queryCond, page, new([]vo.DbTransferTaskListVO)) + biz.ErrIsNil(err) + rc.ResData = res +} + +func (d *DbTransferTask) Logs(rc *req.Ctx) { + queryCond, page := req.BindQueryAndPage[*entity.DbTransferLogQuery](rc, new(entity.DbTransferLogQuery)) + res, err := d.DbTransferTask.GetTaskLogList(queryCond, page, new([]vo.DbTransferLogListVO)) + biz.ErrIsNil(err) + rc.ResData = res +} + +func (d *DbTransferTask) SaveTask(rc *req.Ctx) { + reqForm := &form.DbTransferTaskForm{} + task := req.BindJsonAndCopyTo[*entity.DbTransferTask](rc, reqForm, new(entity.DbTransferTask)) + + rc.ReqParam = reqForm + biz.ErrIsNil(d.DbTransferTask.Save(rc.MetaCtx, task)) +} + +func (d *DbTransferTask) DeleteTask(rc *req.Ctx) { + taskId := rc.PathParam("taskId") + rc.ReqParam = taskId + ids := strings.Split(taskId, ",") + + for _, v := range ids { + value, err := strconv.Atoi(v) + biz.ErrIsNilAppendErr(err, "string类型转换为int异常: %s") + biz.ErrIsNil(d.DbTransferTask.Delete(rc.MetaCtx, uint64(value))) + } +} + +func (d *DbTransferTask) Run(rc *req.Ctx) { + taskId := d.changeState(rc, entity.DbTransferTaskRunStateRunning) + go d.DbTransferTask.Run(taskId, func(msg string, err error) { + // 修改状态为停止 + if err != nil { + logx.Error(msg, err) + } else { + logx.Info(fmt.Sprintf("执行迁移完成,%s", msg)) + } + // 修改任务状态 + task := new(entity.DbTransferTask) + task.Id = taskId + task.RunningState = entity.DbTransferTaskRunStateStop + biz.ErrIsNil(d.DbTransferTask.UpdateById(context.Background(), task)) + }) + +} + +func (d *DbTransferTask) Stop(rc *req.Ctx) { + taskId := d.changeState(rc, entity.DbTransferTaskRunStateStop) + d.DbTransferTask.Stop(taskId) +} + +func (d *DbTransferTask) changeState(rc *req.Ctx, RunningState int) uint64 { + reqForm := &form.DbTransferTaskStatusForm{RunningState: RunningState} + task := req.BindJsonAndCopyTo[*entity.DbTransferTask](rc, reqForm, new(entity.DbTransferTask)) + biz.ErrIsNil(d.DbTransferTask.UpdateById(rc.MetaCtx, task)) + // 记录请求日志 + rc.ReqParam = reqForm + return task.Id +} diff --git a/server/internal/db/api/form/db_transfer.go b/server/internal/db/api/form/db_transfer.go new file mode 100644 index 00000000..f17d42c1 --- /dev/null +++ b/server/internal/db/api/form/db_transfer.go @@ -0,0 +1,24 @@ +package form + +type DbTransferTaskForm struct { + Id uint64 `json:"id"` + CheckedKeys string `binding:"required" json:"checkedKeys"` // 选中需要迁移的表 + DeleteTable int `binding:"required" json:"deleteTable"` // 创建表前是否删除表 1是 2否 + NameCase int `binding:"required" json:"nameCase"` // 表名、字段大小写转换 1无 2大写 3小写 + Strategy int `binding:"required" json:"strategy"` // 迁移策略 1全量 2增量 + SrcDbId int `binding:"required" json:"srcDbId"` // 源库id + SrcDbName string `binding:"required" json:"srcDbName"` // 源库名 + SrcDbType string `binding:"required" json:"srcDbType"` // 源库类型 + SrcInstName string `binding:"required" json:"srcInstName"` // 源库实例名 + SrcTagPath string `binding:"required" json:"srcTagPath"` // 源库tagPath + TargetDbId int `binding:"required" json:"targetDbId"` // 目标库id + TargetDbName string `binding:"required" json:"targetDbName"` // 目标库名 + TargetDbType string `binding:"required" json:"targetDbType"` // 目标库类型 + TargetInstName string `binding:"required" json:"targetInstName"` // 目标库实例名 + TargetTagPath string `binding:"required" json:"targetTagPath"` // 目标库tagPath +} + +type DbTransferTaskStatusForm struct { + Id uint64 `binding:"required" json:"taskId"` + RunningState int `json:"status"` +} diff --git a/server/internal/db/api/vo/db_transfer.go b/server/internal/db/api/vo/db_transfer.go new file mode 100644 index 00000000..e64767d0 --- /dev/null +++ b/server/internal/db/api/vo/db_transfer.go @@ -0,0 +1,37 @@ +package vo + +import "time" + +type DbTransferTaskListVO struct { + Id *int64 `json:"id"` + + UpdateTime *time.Time `json:"updateTime"` + Modifier string `json:"modifier"` + + RunningState int `json:"runningState"` + + CheckedKeys string `json:"checkedKeys"` // 选中需要迁移的表 + DeleteTable int `json:"deleteTable"` // 创建表前是否删除表 + NameCase int `json:"nameCase"` // 表名、字段大小写转换 1无 2大写 3小写 + Strategy int `json:"strategy"` // 迁移策略 1全量 2增量 + + SrcDbId int64 `json:"srcDbId"` // 源库id + SrcDbName string `json:"srcDbName"` // 源库名 + SrcTagPath string `json:"srcTagPath"` // 源库tagPath + SrcDbType string `json:"srcDbType"` // 源库类型 + SrcInstName string `json:"srcInstName"` // 源库实例名 + + TargetDbId int `json:"targetDbId"` // 目标库id + TargetDbName string `json:"targetDbName"` // 目标库名 + TargetDbType string `json:"targetDbType"` // 目标库类型 + TargetInstName string `json:"targetInstName"` // 目标库实例名 + TargetTagPath string `json:"targetTagPath"` // 目标库tagPath +} + +type DbTransferLogListVO struct { + CreateTime *time.Time `json:"createTime"` + DataSqlFull string `json:"dataSqlFull"` + ResNum string `json:"resNum"` + ErrText string `json:"errText"` + Status *int `json:"status"` +} diff --git a/server/internal/db/application/application.go b/server/internal/db/application/application.go index a2fd020d..ea896666 100644 --- a/server/internal/db/application/application.go +++ b/server/internal/db/application/application.go @@ -12,6 +12,7 @@ func InitIoc() { ioc.Register(new(dbSqlExecAppImpl), ioc.WithComponentName("DbSqlExecApp")) ioc.Register(new(dbSqlAppImpl), ioc.WithComponentName("DbSqlApp")) ioc.Register(new(dataSyncAppImpl), ioc.WithComponentName("DbDataSyncTaskApp")) + ioc.Register(new(dbTransferAppImpl), ioc.WithComponentName("DbTransferTaskApp")) ioc.Register(newDbScheduler(), ioc.WithComponentName("DbScheduler")) ioc.Register(new(DbBackupApp), ioc.WithComponentName("DbBackupApp")) @@ -31,6 +32,7 @@ func Init() { panic(fmt.Sprintf("初始化 DbBinlogApp 失败: %v", err)) } GetDataSyncTaskApp().InitCronJob() + GetDbTransferTaskApp().InitJob() InitDbFlowHandler() })() } @@ -54,3 +56,6 @@ func GetDbBinlogApp() *DbBinlogApp { func GetDataSyncTaskApp() DataSyncTask { return ioc.Get[DataSyncTask]("DbDataSyncTaskApp") } +func GetDbTransferTaskApp() DbTransferTask { + return ioc.Get[DbTransferTask]("DbTransferTaskApp") +} diff --git a/server/internal/db/application/db_transfer.go b/server/internal/db/application/db_transfer.go new file mode 100644 index 00000000..c41dc365 --- /dev/null +++ b/server/internal/db/application/db_transfer.go @@ -0,0 +1,302 @@ +package application + +import ( + "context" + "fmt" + "mayfly-go/internal/db/dbm/dbi" + "mayfly-go/internal/db/domain/entity" + "mayfly-go/internal/db/domain/repository" + "mayfly-go/pkg/base" + "mayfly-go/pkg/gormx" + "mayfly-go/pkg/logx" + "mayfly-go/pkg/model" + "strings" +) + +type DbTransferTask interface { + base.App[*entity.DbTransferTask] + + // GetPageList 分页获取数据库实例 + GetPageList(condition *entity.DbTransferTaskQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) + + Save(ctx context.Context, instanceEntity *entity.DbTransferTask) error + + Delete(ctx context.Context, id uint64) error + + InitJob() + + GetTaskLogList(condition *entity.DbTransferLogQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) + + Run(taskId uint64, end func(msg string, err error)) + + Stop(taskId uint64) +} + +type dbTransferAppImpl struct { + base.AppImpl[*entity.DbTransferTask, repository.DbTransferTask] + + dbTransferLogRepo repository.DbTransferLog `inject:"DbTransferLogRepo"` + + dbApp Db `inject:"DbApp"` +} + +func (app *dbTransferAppImpl) InjectDbTransferTaskRepo(repo repository.DbTransferTask) { + app.Repo = repo +} + +func (app *dbTransferAppImpl) GetPageList(condition *entity.DbTransferTaskQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) { + return app.GetRepo().GetTaskList(condition, pageParam, toEntity, orderBy...) +} + +func (app *dbTransferAppImpl) Save(ctx context.Context, taskEntity *entity.DbTransferTask) error { + var err error + if taskEntity.Id == 0 { + err = app.Insert(ctx, taskEntity) + } else { + err = app.UpdateById(ctx, taskEntity) + } + return err +} + +func (app *dbTransferAppImpl) Delete(ctx context.Context, id uint64) error { + if err := app.DeleteById(ctx, id); err != nil { + return err + } + return nil +} + +func (app *dbTransferAppImpl) InitJob() { + // 修改执行状态为待执行 + updateMap := map[string]interface{}{ + "running_state": entity.DbTransferTaskRunStateStop, + } + taskParam := new(entity.DbTransferTask) + taskParam.RunningState = 1 + _ = gormx.Updates(taskParam, taskParam, updateMap) +} + +func (app *dbTransferAppImpl) GetTaskLogList(condition *entity.DbTransferLogQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) { + return app.dbTransferLogRepo.GetTaskLogList(condition, pageParam, toEntity, orderBy...) +} + +func (app *dbTransferAppImpl) Run(taskId uint64, end func(msg string, err error)) { + task, err := app.GetById(new(entity.DbTransferTask), taskId) + if err != nil { + return + } + // 获取源库连接、目标库连接,判断连接可用性,否则记录日志:xx连接不可用 + // 获取源库表信息 + srcConn, err := app.dbApp.GetDbConn(uint64(task.SrcDbId), task.SrcDbName) + if err != nil { + end("获取源库连接失败", err) + return + } + // 获取目标库表信息 + targetConn, err := app.dbApp.GetDbConn(uint64(task.TargetDbId), task.TargetDbName) + if err != nil { + end("获取目标库连接失败", err) + return + } + // 查询出源库表信息 + srcDialect := srcConn.GetDialect() + targetDialect := targetConn.GetDialect() + + var tables []dbi.Table + + if task.CheckedKeys == "all" { + tables, err = srcConn.GetMetaData().GetTables() + if err != nil { + end("获取源表信息失败", err) + return + } + } else { + tableNames := strings.Split(task.CheckedKeys, ",") + tables, err = srcConn.GetMetaData().GetTables(tableNames...) + if err != nil { + end("获取源表信息失败", err) + return + } + } + + // 迁移表 + app.transferTables(task, srcConn, srcDialect, targetConn, targetDialect, tables, end) + + end(fmt.Sprintf("执行迁移任务完成:[%d]", task.Id), nil) +} + +func (app *dbTransferAppImpl) Stop(taskId uint64) { + +} + +func (app *dbTransferAppImpl) recLog(taskId uint64) { + +} + +// 迁移表 +func (app *dbTransferAppImpl) transferTables(task *entity.DbTransferTask, srcConn *dbi.DbConn, srcDialect dbi.Dialect, targetConn *dbi.DbConn, targetDialect dbi.Dialect, tables []dbi.Table, end func(msg string, err error)) { + + tableNames := make([]string, 0) + tableMap := make(map[string]dbi.Table) // 以表名分组,存放表信息 + for _, table := range tables { + tableNames = append(tableNames, table.TableName) + tableMap[table.TableName] = table + } + + if len(tableNames) == 0 { + end("没有需要迁移的表", nil) + return + } + + // 查询源表列信息 + columns, err := srcConn.GetMetaData().GetColumns(tableNames...) + if err != nil { + end("获取源表列信息失败", err) + return + } + // 以表名分组,存放每个表的列信息 + columnMap := make(map[string][]dbi.Column) + for _, column := range columns { + columnMap[column.TableName] = append(columnMap[column.TableName], column) + } + + ctx := context.Background() + + for tbName, cols := range columnMap { + // 在目标库建表 + // 把源列信息转化成公共列信息 + commonColumns := srcDialect.TransColumns(cols) + // 通过公共列信息生成目标库的建表语句,并执行目标库建表 + logx.Infof("开始创建目标表: 表名:%s", tbName) + _, err := targetDialect.CreateTable(commonColumns, tableMap[tbName], true) + if err != nil { + end(fmt.Sprintf("创建目标表失败: 表名:%s, error: %s", tbName, err.Error()), err) + return + } + logx.Infof("创建目标表成功: 表名:%s", tbName) + + // 迁移数据 + logx.Infof("开始迁移数据: 表名:%s", tbName) + total, err := app.transferData(ctx, tbName, srcConn, srcDialect, targetConn, targetDialect) + if err != nil { + end(fmt.Sprintf("迁移数据失败: 表名:%s, error: %s", tbName, err.Error()), err) + return + } + logx.Infof("迁移数据成功: 表名:%s, 数据:%d 条", tbName, total) + + // 有些数据库迁移完数据之后,需要更新表自增序列为当前表最大值 + targetDialect.UpdateSequence(tbName, commonColumns) + + // 迁移索引信息 + logx.Infof("开始迁移索引: 表名:%s", tbName) + err = app.transferIndex(ctx, tableMap[tbName], srcConn, srcDialect, targetConn, targetDialect) + if err != nil { + end(fmt.Sprintf("迁移索引失败: 表名:%s, error: %s", tbName, err.Error()), err) + return + } + logx.Infof("迁移索引成功: 表名:%s", tbName) + + // 记录任务执行日志 + } + + // 修改任务状态 + taskParam := &entity.DbTransferTask{} + taskParam.Id = task.Id + taskParam.RunningState = entity.DbTransferTaskRunStateStop + + if err := app.UpdateById(ctx, task); err != nil { + end("修改任务状态失败", err) + return + } +} + +func (app *dbTransferAppImpl) transferData(ctx context.Context, tableName string, srcConn *dbi.DbConn, srcDialect dbi.Dialect, targetConn *dbi.DbConn, targetDialect dbi.Dialect) (int, error) { + result := make([]map[string]any, 0) + total := 0 // 总条数 + batchSize := 1000 // 每次查询并迁移1000条数据 + var queryColumns []*dbi.QueryColumn + var err error + + // 游标查询源表数据,并批量插入目标表 + err = srcConn.WalkTableRows(ctx, tableName, func(row map[string]any, columns []*dbi.QueryColumn) error { + if len(queryColumns) == 0 { + + for _, col := range columns { + queryColumns = append(queryColumns, &dbi.QueryColumn{ + Name: targetConn.GetMetaData().QuoteIdentifier(col.Name), + Type: col.Type, + }) + } + + } + total++ + result = append(result, row) + if total%batchSize == 0 { + err = app.transfer2Target(targetConn, queryColumns, result, targetDialect, tableName) + if err != nil { + logx.Error("批量插入目标表数据失败", err) + return err + } + result = result[:0] + } + return nil + }) + // 处理剩余的数据 + if len(result) > 0 { + err = app.transfer2Target(targetConn, queryColumns, result, targetDialect, tableName) + if err != nil { + logx.Error(fmt.Sprintf("批量插入目标表数据失败,表名:%s", tableName), err) + return 0, err + } + } + return total, err +} + +func (app *dbTransferAppImpl) transfer2Target(targetConn *dbi.DbConn, cols []*dbi.QueryColumn, result []map[string]any, targetDialect dbi.Dialect, tbName string) error { + tx, err := targetConn.Begin() + if err != nil { + return err + } + // 收集字段名 + var columnNames []string + for _, col := range cols { + columnNames = append(columnNames, col.Name) + } + + // 从目标库数据中取出源库字段对应的值 + values := make([][]any, 0) + for _, record := range result { + rawValue := make([]any, 0) + for _, cn := range columnNames { + rawValue = append(rawValue, record[targetConn.GetMetaData().RemoveQuote(cn)]) + } + values = append(values, rawValue) + } + // 批量插入 + _, err = targetDialect.BatchInsert(tx, tbName, columnNames, values, -1) + + defer func() { + if r := recover(); r != nil { + tx.Rollback() + logx.Error("批量插入目标表数据失败", r) + } + }() + + _ = tx.Commit() + return err +} + +func (app *dbTransferAppImpl) transferIndex(ctx context.Context, tableInfo dbi.Table, srcConn *dbi.DbConn, srcDialect dbi.Dialect, targetConn *dbi.DbConn, targetDialect dbi.Dialect) error { + + // 查询源表索引信息 + indexs, err := srcConn.GetMetaData().GetTableIndex(tableInfo.TableName) + if err != nil { + logx.Error("获取索引信息失败", err) + return err + } + if len(indexs) == 0 { + return nil + } + + // 通过表名、索引信息生成建索引语句,并执行到目标表 + return targetDialect.CreateIndex(tableInfo, indexs) +} diff --git a/server/internal/db/dbm/dbi/conn.go b/server/internal/db/dbm/dbi/conn.go index 717cd8c1..a0948e36 100644 --- a/server/internal/db/dbm/dbi/conn.go +++ b/server/internal/db/dbm/dbi/conn.go @@ -200,7 +200,7 @@ func walkQueryRows(ctx context.Context, db *sql.DB, selectSql string, walkFn Wal rowData[cols[i].Name] = valueConvert(v, colTypes[i]) } if err = walkFn(rowData, cols); err != nil { - logx.Error("游标遍历查询结果集出错,退出遍历: %s", err.Error()) + logx.Errorf("游标遍历查询结果集出错,退出遍历: %s", err.Error()) return err } } diff --git a/server/internal/db/dbm/dbi/dialect.go b/server/internal/db/dbm/dbi/dialect.go index 04471030..b428b470 100644 --- a/server/internal/db/dbm/dbi/dialect.go +++ b/server/internal/db/dbm/dbi/dialect.go @@ -4,6 +4,33 @@ import ( "database/sql" ) +const ( + CommonTypeVarchar string = "varchar" + CommonTypeChar string = "char" + CommonTypeText string = "text" + CommonTypeBlob string = "blob" + CommonTypeLongblob string = "longblob" + CommonTypeLongtext string = "longtext" + CommonTypeBinary string = "binary" + CommonTypeMediumblob string = "mediumblob" + CommonTypeMediumtext string = "mediumtext" + CommonTypeVarbinary string = "varbinary" + + CommonTypeInt string = "int" + CommonTypeSmallint string = "smallint" + CommonTypeTinyint string = "tinyint" + CommonTypeNumber string = "number" + CommonTypeBigint string = "bigint" + + CommonTypeDatetime string = "datetime" + CommonTypeDate string = "date" + CommonTypeTime string = "time" + CommonTypeTimestamp string = "timestamp" + + CommonTypeEnum string = "enum" + CommonTypeJSON string = "json" +) + const ( // -1. 无操作 DuplicateStrategyNone = -1 @@ -32,4 +59,14 @@ type Dialect interface { // 拷贝表 CopyTable(copy *DbCopyTable) error + + CreateTable(commonColumns []Column, tableInfo Table, dropOldTable bool) (int, error) + + CreateIndex(tableInfo Table, indexs []Index) error + + // 把方言类型转换为通用类型 + TransColumns(columns []Column) []Column + + // 有些数据库迁移完数据之后,需要更新表自增序列为当前表最大值 + UpdateSequence(tableName string, columns []Column) } diff --git a/server/internal/db/dbm/dbi/metadata.go b/server/internal/db/dbm/dbi/metadata.go index 554fea8e..5722c718 100644 --- a/server/internal/db/dbm/dbi/metadata.go +++ b/server/internal/db/dbm/dbi/metadata.go @@ -19,7 +19,7 @@ type MetaData interface { GetDbNames() ([]string, error) // 获取表信息 - GetTables() ([]Table, error) + GetTables(tableNames ...string) ([]Table, error) // 获取指定表名的所有列元信息 GetColumns(tableNames ...string) ([]Column, error) diff --git a/server/internal/db/dbm/dbi/metasql/dm_meta.sql b/server/internal/db/dbm/dbi/metasql/dm_meta.sql index 0331b571..8ce62553 100644 --- a/server/internal/db/dbm/dbi/metasql/dm_meta.sql +++ b/server/internal/db/dbm/dbi/metasql/dm_meta.sql @@ -19,19 +19,44 @@ SELECT a.object_name as TABLE_NAME, WHERE OWNER = 'wxb' AND TABLE_NAME = a.object_name)) as INDEX_LENGTH, c.num_rows as TABLE_ROWS - FROM all_objects a LEFT JOIN ALL_TAB_COMMENTS b ON b.TABLE_TYPE = 'TABLE' AND a.object_name = b.TABLE_NAME AND b.owner = a.owner LEFT JOIN (SELECT a.owner, a.table_name, a.num_rows FROM all_tables a) c ON c.owner = a.owner AND c.table_name = a.object_name - WHERE a.owner = (SELECT SF_GET_SCHEMA_NAME_BY_ID(CURRENT_SCHID)) AND a.object_type = 'TABLE' AND a.status = 'VALID' ORDER BY a.object_name --------------------------------------- +--DM_TABLE_INFO_BY_NAMES 表详细信息 +SELECT a.object_name as TABLE_NAME, + b.comments as TABLE_COMMENT, + a.created as CREATE_TIME, + TABLE_USED_SPACE( + (SELECT SF_GET_SCHEMA_NAME_BY_ID(CURRENT_SCHID)), + a.object_name + ) * page() as DATA_LENGTH, + (SELECT sum(INDEX_USED_PAGES(id))* page() + FROM SYSOBJECTS + WHERE NAME IN (SELECT INDEX_NAME + FROM ALL_INDEXES + WHERE OWNER = 'wxb' + AND TABLE_NAME = a.object_name)) as INDEX_LENGTH, + c.num_rows as TABLE_ROWS +FROM all_objects a + LEFT JOIN ALL_TAB_COMMENTS b ON b.TABLE_TYPE = 'TABLE' + AND a.object_name = b.TABLE_NAME + AND b.owner = a.owner + LEFT JOIN (SELECT a.owner, a.table_name, a.num_rows FROM all_tables a) c + ON c.owner = a.owner AND c.table_name = a.object_name +WHERE a.owner = (SELECT SF_GET_SCHEMA_NAME_BY_ID(CURRENT_SCHID)) + AND a.object_type = 'TABLE' + AND a.status = 'VALID' + AND a.object_name in (%s) +ORDER BY a.object_name +--------------------------------------- --DM_INDEX_INFO 表索引信息 select a.index_name as INDEX_NAME, diff --git a/server/internal/db/dbm/dbi/metasql/mssql_meta.sql b/server/internal/db/dbm/dbi/metasql/mssql_meta.sql index 57f99350..2d05eaa4 100644 --- a/server/internal/db/dbm/dbi/metasql/mssql_meta.sql +++ b/server/internal/db/dbm/dbi/metasql/mssql_meta.sql @@ -34,6 +34,21 @@ FROM sys.tables t where ss.name = ? ORDER BY t.name DESC; --------------------------------------- +--MSSQL_TABLE_INFO_BY_NAMES 表详细信息 +SELECT t.name AS tableName, + ss.name AS tableSchema, + c.value AS tableComment, + p.rows AS tableRows, + 0 AS dataLength, + 0 AS indexLength, + t.create_date AS createTime +FROM sys.tables t + left OUTER JOIN sys.schemas ss on t.schema_id = ss.schema_id + left OUTER JOIN sys.partitions p ON t.object_id = p.object_id AND p.index_id = 1 + left OUTER JOIN sys.extended_properties c ON t.object_id = c.major_id AND c.minor_id = 0 AND c.class = 1 +where ss.name = ? and t.name in (%s) +ORDER BY t.name DESC; +--------------------------------------- --MSSQL_INDEX_INFO 索引信息 SELECT ind.name AS indexName, col.name AS columnName, diff --git a/server/internal/db/dbm/dbi/metasql/mysql_meta.sql b/server/internal/db/dbm/dbi/metasql/mysql_meta.sql index 0092c7de..2d67a90b 100644 --- a/server/internal/db/dbm/dbi/metasql/mysql_meta.sql +++ b/server/internal/db/dbm/dbi/metasql/mysql_meta.sql @@ -25,6 +25,25 @@ WHERE ) ORDER BY table_name --------------------------------------- +--MYSQL_TABLE_INFO_BY_NAMES 表详细信息 +SELECT + table_name tableName, + table_comment tableComment, + table_rows tableRows, + data_length dataLength, + index_length indexLength, + create_time createTime +FROM + information_schema.tables +WHERE + table_type = 'BASE TABLE' + AND table_name IN (%s) + AND table_schema = ( + SELECT + database () + ) +ORDER BY table_name +--------------------------------------- --MYSQL_INDEX_INFO 索引信息 SELECT index_name indexName, diff --git a/server/internal/db/dbm/dbi/metasql/pgsql_meta.sql b/server/internal/db/dbm/dbi/metasql/pgsql_meta.sql index 2ea431a8..6c7a9da4 100644 --- a/server/internal/db/dbm/dbi/metasql/pgsql_meta.sql +++ b/server/internal/db/dbm/dbi/metasql/pgsql_meta.sql @@ -31,6 +31,26 @@ where and c.reltype > 0 order by c.relname --------------------------------------- +--PGSQL_TABLE_INFO_BY_NAMES 表详细信息 +select + c.relname as "tableName", + obj_description (c.oid) as "tableComment", + pg_table_size ('"' || n.nspname || '"."' || c.relname || '"') as "dataLength", + pg_indexes_size ('"' || n.nspname || '"."' || c.relname || '"') as "indexLength", + psut.n_live_tup as "tableRows" +from + pg_class c +join pg_namespace n on + c.relnamespace = n.oid +join pg_stat_user_tables psut on + psut.relid = c.oid +where + has_table_privilege(CAST(c.oid AS regclass), 'SELECT') + and n.nspname = current_schema() + and c.reltype > 0 + and c.relname in (%s) +order by c.relname +--------------------------------------- --PGSQL_INDEX_INFO 表索引信息 SELECT indexname AS "indexName", diff --git a/server/internal/db/dbm/dbi/metasql/sqlite_meta.sql b/server/internal/db/dbm/dbi/metasql/sqlite_meta.sql index d29ce4d5..637a610f 100644 --- a/server/internal/db/dbm/dbi/metasql/sqlite_meta.sql +++ b/server/internal/db/dbm/dbi/metasql/sqlite_meta.sql @@ -10,6 +10,19 @@ WHERE type = 'table' and name not like 'sqlite_%' ORDER BY tbl_name --------------------------------------- +--SQLITE_TABLE_INFO_BY_NAMES 表详细信息 +select tbl_name as tableName, + '' as tableComment, + '' as createTime, + 0 as dataLength, + 0 as indexLength, + 0 as tableRows +FROM sqlite_master +WHERE type = 'table' + and name not like 'sqlite_%' + and tbl_name in (%s) +ORDER BY tbl_name +--------------------------------------- --SQLITE_INDEX_INFO 表索引信息 select name as indexName, `sql` as indexSql, diff --git a/server/internal/db/dbm/dm/dialect.go b/server/internal/db/dbm/dm/dialect.go index 30f3b325..17b0bab9 100644 --- a/server/internal/db/dbm/dm/dialect.go +++ b/server/internal/db/dbm/dm/dialect.go @@ -43,13 +43,17 @@ func (dd *DMDialect) batchInsertSimple(tx *sql.Tx, tableName string, columns []s // 去除最后一个逗号,占位符由括号包裹 placeholder := fmt.Sprintf("(%s)", strings.TrimSuffix(repeated, ",")) - sqlTemp := fmt.Sprintf("insert into %s (%s) values %s", dd.dc.GetMetaData().QuoteIdentifier(tableName), strings.Join(columns, ","), placeholder) + identityInsert := fmt.Sprintf("set identity_insert \"%s\" on;", tableName) + + sqlTemp := fmt.Sprintf("%s insert into %s (%s) values %s", identityInsert, dd.dc.GetMetaData().QuoteIdentifier(tableName), strings.Join(columns, ","), placeholder) effRows := 0 + // 设置允许填充自增列之后,显示指定列名可以插入自增列 for _, value := range values { // 达梦数据库只能一条条的执行insert res, err := dd.dc.TxExec(tx, sqlTemp, value...) if err != nil { logx.Errorf("执行sql失败:%s, sql: [ %s ]", err.Error(), sqlTemp) + return 0, err } effRows += int(res) } @@ -147,8 +151,8 @@ func (dd *DMDialect) CopyTable(copy *dbi.DbCopyTable) error { // 复制数据 if copy.CopyData { go func() { - // 设置允许填充自增列之后,显示指定列名可以插入自增列 - _, _ = dd.dc.Exec(fmt.Sprintf("set identity_insert \"%s\" on", newTableName)) + // 设置允许填充自增列之后,显示指定列名可以插入自增列\ + identityInsert := fmt.Sprintf("set identity_insert \"%s\" on", newTableName) // 获取列名 columns, _ := metadata.GetColumns(tableName) columnArr := make([]string, 0) @@ -157,12 +161,168 @@ func (dd *DMDialect) CopyTable(copy *dbi.DbCopyTable) error { } columnStr := strings.Join(columnArr, ",") // 插入新数据并显示指定列 - _, _ = dd.dc.Exec(fmt.Sprintf("insert into \"%s\" (%s) select %s from \"%s\"", newTableName, columnStr, columnStr, tableName)) + _, _ = dd.dc.Exec(fmt.Sprintf("%s insert into \"%s\" (%s) select %s from \"%s\"", identityInsert, newTableName, columnStr, columnStr, tableName)) - // 执行完成后关闭允许填充自增列 - _, _ = dd.dc.Exec(fmt.Sprintf("set identity_insert \"%s\" off", newTableName)) }() } - return err } + +func (dd *DMDialect) TransColumns(columns []dbi.Column) []dbi.Column { + var commonColumns []dbi.Column + for _, column := range columns { + // 取出当前数据库类型 + arr := strings.Split(column.ColumnType, "(") + ctype := arr[0] + + // 翻译为通用数据库类型 + t1 := commonColumnMap[ctype] + if t1 == "" { + ctype = "VARCHAR(2000)" + } else { + // 回写到列信息 + if len(arr) > 1 { + ctype = t1 + "(" + arr[1] + } + } + column.ColumnType = ctype + commonColumns = append(commonColumns, column) + } + return commonColumns +} + +func (dd *DMDialect) CreateTable(commonColumns []dbi.Column, tableInfo dbi.Table, dropOldTable bool) (int, error) { + meta := dd.dc.GetMetaData() + replacer := strings.NewReplacer(";", "", "'", "") + tbName := meta.QuoteIdentifier(tableInfo.TableName) + + if dropOldTable { + _, _ = dd.dc.Exec(fmt.Sprintf("drop table if exists %s", tbName)) + } + // 组装建表语句 + createSql := fmt.Sprintf("create table %s (", tbName) + fields := make([]string, 0) + pks := make([]string, 0) + columnComments := make([]string, 0) + // 把通用类型转换为达梦类型 + for _, column := range commonColumns { + // 取出当前数据库类型 + arr := strings.Split(column.ColumnType, "(") + ctype := arr[0] + // 翻译为通用数据库类型 + t1 := dmColumnMap[ctype] + if t1 == "" { + ctype = "VARCHAR(2000)" + } else { + // 回写到列信息 + if len(arr) > 1 { + ctype = t1 + "(" + arr[1] + } else { + ctype = t1 + } + } + column.ColumnType = ctype + + if column.IsPrimaryKey { + pks = append(pks, meta.QuoteIdentifier(column.ColumnName)) + } + fields = append(fields, dd.genColumnBasicSql(column)) + // 防止注释内含有特殊字符串导致sql出错 + comment := replacer.Replace(column.ColumnComment) + columnComments = append(columnComments, fmt.Sprintf("comment on column %s.%s is '%s'", tbName, meta.QuoteIdentifier(column.ColumnName), comment)) + } + createSql += strings.Join(fields, ",") + if len(pks) > 0 { + createSql += fmt.Sprintf(", PRIMARY KEY (%s)", strings.Join(pks, ",")) + } + createSql += ")" + + tableCommentSql := "" + if tableInfo.TableComment != "" { + // 防止注释内含有特殊字符串导致sql出错 + comment := replacer.Replace(tableInfo.TableComment) + tableCommentSql = fmt.Sprintf(" comment on table %s is '%s'", tbName, comment) + } + + // 达梦需要分开执行sql + var err error + if createSql != "" { + _, err = dd.dc.Exec(createSql) + } + if tableCommentSql != "" { + _, err = dd.dc.Exec(tableCommentSql) + } + if len(columnComments) > 0 { + for _, commentSql := range columnComments { + _, err = dd.dc.Exec(commentSql) + } + } + + return 1, err +} + +func (dd *DMDialect) genColumnBasicSql(column dbi.Column) string { + meta := dd.dc.GetMetaData() + colName := meta.QuoteIdentifier(column.ColumnName) + + incr := "" + if column.IsIdentity { + incr = " IDENTITY" + } + + nullAble := "" + if column.Nullable == "NO" { + nullAble = " NOT NULL" + } + + defVal := "" // 默认值需要判断引号,如函数是不需要引号的 // 为了防止跨源函数不支持 当默认值是函数时,不需要设置默认值 + if column.ColumnDefault != "" && !strings.Contains(column.ColumnDefault, "(") { + // 哪些字段类型默认值需要加引号 + mark := false + if collx.ArrayAnyMatches([]string{"char", "text", "date", "time", "lob"}, strings.ToLower(column.ColumnType)) { + // 当数据类型是日期时间,默认值是日期时间函数时,默认值不需要引号 + if collx.ArrayAnyMatches([]string{"date", "time"}, strings.ToLower(column.ColumnType)) && + collx.ArrayAnyMatches([]string{"DATE", "TIME"}, strings.ToUpper(column.ColumnDefault)) { + mark = false + } else { + mark = true + } + } + if mark { + defVal = fmt.Sprintf(" DEFAULT '%s'", column.ColumnDefault) + } else { + defVal = fmt.Sprintf(" DEFAULT %s", column.ColumnDefault) + } + } + + columnSql := fmt.Sprintf(" %s %s %s %s %s", colName, column.ColumnType, incr, nullAble, defVal) + return columnSql + +} + +func (dd *DMDialect) CreateIndex(tableInfo dbi.Table, indexs []dbi.Index) error { + meta := dd.dc.GetMetaData() + sqls := make([]string, 0) + for _, index := range indexs { + // 通过字段、表名拼接索引名 + columnName := strings.ReplaceAll(index.ColumnName, "-", "") + columnName = strings.ReplaceAll(columnName, "_", "") + colName := strings.ReplaceAll(columnName, ",", "_") + + keyType := "normal" + unique := "" + if index.IsUnique { + keyType = "unique" + unique = "unique" + } + indexName := fmt.Sprintf("%s_key_%s_%s", keyType, tableInfo.TableName, colName) + + sqls = append(sqls, fmt.Sprintf("create %s index %s on %s(%s)", unique, indexName, meta.QuoteIdentifier(tableInfo.TableName), index.ColumnName)) + } + _, err := dd.dc.Exec(strings.Join(sqls, ";")) + return err +} + +func (dd *DMDialect) UpdateSequence(tableName string, columns []dbi.Column) { + +} diff --git a/server/internal/db/dbm/dm/metadata.go b/server/internal/db/dbm/dm/metadata.go index 0eb6de55..e642ff04 100644 --- a/server/internal/db/dbm/dm/metadata.go +++ b/server/internal/db/dbm/dm/metadata.go @@ -12,11 +12,12 @@ import ( ) const ( - DM_META_FILE = "metasql/dm_meta.sql" - DM_DB_SCHEMAS = "DM_DB_SCHEMAS" - DM_TABLE_INFO_KEY = "DM_TABLE_INFO" - DM_INDEX_INFO_KEY = "DM_INDEX_INFO" - DM_COLUMN_MA_KEY = "DM_COLUMN_MA" + DM_META_FILE = "metasql/dm_meta.sql" + DM_DB_SCHEMAS = "DM_DB_SCHEMAS" + DM_TABLE_INFO_KEY = "DM_TABLE_INFO" + DM_INDEX_INFO_KEY = "DM_INDEX_INFO" + DM_COLUMN_MA_KEY = "DM_COLUMN_MA" + DM_TABLE_INFO_BY_NAMES_KEY = "DM_TABLE_INFO_BY_NAMES" ) type DMMetaData struct { @@ -50,14 +51,19 @@ func (dd *DMMetaData) GetDbNames() ([]string, error) { return databases, nil } -// 获取表基础元信息, 如表名等 -func (dd *DMMetaData) GetTables() ([]dbi.Table, error) { +func (dd *DMMetaData) GetTables(tableNames ...string) ([]dbi.Table, error) { + names := strings.Join(collx.ArrayMap[string, string](tableNames, func(val string) string { + return fmt.Sprintf("'%s'", dbi.RemoveQuote(dd, val)) + }), ",") - // 首先执行更新统计信息sql 这个统计信息在数据量比较大的时候就比较耗时,所以最好定时执行 - // _, _, err := pd.dc.Query("dbms_stats.GATHER_SCHEMA_stats(SELECT SF_GET_SCHEMA_NAME_BY_ID(CURRENT_SCHID))") + var res []map[string]any + var err error - // 查询表信息 - _, res, err := dd.dc.Query(dbi.GetLocalSql(DM_META_FILE, DM_TABLE_INFO_KEY)) + if tableNames != nil && len(tableNames) > 0 { + _, res, err = dd.dc.Query(fmt.Sprintf(dbi.GetLocalSql(DM_META_FILE, DM_TABLE_INFO_BY_NAMES_KEY), names)) + } else { + _, res, err = dd.dc.Query(dbi.GetLocalSql(DM_META_FILE, DM_TABLE_INFO_KEY)) + } if err != nil { return nil, err } @@ -254,8 +260,64 @@ var ( dateRegexp = regexp.MustCompile(`(?i)date`) // 时间类型 timeRegexp = regexp.MustCompile(`(?i)time`) + // 定义正则表达式,匹配括号内的数字 + bracketsRegexp = regexp.MustCompile(`\((\d+)\)`) converter = new(DataConverter) + + // 达梦数据类型 对应 公共数据类型 + commonColumnMap = map[string]string{ + + "CHAR": dbi.CommonTypeChar, // 字符数据类型 + "VARCHAR": dbi.CommonTypeVarchar, + "TEXT": dbi.CommonTypeText, + "LONG": dbi.CommonTypeText, + "LONGVARCHAR": dbi.CommonTypeLongtext, + "IMAGE": dbi.CommonTypeLongtext, + "LONGVARBINARY": dbi.CommonTypeLongtext, + "BLOB": dbi.CommonTypeBlob, + "CLOB": dbi.CommonTypeText, + "NUMERIC": dbi.CommonTypeNumber, // 精确数值数据类型 + "DECIMAL": dbi.CommonTypeNumber, + "NUMBER": dbi.CommonTypeNumber, + "INTEGER": dbi.CommonTypeInt, + "INT": dbi.CommonTypeInt, + "BIGINT": dbi.CommonTypeBigint, + "TINYINT": dbi.CommonTypeTinyint, + "BYTE": dbi.CommonTypeTinyint, + "SMALLINT": dbi.CommonTypeSmallint, + "BIT": dbi.CommonTypeTinyint, + "DOUBLE": dbi.CommonTypeNumber, // 近似数值类型 + "FLOAT": dbi.CommonTypeNumber, + "DATE": dbi.CommonTypeDate, // 一般日期时间数据类型 + "TIME": dbi.CommonTypeTime, + "TIMESTAMP": dbi.CommonTypeTimestamp, + } + + // 公共数据类型 对应 达梦数据类型 + dmColumnMap = map[string]string{ + dbi.CommonTypeVarchar: "VARCHAR", + dbi.CommonTypeChar: "CHAR", + dbi.CommonTypeText: "TEXT", + dbi.CommonTypeBlob: "BLOB", + dbi.CommonTypeLongblob: "TEXT", + dbi.CommonTypeLongtext: "TEXT", + dbi.CommonTypeBinary: "TEXT", + dbi.CommonTypeMediumblob: "TEXT", + dbi.CommonTypeMediumtext: "TEXT", + dbi.CommonTypeVarbinary: "TEXT", + dbi.CommonTypeInt: "INT", + dbi.CommonTypeSmallint: "SMALLINT", + dbi.CommonTypeTinyint: "TINYINT", + dbi.CommonTypeNumber: "NUMBER", + dbi.CommonTypeBigint: "BIGINT", + dbi.CommonTypeDatetime: "TIMESTAMP", + dbi.CommonTypeDate: "DATE", + dbi.CommonTypeTime: "DATE", + dbi.CommonTypeTimestamp: "TIMESTAMP", + dbi.CommonTypeEnum: "TEXT", + dbi.CommonTypeJSON: "TEXT", + } ) type DataConverter struct { diff --git a/server/internal/db/dbm/mssql/dialect.go b/server/internal/db/dbm/mssql/dialect.go index fdec4cb2..b001d4b7 100644 --- a/server/internal/db/dbm/mssql/dialect.go +++ b/server/internal/db/dbm/mssql/dialect.go @@ -5,6 +5,7 @@ import ( "fmt" "mayfly-go/internal/db/dbm/dbi" "mayfly-go/pkg/logx" + "mayfly-go/pkg/utils/anyx" "mayfly-go/pkg/utils/collx" "strings" "time" @@ -14,6 +15,17 @@ type MssqlDialect struct { dc *dbi.DbConn } +// 从连接信息中获取数据库和schema信息 +func (md *MssqlDialect) currentSchema() string { + dbName := md.dc.Info.Database + schema := "" + arr := strings.Split(dbName, "/") + if len(arr) == 2 { + schema = arr[1] + } + return schema +} + // GetDbProgram 获取数据库程序模块,用于数据库备份与恢复 func (md *MssqlDialect) GetDbProgram() (dbi.DbProgram, error) { return nil, fmt.Errorf("该数据库类型不支持数据库备份与恢复: %v", md.dc.Info.Type) @@ -29,6 +41,43 @@ func (md *MssqlDialect) BatchInsert(tx *sql.Tx, tableName string, columns []stri } func (md *MssqlDialect) batchInsertSimple(tx *sql.Tx, tableName string, columns []string, values [][]any, duplicateStrategy int) (int64, error) { + // 把二维数组转为一维数组 + var args []any + var singleSize int // 一条数据的参数个数 + for i, v := range values { + if i == 0 { + singleSize = len(v) + } + args = append(args, v...) + } + + // 判断如果参数超过2000,则分批次执行,mssql允许最大参数为2100,保险起见,这里限制到2000 + if len(args) > 2000 { + + rows := 2000 / singleSize // 每批次最大数据条数 + mp := make(map[any][][]any) + + // 把values拆成多份,每份不能超过rows条 + length := len(values) + for i := 0; i < length; i += rows { + if i+rows <= length { + mp[i] = values[i : i+rows] + } else { + mp[i] = values[i:length] + } + } + + var count int64 + for _, v := range mp { + res, err := md.batchInsertSimple(tx, tableName, columns, v, duplicateStrategy) + if err != nil { + return count, err + } + count += res + } + return count, nil + } + msMetadata := md.dc.GetMetaData() schema := md.dc.Info.CurrentSchema() ignoreDupSql := "" @@ -69,11 +118,6 @@ func (md *MssqlDialect) batchInsertSimple(tx *sql.Tx, tableName string, columns sqlStr := fmt.Sprintf("insert into %s (%s) values %s", baseTable, strings.Join(columns, ","), placeholder) // 执行批量insert sql - // 把二维数组转为一维数组 - var args []any - for _, v := range values { - args = append(args, v...) - } // 设置允许填充自增列之后,显示指定列名可以插入自增列 identityInsertOn := fmt.Sprintf("SET IDENTITY_INSERT [%s].[%s] ON", schema, tableName) @@ -206,3 +250,204 @@ func (md *MssqlDialect) CopyTable(copy *dbi.DbCopyTable) error { return err } + +func (md *MssqlDialect) TransColumns(columns []dbi.Column) []dbi.Column { + var commonColumns []dbi.Column + for _, column := range columns { + // 取出当前数据库类型 + arr := strings.Split(column.ColumnType, "(") + ctype := arr[0] + // 翻译为通用数据库类型 + t1 := commonColumnTypeMap[ctype] + if t1 == "" { + ctype = "nvarchar(2000)" + } else { + // 回写到列信息 + if len(arr) > 1 { + ctype = t1 + "(" + arr[1] + } else { + ctype = t1 + } + } + column.ColumnType = ctype + commonColumns = append(commonColumns, column) + } + return commonColumns +} + +func (md *MssqlDialect) CreateTable(commonColumns []dbi.Column, tableInfo dbi.Table, dropOldTable bool) (int, error) { + meta := md.dc.GetMetaData() + replacer := strings.NewReplacer(";", "", "'", "") + if dropOldTable { + _, _ = md.dc.Exec(fmt.Sprintf("DROP TABLE IF EXISTS %s", meta.QuoteIdentifier(tableInfo.TableName))) + } + // 组装建表语句 + createSql := fmt.Sprintf("CREATE TABLE %s (\n", meta.QuoteIdentifier(tableInfo.TableName)) + fields := make([]string, 0) + pks := make([]string, 0) + columnComments := make([]string, 0) + // 把通用类型转换为达梦类型 + for _, column := range commonColumns { + // 取出当前数据库类型 + arr := strings.Split(column.ColumnType, "(") + ctype := arr[0] + // 翻译为通用数据库类型 + t1 := mssqlColumnTypeMap[ctype] + if t1 == "" { + ctype = "nvarchar(2000)" + } else { + // 回写到列信息 + if len(arr) > 1 { + // 如果是int类型不需要指定长度 + if strings.Contains(strings.ToLower(t1), "int") { + ctype = t1 + } else if collx.ArrayAnyMatches([]string{"float", "number", "decimal"}, strings.ToLower(t1)) { + // 如果是float,最大长度为38 + match := bracketsRegexp.FindStringSubmatch(column.ColumnType) + if len(match) > 1 { + // size翻倍, 防止数据超长报错 + size := anyx.ConvInt(match[1]) + if size >= 38 { // 如果长度超过38 + ctype = t1 + "(38)" + } else { + ctype = fmt.Sprintf("%s(%d)", t1, size) + } + } else { + ctype = t1 + "(38)" + } + } else if strings.Contains(strings.ToLower(t1), "char") { + // 如果是字符串类型,长度最大4000,否则修改字段类型为text + match := bracketsRegexp.FindStringSubmatch(column.ColumnType) + if len(match) > 1 { + // size翻倍, 防止数据超长报错 + size := anyx.ConvInt(match[1]) * 2 + + if size >= 4000 { // 如果长度超过4000,则替换为text类型 + ctype = "text" + } else { + ctype = fmt.Sprintf("%s(%d)", t1, size) + } + } else { + ctype = t1 + "(1000)" + } + } else { + ctype = t1 + "(" + arr[1] + } + } else { + ctype = t1 + } + } + column.ColumnType = ctype + + if column.IsPrimaryKey { + pks = append(pks, meta.QuoteIdentifier(column.ColumnName)) + } + fields = append(fields, md.genColumnBasicSql(column)) + commentTmp := "EXECUTE sp_addextendedproperty N'MS_Description', N'%s', N'SCHEMA', N'%s', N'TABLE', N'%s', N'COLUMN', N'%s'" + + // 防止注释内含有特殊字符串导致sql出错 + comment := replacer.Replace(column.ColumnComment) + columnComments = append(columnComments, fmt.Sprintf(commentTmp, comment, md.currentSchema(), column.TableName, column.ColumnName)) + } + createSql += strings.Join(fields, ",") + if len(pks) > 0 { + createSql += fmt.Sprintf(", PRIMARY KEY CLUSTERED (%s)", strings.Join(pks, ",")) + } + createSql += ")" + tableCommentSql := "" + if tableInfo.TableComment != "" { + commentTmp := "EXECUTE sp_addextendedproperty N'MS_Description', N'%s', N'SCHEMA', N'%s', N'TABLE', N'%s'" + tableCommentSql = fmt.Sprintf(commentTmp, replacer.Replace(tableInfo.TableComment), md.currentSchema(), tableInfo.TableName) + } + + columnCommentSql := strings.Join(columnComments, ";") + + sqls := make([]string, 0) + + if createSql != "" { + sqls = append(sqls, createSql) + } + if tableCommentSql != "" { + sqls = append(sqls, tableCommentSql) + } + if columnCommentSql != "" { + sqls = append(sqls, columnCommentSql) + } + + _, err := md.dc.Exec(strings.Join(sqls, ";")) + + return 1, err +} + +func (md *MssqlDialect) genColumnBasicSql(column dbi.Column) string { + meta := md.dc.GetMetaData() + colName := meta.QuoteIdentifier(column.ColumnName) + + incr := "" + if column.IsIdentity { + incr = " IDENTITY(1,1)" + } + + nullAble := "" + if column.Nullable == "NO" { + nullAble = " NOT NULL" + } + + defVal := "" // 默认值需要判断引号,如函数是不需要引号的 // 为了防止跨源函数不支持 当默认值是函数时,不需要设置默认值 + if column.ColumnDefault != "" && !strings.Contains(column.ColumnDefault, "(") { + // 哪些字段类型默认值需要加引号 + mark := false + if collx.ArrayAnyMatches([]string{"char", "text", "date", "time", "lob"}, strings.ToLower(column.ColumnType)) { + // 当数据类型是日期时间,默认值是日期时间函数时,默认值不需要引号 + if collx.ArrayAnyMatches([]string{"date", "time"}, strings.ToLower(column.ColumnType)) && + collx.ArrayAnyMatches([]string{"DATE", "TIME"}, strings.ToUpper(column.ColumnDefault)) { + mark = false + } else { + mark = true + } + } + + if mark { + defVal = fmt.Sprintf(" DEFAULT '%s'", column.ColumnDefault) + } else { + defVal = fmt.Sprintf(" DEFAULT %s", column.ColumnDefault) + } + } + + columnSql := fmt.Sprintf(" %s %s %s %s %s", colName, column.ColumnType, incr, nullAble, defVal) + return columnSql +} + +func (md *MssqlDialect) CreateIndex(tableInfo dbi.Table, indexs []dbi.Index) error { + sqls := make([]string, 0) + comments := make([]string, 0) + for _, index := range indexs { + // 通过字段、表名拼接索引名 + columnName := strings.ReplaceAll(index.ColumnName, "-", "") + columnName = strings.ReplaceAll(columnName, "_", "") + colName := strings.ReplaceAll(columnName, ",", "_") + + keyType := "normal" + unique := "" + if index.IsUnique { + keyType = "unique" + unique = "unique" + } + indexName := fmt.Sprintf("%s_key_%s_%s", keyType, tableInfo.TableName, colName) + + sqls = append(sqls, fmt.Sprintf("create %s NONCLUSTERED index %s on %s.%s(%s)", unique, indexName, md.currentSchema(), tableInfo.TableName, index.ColumnName)) + if index.IndexComment != "" { + comments = append(comments, fmt.Sprintf("EXECUTE sp_addextendedproperty N'MS_Description', N'%s', N'SCHEMA', N'%s', N'TABLE', N'%s', N'INDEX', N'%s'", index.IndexComment, md.currentSchema(), tableInfo.TableName, indexName)) + } + } + _, err := md.dc.Exec(strings.Join(sqls, ";")) + // 添加注释 + if len(comments) > 0 { + _, err = md.dc.Exec(strings.Join(comments, ";")) + } + return err +} + +func (md *MssqlDialect) UpdateSequence(tableName string, columns []dbi.Column) { + +} diff --git a/server/internal/db/dbm/mssql/metadata.go b/server/internal/db/dbm/mssql/metadata.go index 22e787cf..816e7885 100644 --- a/server/internal/db/dbm/mssql/metadata.go +++ b/server/internal/db/dbm/mssql/metadata.go @@ -12,14 +12,15 @@ import ( ) const ( - MSSQL_META_FILE = "metasql/mssql_meta.sql" - MSSQL_DBS_KEY = "MSSQL_DBS" - MSSQL_DB_SCHEMAS_KEY = "MSSQL_DB_SCHEMAS" - MSSQL_TABLE_INFO_KEY = "MSSQL_TABLE_INFO" - MSSQL_INDEX_INFO_KEY = "MSSQL_INDEX_INFO" - MSSQL_COLUMN_MA_KEY = "MSSQL_COLUMN_MA" - MSSQL_TABLE_DETAIL_KEY = "MSSQL_TABLE_DETAIL" - MSSQL_TABLE_INDEX_DDL_KEY = "MSSQL_TABLE_INDEX_DDL" + MSSQL_META_FILE = "metasql/mssql_meta.sql" + MSSQL_DBS_KEY = "MSSQL_DBS" + MSSQL_DB_SCHEMAS_KEY = "MSSQL_DB_SCHEMAS" + MSSQL_TABLE_INFO_KEY = "MSSQL_TABLE_INFO" + MSSQL_TABLE_INFO_BY_NAMES_KEY = "MSSQL_TABLE_INFO_BY_NAMES" + MSSQL_INDEX_INFO_KEY = "MSSQL_INDEX_INFO" + MSSQL_COLUMN_MA_KEY = "MSSQL_COLUMN_MA" + MSSQL_TABLE_DETAIL_KEY = "MSSQL_TABLE_DETAIL" + MSSQL_TABLE_INDEX_DDL_KEY = "MSSQL_TABLE_INDEX_DDL" ) type MssqlMetaData struct { @@ -54,8 +55,21 @@ func (md *MssqlMetaData) GetDbNames() ([]string, error) { } // 获取表基础元信息, 如表名等 -func (md *MssqlMetaData) GetTables() ([]dbi.Table, error) { - _, res, err := md.dc.Query(dbi.GetLocalSql(MSSQL_META_FILE, MSSQL_TABLE_INFO_KEY), md.dc.Info.CurrentSchema()) +func (md *MssqlMetaData) GetTables(tableNames ...string) ([]dbi.Table, error) { + meta := md.dc.GetMetaData() + schema := md.dc.Info.CurrentSchema() + names := strings.Join(collx.ArrayMap[string, string](tableNames, func(val string) string { + return fmt.Sprintf("'%s'", meta.RemoveQuote(val)) + }), ",") + + var res []map[string]any + var err error + + if tableNames != nil || len(tableNames) > 0 { + _, res, err = md.dc.Query(fmt.Sprintf(dbi.GetLocalSql(MSSQL_META_FILE, MSSQL_TABLE_INFO_BY_NAMES_KEY), names), schema) + } else { + _, res, err = md.dc.Query(dbi.GetLocalSql(MSSQL_META_FILE, MSSQL_TABLE_INFO_KEY), schema) + } if err != nil { return nil, err @@ -77,8 +91,9 @@ func (md *MssqlMetaData) GetTables() ([]dbi.Table, error) { // 获取列元信息, 如列名等 func (md *MssqlMetaData) GetColumns(tableNames ...string) ([]dbi.Column, error) { + meta := md.dc.GetMetaData() tableName := strings.Join(collx.ArrayMap[string, string](tableNames, func(val string) string { - return fmt.Sprintf("'%s'", dbi.RemoveQuote(md, val)) + return fmt.Sprintf("'%s'", meta.RemoveQuote(val)) }), ",") _, res, err := md.dc.Query(fmt.Sprintf(dbi.GetLocalSql(MSSQL_META_FILE, MSSQL_COLUMN_MA_KEY), tableName), md.dc.Info.CurrentSchema()) @@ -176,6 +191,8 @@ func (md *MssqlMetaData) CopyTableDDL(tableName string, newTableName string) (st newTableName = tableName } + meta := md.dc.GetMetaData() + // 根据列信息生成建表语句 var builder strings.Builder var commentBuilder strings.Builder @@ -195,7 +212,7 @@ func (md *MssqlMetaData) CopyTableDDL(tableName string, newTableName string) (st } } - baseTable := fmt.Sprintf("%s.%s", dbi.QuoteIdentifier(md, md.dc.Info.CurrentSchema()), dbi.QuoteIdentifier(md, newTableName)) + baseTable := fmt.Sprintf("%s.%s", meta.QuoteIdentifier(md.dc.Info.CurrentSchema()), meta.QuoteIdentifier(newTableName)) // 查询列信息 columns, err := md.GetColumns(tableName) @@ -294,6 +311,71 @@ var ( timeRegexp = regexp.MustCompile(`(?i)time`) converter = new(DataConverter) + // 定义正则表达式,匹配括号内的数字 + bracketsRegexp = regexp.MustCompile(`\((\d+)\)`) + // mssql数据类型 对应 公共数据类型 + commonColumnTypeMap = map[string]string{ + "bigint": dbi.CommonTypeBigint, + "numeric": dbi.CommonTypeNumber, + "bit": dbi.CommonTypeInt, + "smallint": dbi.CommonTypeSmallint, + "decimal": dbi.CommonTypeNumber, + "smallmoney": dbi.CommonTypeNumber, + "int": dbi.CommonTypeInt, + "tinyint": dbi.CommonTypeSmallint, // mssql tinyint不支持负数 + "money": dbi.CommonTypeNumber, + "float": dbi.CommonTypeNumber, // 近似数字 + "real": dbi.CommonTypeVarchar, + "date": dbi.CommonTypeDate, // 日期和时间 + "datetimeoffset": dbi.CommonTypeDatetime, + "datetime2": dbi.CommonTypeDatetime, + "smalldatetime": dbi.CommonTypeDatetime, + "datetime": dbi.CommonTypeDatetime, + "time": dbi.CommonTypeTime, + "char": dbi.CommonTypeChar, // 字符串 + "varchar": dbi.CommonTypeVarchar, + "text": dbi.CommonTypeText, + "nchar": dbi.CommonTypeChar, + "nvarchar": dbi.CommonTypeVarchar, + "ntext": dbi.CommonTypeText, + "binary": dbi.CommonTypeBinary, + "varbinary": dbi.CommonTypeBinary, + "cursor": dbi.CommonTypeVarchar, // 其他 + "rowversion": dbi.CommonTypeVarchar, + "hierarchyid": dbi.CommonTypeVarchar, + "uniqueidentifier": dbi.CommonTypeVarchar, + "sql_variant": dbi.CommonTypeVarchar, + "xml": dbi.CommonTypeText, + "table": dbi.CommonTypeText, + "geometry": dbi.CommonTypeText, // 空间几何类型 + "geography": dbi.CommonTypeText, // 空间地理类型 + } + + // 公共数据类型 对应 mssql数据类型 + + mssqlColumnTypeMap = map[string]string{ + dbi.CommonTypeVarchar: "nvarchar", + dbi.CommonTypeChar: "nchar", + dbi.CommonTypeText: "ntext", + dbi.CommonTypeBlob: "ntext", + dbi.CommonTypeLongblob: "ntext", + dbi.CommonTypeLongtext: "ntext", + dbi.CommonTypeBinary: "varbinary", + dbi.CommonTypeMediumblob: "ntext", + dbi.CommonTypeMediumtext: "ntext", + dbi.CommonTypeVarbinary: "varbinary", + dbi.CommonTypeInt: "int", + dbi.CommonTypeSmallint: "smallint", + dbi.CommonTypeTinyint: "smallint", + dbi.CommonTypeNumber: "decimal", + dbi.CommonTypeBigint: "bigint", + dbi.CommonTypeDatetime: "datetime2", + dbi.CommonTypeDate: "date", + dbi.CommonTypeTime: "time", + dbi.CommonTypeTimestamp: "timestamp", + dbi.CommonTypeEnum: "nvarchar", + dbi.CommonTypeJSON: "nvarchar", + } ) type DataConverter struct { diff --git a/server/internal/db/dbm/mysql/dialect.go b/server/internal/db/dbm/mysql/dialect.go index d342db79..e65c74dc 100644 --- a/server/internal/db/dbm/mysql/dialect.go +++ b/server/internal/db/dbm/mysql/dialect.go @@ -4,6 +4,7 @@ import ( "database/sql" "fmt" "mayfly-go/internal/db/dbm/dbi" + "mayfly-go/pkg/utils/collx" "strings" "time" ) @@ -49,6 +50,10 @@ func (md *MysqlDialect) BatchInsert(tx *sql.Tx, tableName string, columns []stri return md.dc.TxExec(tx, sqlStr, args...) } +func (md *MysqlDialect) GetDataConverter() dbi.DataConverter { + return converter +} + func (md *MysqlDialect) CopyTable(copy *dbi.DbCopyTable) error { tableName := copy.TableName @@ -70,3 +75,143 @@ func (md *MysqlDialect) CopyTable(copy *dbi.DbCopyTable) error { } return err } + +func (md *MysqlDialect) TransColumns(columns []dbi.Column) []dbi.Column { + var commonColumns []dbi.Column + for _, column := range columns { + // 取出当前数据库类型 + arr := strings.Split(column.ColumnType, "(") + ctype := arr[0] + // 翻译为通用数据库类型 + t1 := commonColumnTypeMap[ctype] + if t1 == "" { + ctype = "varchar(2000)" + } else { + // 回写到列信息 + if len(arr) > 1 { + ctype = t1 + "(" + arr[1] + } else { + ctype = t1 + } + } + column.ColumnType = ctype + commonColumns = append(commonColumns, column) + } + return commonColumns +} + +func (md *MysqlDialect) CreateTable(commonColumns []dbi.Column, tableInfo dbi.Table, dropOldTable bool) (int, error) { + if dropOldTable { + _, _ = md.dc.Exec(fmt.Sprintf("DROP TABLE IF EXISTS %s", tableInfo.TableName)) + } + // 组装建表语句 + createSql := fmt.Sprintf("CREATE TABLE %s (\n", tableInfo.TableName) + fields := make([]string, 0) + pks := make([]string, 0) + // 把通用类型转换为达梦类型 + for _, column := range commonColumns { + // 取出当前数据库类型 + arr := strings.Split(column.ColumnType, "(") + ctype := arr[0] + // 翻译为通用数据库类型 + t1 := mysqlColumnTypeMap[ctype] + if t1 == "" { + ctype = "varchar(2000)" + } else { + // 回写到列信息 + if len(arr) > 1 { + ctype = t1 + "(" + arr[1] + } + } + column.ColumnType = ctype + + if column.IsPrimaryKey { + pks = append(pks, column.ColumnName) + } + fields = append(fields, md.genColumnBasicSql(column)) + } + createSql += strings.Join(fields, ",") + if len(pks) > 0 { + createSql += fmt.Sprintf(", PRIMARY KEY (%s)", strings.Join(pks, ",")) + } + createSql += fmt.Sprintf(") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ") + if tableInfo.TableComment != "" { + replacer := strings.NewReplacer(";", "", "'", "") + createSql += fmt.Sprintf(" COMMENT '%s'", replacer.Replace(tableInfo.TableComment)) + } + _, err := md.dc.Exec(createSql) + + return 1, err +} + +func (md *MysqlDialect) genColumnBasicSql(column dbi.Column) string { + + incr := "" + if column.IsIdentity { + incr = " AUTO_INCREMENT" + } + + nullAble := "" + if column.Nullable == "NO" { + nullAble = " NOT NULL" + } + + defVal := "" // 默认值需要判断引号,如函数是不需要引号的 // 为了防止跨源函数不支持 当默认值是函数时,不需要设置默认值 + if column.ColumnDefault != "" && !strings.Contains(column.ColumnDefault, "(") { + // 哪些字段类型默认值需要加引号 + mark := false + if collx.ArrayAnyMatches([]string{"char", "text", "date", "time", "lob"}, strings.ToLower(column.ColumnType)) { + // 当数据类型是日期时间,默认值是日期时间函数时,默认值不需要引号 + if collx.ArrayAnyMatches([]string{"date", "time"}, strings.ToLower(column.ColumnType)) && + collx.ArrayAnyMatches([]string{"DATE", "TIME"}, strings.ToUpper(column.ColumnDefault)) { + mark = false + } else { + mark = true + } + } + if mark { + defVal = fmt.Sprintf(" DEFAULT '%s'", column.ColumnDefault) + } else { + defVal = fmt.Sprintf(" DEFAULT %s", column.ColumnDefault) + } + } + comment := "" + if column.ColumnComment != "" { + // 防止注释内含有特殊字符串导致sql出错 + replacer := strings.NewReplacer(";", "", "'", "") + commentStr := replacer.Replace(column.ColumnComment) + comment = fmt.Sprintf(" COMMENT '%s'", commentStr) + } + + columnSql := fmt.Sprintf(" %s %s %s %s %s %s", md.dc.GetMetaData().QuoteIdentifier(column.ColumnName), column.ColumnType, nullAble, incr, defVal, comment) + return columnSql +} + +func (md *MysqlDialect) CreateIndex(tableInfo dbi.Table, indexs []dbi.Index) error { + meta := md.dc.GetMetaData() + for _, index := range indexs { + // 通过字段、表名拼接索引名 + columnName := strings.ReplaceAll(index.ColumnName, "-", "") + columnName = strings.ReplaceAll(columnName, "_", "") + colName := strings.ReplaceAll(columnName, ",", "_") + + keyType := "normal" + unique := "" + if index.IsUnique { + keyType = "unique" + unique = "unique" + } + indexName := fmt.Sprintf("%s_key_%s_%s", keyType, tableInfo.TableName, colName) + sqlTmp := "ALTER TABLE %s ADD %s INDEX %s(%s) USING BTREE COMMENT '%s'" + replacer := strings.NewReplacer(";", "", "'", "") + _, err := md.dc.Exec(fmt.Sprintf(sqlTmp, meta.QuoteIdentifier(tableInfo.TableName), unique, indexName, index.ColumnName, replacer.Replace(index.IndexComment))) + if err != nil { + return err + } + } + return nil +} + +func (md *MysqlDialect) UpdateSequence(tableName string, columns []dbi.Column) { + +} diff --git a/server/internal/db/dbm/mysql/metadata.go b/server/internal/db/dbm/mysql/metadata.go index f0463192..f4ece291 100644 --- a/server/internal/db/dbm/mysql/metadata.go +++ b/server/internal/db/dbm/mysql/metadata.go @@ -15,11 +15,12 @@ import ( ) const ( - MYSQL_META_FILE = "metasql/mysql_meta.sql" - MYSQL_DBS = "MYSQL_DBS" - MYSQL_TABLE_INFO_KEY = "MYSQL_TABLE_INFO" - MYSQL_INDEX_INFO_KEY = "MYSQL_INDEX_INFO" - MYSQL_COLUMN_MA_KEY = "MYSQL_COLUMN_MA" + MYSQL_META_FILE = "metasql/mysql_meta.sql" + MYSQL_DBS = "MYSQL_DBS" + MYSQL_TABLE_INFO_KEY = "MYSQL_TABLE_INFO" + MYSQL_TABLE_INFO_BY_NAMES_KEY = "MYSQL_TABLE_INFO_BY_NAMES" + MYSQL_INDEX_INFO_KEY = "MYSQL_INDEX_INFO" + MYSQL_COLUMN_MA_KEY = "MYSQL_COLUMN_MA" ) type MysqlMetaData struct { @@ -52,9 +53,21 @@ func (md *MysqlMetaData) GetDbNames() ([]string, error) { return databases, nil } -// 获取表基础元信息, 如表名等 -func (md *MysqlMetaData) GetTables() ([]dbi.Table, error) { - _, res, err := md.dc.Query(dbi.GetLocalSql(MYSQL_META_FILE, MYSQL_TABLE_INFO_KEY)) +func (md *MysqlMetaData) GetTables(tableNames ...string) ([]dbi.Table, error) { + meta := md.dc.GetMetaData() + names := strings.Join(collx.ArrayMap[string, string](tableNames, func(val string) string { + return fmt.Sprintf("'%s'", meta.RemoveQuote(val)) + }), ",") + + var res []map[string]any + var err error + + if tableNames != nil || len(tableNames) > 0 { + _, res, err = md.dc.Query(fmt.Sprintf(dbi.GetLocalSql(MYSQL_META_FILE, MYSQL_TABLE_INFO_BY_NAMES_KEY), names)) + } else { + _, res, err = md.dc.Query(dbi.GetLocalSql(MYSQL_META_FILE, MYSQL_TABLE_INFO_KEY)) + } + if err != nil { return nil, err } @@ -75,8 +88,9 @@ func (md *MysqlMetaData) GetTables() ([]dbi.Table, error) { // 获取列元信息, 如列名等 func (md *MysqlMetaData) GetColumns(tableNames ...string) ([]dbi.Column, error) { + meta := md.dc.GetMetaData() tableName := strings.Join(collx.ArrayMap[string, string](tableNames, func(val string) string { - return fmt.Sprintf("'%s'", dbi.RemoveQuote(md, val)) + return fmt.Sprintf("'%s'", meta.RemoveQuote(val)) }), ",") _, res, err := md.dc.Query(fmt.Sprintf(dbi.GetLocalSql(MYSQL_META_FILE, MYSQL_COLUMN_MA_KEY), tableName)) @@ -89,7 +103,7 @@ func (md *MysqlMetaData) GetColumns(tableNames ...string) ([]dbi.Column, error) columns = append(columns, dbi.Column{ TableName: anyx.ConvString(re["tableName"]), ColumnName: anyx.ConvString(re["columnName"]), - ColumnType: anyx.ConvString(re["columnType"]), + ColumnType: strings.Replace(anyx.ConvString(re["columnType"]), " unsigned", "", 1), ColumnComment: anyx.ConvString(re["columnComment"]), Nullable: anyx.ConvString(re["nullable"]), IsPrimaryKey: anyx.ConvInt(re["isPrimaryKey"]) == 1, @@ -199,6 +213,59 @@ var ( timeRegexp = regexp.MustCompile(`(?i)time`) converter = new(DataConverter) + + // mysql数据类型 映射 公共数据类型 + commonColumnTypeMap = map[string]string{ + "bigint": dbi.CommonTypeBigint, + "binary": dbi.CommonTypeBinary, + "blob": dbi.CommonTypeBlob, + "char": dbi.CommonTypeChar, + "datetime": dbi.CommonTypeDatetime, + "date": dbi.CommonTypeDate, + "decimal": dbi.CommonTypeNumber, + "double": dbi.CommonTypeNumber, + "enum": dbi.CommonTypeEnum, + "float": dbi.CommonTypeNumber, + "int": dbi.CommonTypeInt, + "json": dbi.CommonTypeJSON, + "longblob": dbi.CommonTypeLongblob, + "longtext": dbi.CommonTypeLongtext, + "mediumblob": dbi.CommonTypeBlob, + "mediumtext": dbi.CommonTypeText, + "set": dbi.CommonTypeVarchar, + "smallint": dbi.CommonTypeSmallint, + "text": dbi.CommonTypeText, + "time": dbi.CommonTypeTime, + "timestamp": dbi.CommonTypeTimestamp, + "tinyint": dbi.CommonTypeTinyint, + "varbinary": dbi.CommonTypeVarbinary, + "varchar": dbi.CommonTypeVarchar, + } + + // 公共数据类型 映射 mysql数据类型 + mysqlColumnTypeMap = map[string]string{ + dbi.CommonTypeVarchar: "varchar", + dbi.CommonTypeChar: "char", + dbi.CommonTypeText: "text", + dbi.CommonTypeBlob: "blob", + dbi.CommonTypeLongblob: "longblob", + dbi.CommonTypeLongtext: "longtext", + dbi.CommonTypeBinary: "binary", + dbi.CommonTypeMediumblob: "blob", + dbi.CommonTypeMediumtext: "text", + dbi.CommonTypeVarbinary: "varbinary", + dbi.CommonTypeInt: "int", + dbi.CommonTypeSmallint: "smallint", + dbi.CommonTypeTinyint: "tinyint", + dbi.CommonTypeNumber: "decimal", + dbi.CommonTypeBigint: "bigint", + dbi.CommonTypeDatetime: "datetime", + dbi.CommonTypeDate: "date", + dbi.CommonTypeTime: "time", + dbi.CommonTypeTimestamp: "timestamp", + dbi.CommonTypeEnum: "enum", + dbi.CommonTypeJSON: "json", + } ) type DataConverter struct { diff --git a/server/internal/db/dbm/oracle/dialect.go b/server/internal/db/dbm/oracle/dialect.go index bd542e33..6e2ccdfc 100644 --- a/server/internal/db/dbm/oracle/dialect.go +++ b/server/internal/db/dbm/oracle/dialect.go @@ -5,6 +5,7 @@ import ( "fmt" "mayfly-go/internal/db/dbm/dbi" "mayfly-go/pkg/logx" + "mayfly-go/pkg/utils/anyx" "mayfly-go/pkg/utils/collx" "strings" "time" @@ -157,3 +158,217 @@ func (od *OracleDialect) CopyTable(copy *dbi.DbCopyTable) error { _, err := od.dc.Exec(fmt.Sprintf("create table \"%s\" as select * from \"%s\" %s", newTableName, copy.TableName, condition)) return err } + +func (od *OracleDialect) TransColumns(columns []dbi.Column) []dbi.Column { + var commonColumns []dbi.Column + for _, column := range columns { + // 取出当前数据库类型 + arr := strings.Split(column.ColumnType, "(") + ctype := arr[0] + + // 翻译为通用数据库类型 + t1 := commonColumnTypeMap[ctype] + if t1 == "" { + ctype = "NVARCHAR2(2000)" + } else { + // 回写到列信息 + if t1 == "NUMBER" { + // 如果是转number类型,需要根据公共类型加上长度, 如 bigint 需要转换为number(19,0) + if column.ColumnType == dbi.CommonTypeBigint { + ctype = t1 + "(19, 0)" + } else { + ctype = t1 + } + } else if t1 != "NUMBER" && len(arr) > 1 { + ctype = t1 + "(" + arr[1] + } else { + ctype = t1 + } + } + column.ColumnType = ctype + commonColumns = append(commonColumns, column) + } + return commonColumns +} + +func (od *OracleDialect) CreateTable(commonColumns []dbi.Column, tableInfo dbi.Table, dropOldTable bool) (int, error) { + meta := od.dc.GetMetaData() + replacer := strings.NewReplacer(";", "", "'", "") + quoteTableName := meta.QuoteIdentifier(tableInfo.TableName) + if dropOldTable { + // 如果表存在,先删除表 + dropSqlTmp := ` +declare + num number; +begin + select count(1) into num from user_tables where table_name = '%s' and owner = (SELECT sys_context('USERENV', 'CURRENT_SCHEMA') FROM dual) ; + if num > 0 then + execute immediate 'drop table "%s"' ; + end if; +end; +` + _, _ = od.dc.Exec(fmt.Sprintf(dropSqlTmp, tableInfo.TableName, tableInfo.TableName)) + } + // 组装建表语句 + createSql := fmt.Sprintf("CREATE TABLE %s (", quoteTableName) + fields := make([]string, 0) + pks := make([]string, 0) + columnComments := make([]string, 0) + // 把通用类型转换为达梦类型 + for _, column := range commonColumns { + // 取出当前数据库类型 + arr := strings.Split(column.ColumnType, "(") + ctype := arr[0] + // 翻译为通用数据库类型 + t1 := oracleColumnTypeMap[ctype] + if t1 == "" { + ctype = "NVARCHAR2(2000)" + } else { + // 回写到列信息 + if len(arr) > 1 { + // 如果是字符串类型,长度最大4000,否则修改字段类型为clob + if strings.Contains(strings.ToLower(t1), "char") { + match := bracketsRegexp.FindStringSubmatch(column.ColumnType) + if len(match) > 1 { + size := anyx.ConvInt(match[1]) + if size >= 4000 { // 如果长度超过4000,则替换为text类型 + ctype = "CLOB" + } else { + ctype = fmt.Sprintf("%s(%d)", t1, size) + } + } else { + ctype = t1 + "(2000)" + } + } else { + ctype = t1 + "(" + arr[1] + } + } else { + ctype = t1 + } + } + column.ColumnType = ctype + + if column.IsPrimaryKey { + pks = append(pks, meta.QuoteIdentifier(column.ColumnName)) + } + fields = append(fields, od.genColumnBasicSql(column)) + // 防止注释内含有特殊字符串导致sql出错 + comment := replacer.Replace(column.ColumnComment) + if comment != "" { + columnComments = append(columnComments, fmt.Sprintf("COMMENT ON COLUMN %s.%s IS '%s'", quoteTableName, meta.QuoteIdentifier(column.ColumnName), comment)) + } + } + createSql += strings.Join(fields, ",") + if len(pks) > 0 { + createSql += fmt.Sprintf(", PRIMARY KEY (%s)", strings.Join(pks, ",")) + } + createSql += ")" + + tableCommentSql := "" + if tableInfo.TableComment != "" { + tableCommentSql = fmt.Sprintf(" COMMENT ON TABLE %s is '%s'", meta.QuoteIdentifier(tableInfo.TableName), replacer.Replace(tableInfo.TableComment)) + } + + // 需要分开执行sql + var err error + if createSql != "" { + _, err = od.dc.Exec(createSql) + } + if tableCommentSql != "" { + _, err = od.dc.Exec(tableCommentSql) + } + if len(columnComments) > 0 { + for _, commentSql := range columnComments { + _, err = od.dc.Exec(commentSql) + } + } + + return 1, err +} + +func (od *OracleDialect) genColumnBasicSql(column dbi.Column) string { + meta := od.dc.GetMetaData() + colName := meta.QuoteIdentifier(column.ColumnName) + + if column.IsIdentity { + // 如果是自增,不需要设置默认值和空值,自增列数据类型必须是number + return fmt.Sprintf(" %s NUMBER generated by default as IDENTITY", colName) + } + + nullAble := "" + if column.Nullable == "NO" { + nullAble = " NOT NULL" + } + + defVal := "" // 默认值需要判断引号,如函数是不需要引号的 + if column.ColumnDefault != "" { + mark := false + // 哪些字段类型默认值需要加引号 + if collx.ArrayAnyMatches([]string{"CHAR", "LONG", "DATE", "TIME", "CLOB", "BLOB", "BFILE"}, column.ColumnType) { + // 默认值是时间日期函数的必须要加引号 + val := strings.ToUpper(column.ColumnDefault) + if collx.ArrayAnyMatches([]string{"DATE", "TIMESTAMP"}, column.ColumnType) && val == "CURRENT_DATE" || val == "CURRENT_TIMESTAMP" { + mark = false + } else { + mark = true + } + if mark { + defVal = fmt.Sprintf(" DEFAULT '%s'", column.ColumnDefault) + } else { + defVal = fmt.Sprintf(" DEFAULT %s", column.ColumnDefault) + } + } else { + // 如果是数字,默认值提取数字 + if collx.ArrayAnyMatches([]string{"NUM", "INT"}, column.ColumnType) { + match := bracketsRegexp.FindStringSubmatch(column.ColumnType) + if len(match) > 1 { + length := anyx.ConvInt(match[1]) + defVal = fmt.Sprintf(" DEFAULT %d", length) + } else { + defVal = fmt.Sprintf(" DEFAULT 0") + } + } + + defVal = fmt.Sprintf(" DEFAULT %s", column.ColumnDefault) + } + } + + columnSql := fmt.Sprintf(" %s %s %s %s", colName, column.ColumnType, defVal, nullAble) + return columnSql +} + +func (od *OracleDialect) CreateIndex(tableInfo dbi.Table, indexs []dbi.Index) error { + meta := od.dc.GetMetaData() + sqls := make([]string, 0) + comments := make([]string, 0) + for _, index := range indexs { + // 通过字段、表名拼接索引名 + columnName := strings.ReplaceAll(index.ColumnName, "-", "") + columnName = strings.ReplaceAll(columnName, "_", "") + colName := strings.ReplaceAll(columnName, ",", "_") + + keyType := "normal" + unique := "" + if index.IsUnique { + keyType = "unique" + unique = "unique" + } + indexName := fmt.Sprintf("%s_key_%s_%s", keyType, tableInfo.TableName, colName) + + sqls = append(sqls, fmt.Sprintf("CREATE %s INDEX %s ON %s(%s)", unique, indexName, meta.QuoteIdentifier(tableInfo.TableName), index.ColumnName)) + if index.IndexComment != "" { + comments = append(comments, fmt.Sprintf("COMMENT ON INDEX %s IS '%s'", indexName, index.IndexComment)) + } + } + _, err := od.dc.Exec(strings.Join(sqls, ";")) + + // 添加注释 + if len(comments) > 0 { + _, err = od.dc.Exec(strings.Join(comments, ";")) + } + return err +} + +func (od *OracleDialect) UpdateSequence(tableName string, columns []dbi.Column) { + +} diff --git a/server/internal/db/dbm/oracle/metadata.go b/server/internal/db/dbm/oracle/metadata.go index 79a576a8..a47376e1 100644 --- a/server/internal/db/dbm/oracle/metadata.go +++ b/server/internal/db/dbm/oracle/metadata.go @@ -13,11 +13,12 @@ import ( // ---------------------------------- DM元数据 ----------------------------------- const ( - ORACLE_META_FILE = "metasql/oracle_meta.sql" - ORACLE_DB_SCHEMAS = "ORACLE_DB_SCHEMAS" - ORACLE_TABLE_INFO_KEY = "ORACLE_TABLE_INFO" - ORACLE_INDEX_INFO_KEY = "ORACLE_INDEX_INFO" - ORACLE_COLUMN_MA_KEY = "ORACLE_COLUMN_MA" + ORACLE_META_FILE = "metasql/oracle_meta.sql" + ORACLE_DB_SCHEMAS = "ORACLE_DB_SCHEMAS" + ORACLE_TABLE_INFO_KEY = "ORACLE_TABLE_INFO" + ORACLE_TABLE_INFO_BY_NAMES_KEY = "ORACLE_TABLE_INFO_BY_NAMES" + ORACLE_INDEX_INFO_KEY = "ORACLE_INDEX_INFO" + ORACLE_COLUMN_MA_KEY = "ORACLE_COLUMN_MA" ) type OracleMetaData struct { @@ -51,14 +52,20 @@ func (od *OracleMetaData) GetDbNames() ([]string, error) { return databases, nil } -// 获取表基础元信息, 如表名等 -func (od *OracleMetaData) GetTables() ([]dbi.Table, error) { +func (od *OracleMetaData) GetTables(tableNames ...string) ([]dbi.Table, error) { + meta := od.dc.GetMetaData() + names := strings.Join(collx.ArrayMap[string, string](tableNames, func(val string) string { + return fmt.Sprintf("'%s'", meta.RemoveQuote(val)) + }), ",") - // 首先执行更新统计信息sql 这个统计信息在数据量比较大的时候就比较耗时,所以最好定时执行 - // _, _, err := pd.dc.Query("dbms_stats.GATHER_SCHEMA_stats(SELECT SF_GET_SCHEMA_NAME_BY_ID(CURRENT_SCHID))") + var res []map[string]any + var err error - // 查询表信息 - _, res, err := od.dc.Query(dbi.GetLocalSql(ORACLE_META_FILE, ORACLE_TABLE_INFO_KEY)) + if tableNames != nil || len(tableNames) > 0 { + _, res, err = od.dc.Query(fmt.Sprintf(dbi.GetLocalSql(ORACLE_META_FILE, ORACLE_TABLE_INFO_BY_NAMES_KEY), names)) + } else { + _, res, err = od.dc.Query(dbi.GetLocalSql(ORACLE_META_FILE, ORACLE_TABLE_INFO_KEY)) + } if err != nil { return nil, err } @@ -79,8 +86,9 @@ func (od *OracleMetaData) GetTables() ([]dbi.Table, error) { // 获取列元信息, 如列名等 func (od *OracleMetaData) GetColumns(tableNames ...string) ([]dbi.Column, error) { + meta := od.dc.GetMetaData() tableName := strings.Join(collx.ArrayMap[string, string](tableNames, func(val string) string { - return fmt.Sprintf("'%s'", dbi.RemoveQuote(od, val)) + return fmt.Sprintf("'%s'", meta.RemoveQuote(val)) }), ",") // 如果表数量超过了1000,需要分批查询 @@ -273,7 +281,57 @@ var ( // 日期时间类型 datetimeTypeRegexp = regexp.MustCompile(`(?i)date|timestamp`) + bracketsRegexp = regexp.MustCompile(`\((\d+)\)`) + converter = new(DataConverter) + + // oracle数据类型 映射 公共数据类型 + commonColumnTypeMap = map[string]string{ + "CHAR": dbi.CommonTypeChar, + "NCHAR": dbi.CommonTypeChar, + "VARCHAR2": dbi.CommonTypeVarchar, + "NVARCHAR2": dbi.CommonTypeVarchar, + "NUMBER": dbi.CommonTypeNumber, + "INTEGER": dbi.CommonTypeInt, + "INT": dbi.CommonTypeInt, + "DECIMAL": dbi.CommonTypeNumber, + "FLOAT": dbi.CommonTypeNumber, + "REAL": dbi.CommonTypeNumber, + "BINARY_FLOAT": dbi.CommonTypeNumber, + "BINARY_DOUBLE": dbi.CommonTypeNumber, + "DATE": dbi.CommonTypeDate, + "TIMESTAMP": dbi.CommonTypeDatetime, + "LONG": dbi.CommonTypeLongtext, + "BLOB": dbi.CommonTypeLongtext, + "CLOB": dbi.CommonTypeLongtext, + "NCLOB": dbi.CommonTypeLongtext, + "BFILE": dbi.CommonTypeBinary, + } + + // 公共数据类型 映射 oracle数据类型 + oracleColumnTypeMap = map[string]string{ + dbi.CommonTypeVarchar: "NVARCHAR2", + dbi.CommonTypeChar: "NCHAR", + dbi.CommonTypeText: "CLOB", + dbi.CommonTypeBlob: "CLOB", + dbi.CommonTypeLongblob: "CLOB", + dbi.CommonTypeLongtext: "CLOB", + dbi.CommonTypeBinary: "BFILE", + dbi.CommonTypeMediumblob: "CLOB", + dbi.CommonTypeMediumtext: "CLOB", + dbi.CommonTypeVarbinary: "BFILE", + dbi.CommonTypeInt: "INT", + dbi.CommonTypeSmallint: "INT", + dbi.CommonTypeTinyint: "INT", + dbi.CommonTypeNumber: "NUMBER", + dbi.CommonTypeBigint: "NUMBER", + dbi.CommonTypeDatetime: "DATE", + dbi.CommonTypeDate: "DATE", + dbi.CommonTypeTime: "DATE", + dbi.CommonTypeTimestamp: "TIMESTAMP", + dbi.CommonTypeEnum: "CLOB", + dbi.CommonTypeJSON: "CLOB", + } ) type DataConverter struct { diff --git a/server/internal/db/dbm/postgres/dialect.go b/server/internal/db/dbm/postgres/dialect.go index e6222814..ba4ed550 100644 --- a/server/internal/db/dbm/postgres/dialect.go +++ b/server/internal/db/dbm/postgres/dialect.go @@ -58,7 +58,7 @@ func (pd *PgsqlDialect) BatchInsert(tx *sql.Tx, tableName string, columns []stri } // pgsql默认唯一键冲突策略 -func (pd PgsqlDialect) pgsqlOnDuplicateStrategySql(duplicateStrategy int, tableName string, columns []string) string { +func (pd *PgsqlDialect) pgsqlOnDuplicateStrategySql(duplicateStrategy int, tableName string, columns []string) string { suffix := "" if duplicateStrategy == dbi.DuplicateStrategyIgnore { suffix = " \n on conflict do nothing" @@ -83,7 +83,7 @@ func (pd PgsqlDialect) pgsqlOnDuplicateStrategySql(duplicateStrategy int, tableN } // 高斯db唯一键冲突策略,使用ON DUPLICATE KEY UPDATE 参考:https://support.huaweicloud.com/distributed-devg-v3-gaussdb/gaussdb-12-0607.html#ZH-CN_TOPIC_0000001633948138 -func (pd PgsqlDialect) gaussOnDuplicateStrategySql(duplicateStrategy int, tableName string, columns []string) string { +func (pd *PgsqlDialect) gaussOnDuplicateStrategySql(duplicateStrategy int, tableName string, columns []string) string { suffix := "" metadata := pd.dc.GetMetaData() if duplicateStrategy == dbi.DuplicateStrategyIgnore { @@ -122,17 +122,7 @@ func (pd PgsqlDialect) gaussOnDuplicateStrategySql(duplicateStrategy int, tableN // 从连接信息中获取数据库和schema信息 func (pd *PgsqlDialect) currentSchema() string { - dbName := pd.dc.Info.Database - schema := "" - arr := strings.Split(dbName, "/") - if len(arr) == 2 { - schema = arr[1] - } - return schema -} - -func (pd *PgsqlDialect) IsGauss() bool { - return strings.Contains(pd.dc.Info.Params, "gauss") + return pd.dc.Info.CurrentSchema() } func (pd *PgsqlDialect) CopyTable(copy *dbi.DbCopyTable) error { @@ -192,3 +182,221 @@ func (pd *PgsqlDialect) CopyTable(copy *dbi.DbCopyTable) error { } return err } + +func (pd *PgsqlDialect) TransColumns(columns []dbi.Column) []dbi.Column { + var commonColumns []dbi.Column + for _, column := range columns { + // 取出当前数据库类型 + arr := strings.Split(column.ColumnType, "(") + ctype := arr[0] + // 翻译为通用数据库类型 + t1 := commonColumnTypeMap[ctype] + if t1 == "" { + ctype = "varchar(2000)" + } else { + // 回写到列信息 + if len(arr) > 1 { + ctype = t1 + "(" + arr[1] + } else { + ctype = t1 + } + } + column.ColumnType = ctype + commonColumns = append(commonColumns, column) + } + return commonColumns +} + +func (pd *PgsqlDialect) CreateTable(commonColumns []dbi.Column, tableInfo dbi.Table, dropOldTable bool) (int, error) { + meta := pd.dc.GetMetaData() + replacer := strings.NewReplacer(";", "", "'", "") + if dropOldTable { + _, _ = pd.dc.Exec(fmt.Sprintf("DROP TABLE IF EXISTS %s", tableInfo.TableName)) + } + // 组装建表语句 + createSql := fmt.Sprintf("CREATE TABLE %s (\n", meta.QuoteIdentifier(tableInfo.TableName)) + fields := make([]string, 0) + pks := make([]string, 0) + columnComments := make([]string, 0) + // 把通用类型转换为达梦类型 + for _, column := range commonColumns { + // 取出当前数据库类型 + arr := strings.Split(column.ColumnType, "(") + ctype := arr[0] + // 翻译为通用数据库类型 + t1 := pgsqlColumnTypeMap[ctype] + if t1 == "" { + ctype = "varchar(2000)" + } else { + // 回写到列信息 + if len(arr) > 1 { + if strings.Contains(strings.ToLower(t1), "int") { + // 如果是数字,类型后不需要带长度 + ctype = t1 + } else if strings.Contains(strings.ToLower(t1), "char") { + // 如果是字符串,长度翻倍 + match := bracketsRegexp.FindStringSubmatch(column.ColumnType) + if len(match) > 1 { + ctype = fmt.Sprintf("%s(%d)", t1, anyx.ConvInt(match[1])*2) + } else { + ctype = t1 + "(1000)" + } + } else { + ctype = t1 + "(" + arr[1] + } + } else { + ctype = t1 + } + } + column.ColumnType = ctype + + if column.IsPrimaryKey { + pks = append(pks, meta.QuoteIdentifier(column.ColumnName)) + } + fields = append(fields, pd.genColumnBasicSql(column)) + commentTmp := "comment on column %s.%s is '%s'" + // 防止注释内含有特殊字符串导致sql出错 + comment := replacer.Replace(column.ColumnComment) + columnComments = append(columnComments, fmt.Sprintf(commentTmp, column.TableName, column.ColumnName, comment)) + } + createSql += strings.Join(fields, ",") + if len(pks) > 0 { + createSql += fmt.Sprintf(", PRIMARY KEY (%s)", strings.Join(pks, ",")) + } + createSql += ")" + tableCommentSql := "" + if tableInfo.TableComment != "" { + commentTmp := "comment on table %s is '%s'" + tableCommentSql = fmt.Sprintf(commentTmp, tableInfo.TableName, replacer.Replace(tableInfo.TableComment)) + } + + columnCommentSql := strings.Join(columnComments, ";") + + sqls := make([]string, 0) + + if createSql != "" { + sqls = append(sqls, createSql) + } + if tableCommentSql != "" { + sqls = append(sqls, tableCommentSql) + } + if columnCommentSql != "" { + sqls = append(sqls, columnCommentSql) + } + + _, err := pd.dc.Exec(strings.Join(sqls, ";")) + + return 1, err +} + +func (pd *PgsqlDialect) genColumnBasicSql(column dbi.Column) string { + meta := pd.dc.GetMetaData() + colName := meta.QuoteIdentifier(column.ColumnName) + + // 如果是自增类型,需要转换为serial + if column.IsIdentity { + if column.ColumnType == "int4" { + column.ColumnType = "serial" + } else if column.ColumnType == "int2" { + column.ColumnType = "smallserial" + } else if column.ColumnType == "int8" { + column.ColumnType = "bigserial" + } else { + column.ColumnType = "bigserial" + } + + return fmt.Sprintf(" %s %s NOT NULL", colName, column.ColumnType) + } + + nullAble := "" + if column.Nullable == "NO" { + nullAble = " NOT NULL" + // 如果字段不能为空,则设置默认值 + if column.ColumnDefault == "" { + if collx.ArrayAnyMatches([]string{"char", "text", "lob"}, strings.ToLower(column.ColumnType)) { + // 文本默认值为空字符串 + column.ColumnDefault = " " + } else if collx.ArrayAnyMatches([]string{"int", "num"}, strings.ToLower(column.ColumnType)) { + // 数字默认值为0 + column.ColumnDefault = "0" + } + } + } + + defVal := "" // 默认值需要判断引号,如函数是不需要引号的 // 为了防止跨源函数不支持 当默认值是函数时,不需要设置默认值 + if column.ColumnDefault != "" && !strings.Contains(column.ColumnDefault, "(") { + // 哪些字段类型默认值需要加引号 + mark := false + if collx.ArrayAnyMatches([]string{"char", "text", "date", "time", "lob"}, strings.ToLower(column.ColumnType)) { + // 如果是文本类型,则默认值不能带括号 + if collx.ArrayAnyMatches([]string{"char", "text", "lob"}, strings.ToLower(column.ColumnType)) { + column.ColumnDefault = "" + } + + // 当数据类型是日期时间,默认值是日期时间函数时,默认值不需要引号 + if collx.ArrayAnyMatches([]string{"date", "time"}, strings.ToLower(column.ColumnType)) && + collx.ArrayAnyMatches([]string{"DATE", "TIME"}, strings.ToUpper(column.ColumnDefault)) { + mark = false + } else { + mark = true + } + } + // 如果数据类型是日期时间,则写死默认值函数 + if collx.ArrayAnyMatches([]string{"date", "time"}, strings.ToLower(column.ColumnType)) { + column.ColumnDefault = "CURRENT_TIMESTAMP" + } + + if column.ColumnDefault != "" { + if mark { + defVal = fmt.Sprintf(" DEFAULT '%s'", column.ColumnDefault) + } else { + defVal = fmt.Sprintf(" DEFAULT %s", column.ColumnDefault) + } + } + } + + columnSql := fmt.Sprintf(" %s %s %s %s ", colName, column.ColumnType, nullAble, defVal) + return columnSql +} + +func (pd *PgsqlDialect) CreateIndex(tableInfo dbi.Table, indexs []dbi.Index) error { + sqls := make([]string, 0) + comments := make([]string, 0) + for _, index := range indexs { + // 通过字段、表名拼接索引名 + columnName := strings.ReplaceAll(index.ColumnName, "-", "") + columnName = strings.ReplaceAll(columnName, "_", "") + colName := strings.ReplaceAll(columnName, ",", "_") + + keyType := "normal" + unique := "" + if index.IsUnique { + keyType = "unique" + unique = "unique" + } + indexName := fmt.Sprintf("%s_key_%s_%s", keyType, tableInfo.TableName, colName) + + // 如果索引名存在,先删除索引 + sqls = append(sqls, fmt.Sprintf("drop index if exists %s.%s", pd.currentSchema(), indexName)) + + // 创建索引 + sqls = append(sqls, fmt.Sprintf("CREATE %s INDEX %s on %s.%s(%s)", unique, indexName, pd.currentSchema(), tableInfo.TableName, index.ColumnName)) + if index.IndexComment != "" { + comments = append(comments, fmt.Sprintf("COMMENT ON INDEX %s.%s IS '%s'", pd.currentSchema(), indexName, index.IndexComment)) + } + } + _, err := pd.dc.Exec(strings.Join(sqls, ";")) + // 添加注释 + if len(comments) > 0 { + _, err = pd.dc.Exec(strings.Join(comments, ";")) + } + return err +} + +func (pd *PgsqlDialect) UpdateSequence(tableName string, columns []dbi.Column) { + for _, column := range columns { + if column.IsIdentity { + _, _ = pd.dc.Exec(fmt.Sprintf("select setval('%s_%s_seq', (SELECT max(%s) from %s))", tableName, column.ColumnName, column.ColumnName, tableName)) + } + } +} diff --git a/server/internal/db/dbm/postgres/metadata.go b/server/internal/db/dbm/postgres/metadata.go index 67ce809b..4bd82de2 100644 --- a/server/internal/db/dbm/postgres/metadata.go +++ b/server/internal/db/dbm/postgres/metadata.go @@ -12,12 +12,13 @@ import ( ) const ( - PGSQL_META_FILE = "metasql/pgsql_meta.sql" - PGSQL_DB_SCHEMAS = "PGSQL_DB_SCHEMAS" - PGSQL_TABLE_INFO_KEY = "PGSQL_TABLE_INFO" - PGSQL_INDEX_INFO_KEY = "PGSQL_INDEX_INFO" - PGSQL_COLUMN_MA_KEY = "PGSQL_COLUMN_MA" - PGSQL_TABLE_DDL_KEY = "PGSQL_TABLE_DDL_FUNC" + PGSQL_META_FILE = "metasql/pgsql_meta.sql" + PGSQL_DB_SCHEMAS = "PGSQL_DB_SCHEMAS" + PGSQL_TABLE_INFO_KEY = "PGSQL_TABLE_INFO" + PGSQL_INDEX_INFO_KEY = "PGSQL_INDEX_INFO" + PGSQL_COLUMN_MA_KEY = "PGSQL_COLUMN_MA" + PGSQL_TABLE_DDL_KEY = "PGSQL_TABLE_DDL_FUNC" + PGSQL_TABLE_INFO_BY_NAMES_KEY = "PGSQL_TABLE_INFO_BY_NAMES" ) type PgsqlMetaData struct { @@ -51,13 +52,23 @@ func (pd *PgsqlMetaData) GetDbNames() ([]string, error) { return databases, nil } -// 获取表基础元信息, 如表名等 -func (pd *PgsqlMetaData) GetTables() ([]dbi.Table, error) { - _, res, err := pd.dc.Query(dbi.GetLocalSql(PGSQL_META_FILE, PGSQL_TABLE_INFO_KEY)) +func (pd *PgsqlMetaData) GetTables(tableNames ...string) ([]dbi.Table, error) { + meta := pd.dc.GetMetaData() + names := strings.Join(collx.ArrayMap[string, string](tableNames, func(val string) string { + return fmt.Sprintf("'%s'", meta.RemoveQuote(val)) + }), ",") + + var res []map[string]any + var err error + + if tableNames != nil || len(tableNames) > 0 { + _, res, err = pd.dc.Query(fmt.Sprintf(dbi.GetLocalSql(PGSQL_META_FILE, PGSQL_TABLE_INFO_BY_NAMES_KEY), names)) + } else { + _, res, err = pd.dc.Query(dbi.GetLocalSql(PGSQL_META_FILE, PGSQL_TABLE_INFO_KEY)) + } if err != nil { return nil, err } - tables := make([]dbi.Table, 0) for _, re := range res { tables = append(tables, dbi.Table{ @@ -74,8 +85,9 @@ func (pd *PgsqlMetaData) GetTables() ([]dbi.Table, error) { // 获取列元信息, 如列名等 func (pd *PgsqlMetaData) GetColumns(tableNames ...string) ([]dbi.Column, error) { + meta := pd.dc.GetMetaData() tableName := strings.Join(collx.ArrayMap[string, string](tableNames, func(val string) string { - return fmt.Sprintf("'%s'", dbi.RemoveQuote(pd, val)) + return fmt.Sprintf("'%s'", meta.RemoveQuote(val)) }), ",") _, res, err := pd.dc.Query(fmt.Sprintf(dbi.GetLocalSql(PGSQL_META_FILE, PGSQL_COLUMN_MA_KEY), tableName)) @@ -210,8 +222,58 @@ var ( dateRegexp = regexp.MustCompile(`(?i)date`) // 时间类型 timeRegexp = regexp.MustCompile(`(?i)time`) + // 定义正则表达式,匹配括号内的数字 + bracketsRegexp = regexp.MustCompile(`\((\d+)\)`) converter = new(DataConverter) + + // pgsql数据类型 映射 公共数据类型 + commonColumnTypeMap = map[string]string{ + "int2": dbi.CommonTypeSmallint, + "int4": dbi.CommonTypeInt, + "int8": dbi.CommonTypeBigint, + "numeric": dbi.CommonTypeNumber, + "decimal": dbi.CommonTypeNumber, + "smallserial": dbi.CommonTypeSmallint, + "serial": dbi.CommonTypeInt, + "bigserial": dbi.CommonTypeBigint, + "largeserial": dbi.CommonTypeBigint, + "money": dbi.CommonTypeNumber, + "bool": dbi.CommonTypeTinyint, + "char": dbi.CommonTypeChar, + "character": dbi.CommonTypeChar, + "nchar": dbi.CommonTypeChar, + "varchar": dbi.CommonTypeVarchar, + "text": dbi.CommonTypeText, + "bytea": dbi.CommonTypeBinary, + "date": dbi.CommonTypeDate, + "time": dbi.CommonTypeTime, + "timestamp": dbi.CommonTypeTimestamp, + } + // 公共数据类型 映射 pgsql数据类型 + pgsqlColumnTypeMap = map[string]string{ + dbi.CommonTypeVarchar: "varchar", + dbi.CommonTypeChar: "char", + dbi.CommonTypeText: "text", + dbi.CommonTypeBlob: "text", + dbi.CommonTypeLongblob: "text", + dbi.CommonTypeLongtext: "text", + dbi.CommonTypeBinary: "bytea", + dbi.CommonTypeMediumblob: "text", + dbi.CommonTypeMediumtext: "text", + dbi.CommonTypeVarbinary: "bytea", + dbi.CommonTypeInt: "int4", + dbi.CommonTypeSmallint: "int2", + dbi.CommonTypeTinyint: "int2", + dbi.CommonTypeNumber: "numeric", + dbi.CommonTypeBigint: "int8", + dbi.CommonTypeDatetime: "timestamp", + dbi.CommonTypeDate: "date", + dbi.CommonTypeTime: "time", + dbi.CommonTypeTimestamp: "timestamp", + dbi.CommonTypeEnum: "varchar(2000)", + dbi.CommonTypeJSON: "varchar(2000)", + } ) type DataConverter struct { diff --git a/server/internal/db/dbm/sqlite/dialect.go b/server/internal/db/dbm/sqlite/dialect.go index a544f4c9..6868fc70 100644 --- a/server/internal/db/dbm/sqlite/dialect.go +++ b/server/internal/db/dbm/sqlite/dialect.go @@ -4,6 +4,8 @@ import ( "database/sql" "fmt" "mayfly-go/internal/db/dbm/dbi" + "mayfly-go/pkg/logx" + "mayfly-go/pkg/utils/collx" "strings" "time" ) @@ -18,6 +20,7 @@ func (sd *SqliteDialect) GetDbProgram() (dbi.DbProgram, error) { } func (sd *SqliteDialect) BatchInsert(tx *sql.Tx, tableName string, columns []string, values [][]any, duplicateStrategy int) (int64, error) { + _, _ = sd.dc.Exec("PRAGMA foreign_keys = false") // 执行批量insert sql,跟mysql一样 支持批量insert语法 // 生成占位符字符串:如:(?,?) // 重复字符串并用逗号连接 @@ -44,9 +47,16 @@ func (sd *SqliteDialect) BatchInsert(tx *sql.Tx, tableName string, columns []str for _, v := range values { args = append(args, v...) } + exec, err := sd.dc.TxExec(tx, sqlStr, args...) + + _, _ = sd.dc.Exec("PRAGMA foreign_keys = true;") // 执行批量insert sql - return sd.dc.TxExec(tx, sqlStr, args...) + return exec, err +} + +func (sd *SqliteDialect) GetDataConverter() dbi.DataConverter { + return converter } func (sd *SqliteDialect) CopyTable(copy *dbi.DbCopyTable) error { @@ -80,3 +90,130 @@ func (sd *SqliteDialect) CopyTable(copy *dbi.DbCopyTable) error { return err } + +func (sd *SqliteDialect) TransColumns(columns []dbi.Column) []dbi.Column { + var commonColumns []dbi.Column + for _, column := range columns { + // 取出当前数据库类型 + arr := strings.Split(column.ColumnType, "(") + ctype := arr[0] + // 翻译为通用数据库类型 + t1 := commonColumnTypeMap[ctype] + if t1 == "" { + ctype = "varchar(2000)" + } else { + // 回写到列信息 + if len(arr) > 1 { + ctype = t1 + "(" + arr[1] + } else { + ctype = t1 + } + } + column.ColumnType = ctype + commonColumns = append(commonColumns, column) + } + return commonColumns +} + +func (sd *SqliteDialect) CreateTable(commonColumns []dbi.Column, tableInfo dbi.Table, dropOldTable bool) (int, error) { + tbName := sd.dc.GetMetaData().QuoteIdentifier(tableInfo.TableName) + if dropOldTable { + _, err := sd.dc.Exec(fmt.Sprintf("DROP TABLE IF EXISTS %s", tbName)) + if err != nil { + logx.Error("删除表失败", err) + } + } + + // 组装建表语句 + createSql := fmt.Sprintf("CREATE TABLE %s (\n", tbName) + fields := make([]string, 0) + // 把通用类型转换为达梦类型 + for _, column := range commonColumns { + // 取出当前数据库类型 + arr := strings.Split(column.ColumnType, "(") + ctype := arr[0] + // 翻译为通用数据库类型 + t1 := sqliteColumnTypeMap[ctype] + if t1 == "" { + ctype = "nvarchar(2000)" + } else { + // 回写到列信息 + if len(arr) > 1 { + ctype = t1 + "(" + arr[1] + } + } + column.ColumnType = ctype + fields = append(fields, sd.genColumnBasicSql(column)) + } + createSql += strings.Join(fields, ",") + createSql += fmt.Sprintf(") ") + _, err := sd.dc.Exec(createSql) + + return 1, err +} + +func (sd *SqliteDialect) CreateIndex(tableInfo dbi.Table, indexs []dbi.Index) error { + sqls := make([]string, 0) + for _, index := range indexs { + // 通过字段、表名拼接索引名 + columnName := strings.ReplaceAll(index.ColumnName, "-", "") + columnName = strings.ReplaceAll(columnName, "_", "") + colName := strings.ReplaceAll(columnName, ",", "_") + + keyType := "normal" + unique := "" + if index.IsUnique { + keyType = "unique" + unique = "unique" + } + indexName := fmt.Sprintf("%s_key_%s_%s", keyType, tableInfo.TableName, colName) + sqlTmp := "CREATE %s INDEX %s ON \"%s\" (%s) " + sqls = append(sqls, fmt.Sprintf(sqlTmp, unique, indexName, tableInfo.TableName, index.ColumnName)) + } + _, err := sd.dc.Exec(strings.Join(sqls, ";")) + return err +} + +func (sd *SqliteDialect) genColumnBasicSql(column dbi.Column) string { + + incr := "" + if column.IsIdentity { + incr = " AUTOINCREMENT" + } + + nullAble := "" + if column.Nullable == "NO" { + nullAble = " NOT NULL" + } + + // 如果是主键,则直接返回,不判断默认值 + if column.IsPrimaryKey { + return fmt.Sprintf(" %s integer PRIMARY KEY %s %s", column.ColumnName, incr, nullAble) + } + + defVal := "" // 默认值需要判断引号,如函数是不需要引号的 // 为了防止跨源函数不支持 当默认值是函数时,不需要设置默认值 + if column.ColumnDefault != "" && !strings.Contains(column.ColumnDefault, "(") { + // 哪些字段类型默认值需要加引号 + mark := false + if collx.ArrayAnyMatches([]string{"char", "text", "date", "time", "lob"}, strings.ToLower(column.ColumnType)) { + // 当数据类型是日期时间,默认值是日期时间函数时,默认值不需要引号 + if collx.ArrayAnyMatches([]string{"date", "time"}, strings.ToLower(column.ColumnType)) && + collx.ArrayAnyMatches([]string{"DATE", "TIME"}, strings.ToUpper(column.ColumnDefault)) { + mark = false + } else { + mark = true + } + } + if mark { + defVal = fmt.Sprintf(" DEFAULT '%s'", column.ColumnDefault) + } else { + defVal = fmt.Sprintf(" DEFAULT %s", column.ColumnDefault) + } + } + + return fmt.Sprintf(" %s %s %s %s", sd.dc.GetMetaData().QuoteIdentifier(column.ColumnName), column.ColumnType, nullAble, defVal) +} + +func (sd *SqliteDialect) UpdateSequence(tableName string, columns []dbi.Column) { + +} diff --git a/server/internal/db/dbm/sqlite/meta.go b/server/internal/db/dbm/sqlite/meta.go index 855163b3..91de9b27 100644 --- a/server/internal/db/dbm/sqlite/meta.go +++ b/server/internal/db/dbm/sqlite/meta.go @@ -20,7 +20,13 @@ func (md *SqliteMeta) GetSqlDb(d *dbi.DbInfo) (*sql.DB, error) { if _, err := os.Stat(d.Host); err != nil { return nil, errors.New("数据库文件不存在") } - return sql.Open("sqlite", d.Host) + + db, err := sql.Open("sqlite", d.Host) + if err != nil { + return nil, err + } + _, err = db.Exec("PRAGMA busy_timeout = 50000;") + return db, err } func (sm *SqliteMeta) GetDialect(conn *dbi.DbConn) dbi.Dialect { diff --git a/server/internal/db/dbm/sqlite/metadata.go b/server/internal/db/dbm/sqlite/metadata.go index 70f08025..aac7bf31 100644 --- a/server/internal/db/dbm/sqlite/metadata.go +++ b/server/internal/db/dbm/sqlite/metadata.go @@ -6,15 +6,17 @@ import ( "mayfly-go/internal/db/dbm/dbi" "mayfly-go/pkg/logx" "mayfly-go/pkg/utils/anyx" + "mayfly-go/pkg/utils/collx" "regexp" "strings" "time" ) const ( - SQLITE_META_FILE = "metasql/sqlite_meta.sql" - SQLITE_TABLE_INFO_KEY = "SQLITE_TABLE_INFO" - SQLITE_INDEX_INFO_KEY = "SQLITE_INDEX_INFO" + SQLITE_META_FILE = "metasql/sqlite_meta.sql" + SQLITE_TABLE_INFO_KEY = "SQLITE_TABLE_INFO" + SQLITE_INDEX_INFO_KEY = "SQLITE_INDEX_INFO" + SQLITE_TABLE_INFO_BY_NAMES_KEY = "SQLITE_TABLE_INFO_BY_NAMES" ) type SqliteMetaData struct { @@ -48,13 +50,22 @@ func (sd *SqliteMetaData) GetDbNames() ([]string, error) { } // 获取表基础元信息, 如表名等 -func (sd *SqliteMetaData) GetTables() ([]dbi.Table, error) { - _, res, err := sd.dc.Query(dbi.GetLocalSql(SQLITE_META_FILE, SQLITE_TABLE_INFO_KEY)) - //cols, res, err := sd.dc.Query("SELECT datetime(1092941466, 'unixepoch')") +func (sd *SqliteMetaData) GetTables(tableNames ...string) ([]dbi.Table, error) { + names := strings.Join(collx.ArrayMap[string, string](tableNames, func(val string) string { + return fmt.Sprintf("'%s'", dbi.RemoveQuote(sd, val)) + }), ",") + + var res []map[string]any + var err error + + if tableNames != nil || len(tableNames) > 0 { + _, res, err = sd.dc.Query(fmt.Sprintf(dbi.GetLocalSql(SQLITE_META_FILE, SQLITE_TABLE_INFO_BY_NAMES_KEY), names)) + } else { + _, res, err = sd.dc.Query(dbi.GetLocalSql(SQLITE_META_FILE, SQLITE_TABLE_INFO_KEY)) + } if err != nil { return nil, err } - tables := make([]dbi.Table, 0) for _, re := range res { tables = append(tables, dbi.Table{ @@ -191,6 +202,61 @@ var ( datetimeRegexp = regexp.MustCompile(`(?i)datetime`) converter = new(DataConverter) + + // sqlite数据类型 映射 公共数据类型 + commonColumnTypeMap = map[string]string{ + "int": dbi.CommonTypeInt, + "integer": dbi.CommonTypeInt, + "tinyint": dbi.CommonTypeTinyint, + "smallint": dbi.CommonTypeSmallint, + "mediumint": dbi.CommonTypeSmallint, + "bigint": dbi.CommonTypeBigint, + "int2": dbi.CommonTypeInt, + "int8": dbi.CommonTypeInt, + "character": dbi.CommonTypeChar, + "varchar": dbi.CommonTypeVarchar, + "varying character": dbi.CommonTypeVarchar, + "nchar": dbi.CommonTypeChar, + "native character": dbi.CommonTypeVarchar, + "nvarchar": dbi.CommonTypeVarchar, + "text": dbi.CommonTypeText, + "clob": dbi.CommonTypeBlob, + "blob": dbi.CommonTypeBlob, + "real": dbi.CommonTypeNumber, + "double": dbi.CommonTypeNumber, + "double precision": dbi.CommonTypeNumber, + "float": dbi.CommonTypeNumber, + "numeric": dbi.CommonTypeNumber, + "decimal": dbi.CommonTypeNumber, + "boolean": dbi.CommonTypeTinyint, + "date": dbi.CommonTypeDate, + "datetime": dbi.CommonTypeDatetime, + } + + // 公共数据类型 映射 sqlite数据类型 + sqliteColumnTypeMap = map[string]string{ + dbi.CommonTypeVarchar: "nvarchar", + dbi.CommonTypeChar: "nchar", + dbi.CommonTypeText: "text", + dbi.CommonTypeBlob: "blob", + dbi.CommonTypeLongblob: "blob", + dbi.CommonTypeLongtext: "text", + dbi.CommonTypeBinary: "text", + dbi.CommonTypeMediumblob: "blob", + dbi.CommonTypeMediumtext: "text", + dbi.CommonTypeVarbinary: "text", + dbi.CommonTypeInt: "int", + dbi.CommonTypeSmallint: "smallint", + dbi.CommonTypeTinyint: "tinyint", + dbi.CommonTypeNumber: "number", + dbi.CommonTypeBigint: "bigint", + dbi.CommonTypeDatetime: "datetime", + dbi.CommonTypeDate: "date", + dbi.CommonTypeTime: "datetime", + dbi.CommonTypeTimestamp: "datetime", + dbi.CommonTypeEnum: "nvarchar(2000)", + dbi.CommonTypeJSON: "nvarchar(2000)", + } ) type DataConverter struct { diff --git a/server/internal/db/domain/entity/db_transfer.go b/server/internal/db/domain/entity/db_transfer.go new file mode 100644 index 00000000..01c9209b --- /dev/null +++ b/server/internal/db/domain/entity/db_transfer.go @@ -0,0 +1,60 @@ +package entity + +import ( + "mayfly-go/pkg/model" + "time" +) + +type DbTransferTask struct { + model.Model + + RunningState int `orm:"column(running_state)" json:"runningState"` // 运行状态 1运行中 2待运行 + + CheckedKeys string `orm:"column(checked_keys)" json:"checkedKeys"` // 选中需要迁移的表 + DeleteTable int `orm:"column(delete_table)" json:"deleteTable"` // 创建表前是否删除表 + NameCase int `orm:"column(name_case)" json:"nameCase"` // 表名、字段大小写转换 1无 2大写 3小写 + Strategy int `orm:"column(strategy)" json:"strategy"` // 迁移策略 1全量 2增量 + + SrcDbId int64 `orm:"column(src_db_id)" json:"srcDbId"` // 源库id + SrcDbName string `orm:"column(src_db_name)" json:"srcDbName"` // 源库名 + SrcTagPath string `orm:"column(src_tag_path)" json:"srcTagPath"` // 源库tagPath + SrcDbType string `orm:"column(src_db_type)" json:"srcDbType"` // 源库类型 + SrcInstName string `orm:"column(src_inst_name)" json:"srcInstName"` // 源库实例名 + + TargetDbId int `orm:"column(target_db_id)" json:"targetDbId"` // 目标库id + TargetDbName string `orm:"column(target_db_name)" json:"targetDbName"` // 目标库名 + TargetDbType string `orm:"column(target_tag_path)" json:"targetDbType"` // 目标库类型 + TargetInstName string `orm:"column(target_db_type)" json:"targetInstName"` // 目标库实例名 + TargetTagPath string `orm:"column(target_inst_name)" json:"targetTagPath"` // 目标库tagPath + +} + +func (d *DbTransferTask) TableName() string { + return "t_db_transfer_task" +} + +type DbTransferLog struct { + model.IdModel + TaskId uint64 `orm:"column(task_id)" json:"taskId"` // 任务表id + CreateTime *time.Time `orm:"column(create_time)" json:"createTime"` + DataSqlFull string `orm:"column(data_sql_full)" json:"dataSqlFull"` // 执行的完整sql + ResNum int `orm:"column(res_num)" json:"resNum"` // 收到数据条数 + ErrText string `orm:"column(err_text)" json:"errText"` // 错误日志 + Status int8 `orm:"column(status)" json:"status"` // 状态:1.成功 -1.失败 +} + +func (d *DbTransferLog) TableName() string { + return "t_db_transfer_log" +} + +const ( + DbTransferTaskStatusEnable int = 1 // 启用状态 + DbTransferTaskStatusDisable int = -1 // 禁用状态 + + DbTransferTaskStateSuccess int = 1 // 执行成功状态 + DbTransferTaskStateRunning int = 2 // 执行成功状态 + DbTransferTaskStateFail int = -1 // 执行失败状态 + + DbTransferTaskRunStateRunning int = 1 // 运行中状态 + DbTransferTaskRunStateStop int = 2 // 手动停止状态 +) diff --git a/server/internal/db/domain/entity/query.go b/server/internal/db/domain/entity/query.go index 316a7d3f..5f93d6c3 100644 --- a/server/internal/db/domain/entity/query.go +++ b/server/internal/db/domain/entity/query.go @@ -15,6 +15,13 @@ type DataSyncLogQuery struct { TaskId uint64 `json:"task_id" form:"taskId"` } +type DbTransferTaskQuery struct { +} + +type DbTransferLogQuery struct { + TaskId uint64 `json:"task_id" form:"taskId"` +} + // 数据库查询实体,不与数据库表字段一一对应 type DbQuery struct { Id uint64 `form:"id"` diff --git a/server/internal/db/domain/repository/db_transfer.go b/server/internal/db/domain/repository/db_transfer.go new file mode 100644 index 00000000..9db65313 --- /dev/null +++ b/server/internal/db/domain/repository/db_transfer.go @@ -0,0 +1,21 @@ +package repository + +import ( + "mayfly-go/internal/db/domain/entity" + "mayfly-go/pkg/base" + "mayfly-go/pkg/model" +) + +type DbTransferTask interface { + base.Repo[*entity.DbTransferTask] + + // 分页获取数据库实例信息列表 + GetTaskList(condition *entity.DbTransferTaskQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) +} + +type DbTransferLog interface { + base.Repo[*entity.DbTransferLog] + + // 分页获取数据库实例信息列表 + GetTaskLogList(condition *entity.DbTransferLogQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) +} diff --git a/server/internal/db/infrastructure/persistence/db_transfer.go b/server/internal/db/infrastructure/persistence/db_transfer.go new file mode 100644 index 00000000..2839f602 --- /dev/null +++ b/server/internal/db/infrastructure/persistence/db_transfer.go @@ -0,0 +1,40 @@ +package persistence + +import ( + "mayfly-go/internal/db/domain/entity" + "mayfly-go/internal/db/domain/repository" + "mayfly-go/pkg/base" + "mayfly-go/pkg/gormx" + "mayfly-go/pkg/model" +) + +type dbTransferTaskRepoImpl struct { + base.RepoImpl[*entity.DbTransferTask] +} + +func newDbTransferTaskRepo() repository.DbTransferTask { + return &dbTransferTaskRepoImpl{base.RepoImpl[*entity.DbTransferTask]{M: new(entity.DbTransferTask)}} +} + +// 分页获取数据库信息列表 +func (d *dbTransferTaskRepoImpl) GetTaskList(condition *entity.DbTransferTaskQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) { + qd := gormx.NewQuery(new(entity.DbTransferTask)) + //Like("task_name", condition.Name). + //Eq("status", condition.Status) + return gormx.PageQuery(qd, pageParam, toEntity) +} + +type dbTransferLogRepoImpl struct { + base.RepoImpl[*entity.DbTransferLog] +} + +// 分页获取数据库信息列表 +func (d *dbTransferLogRepoImpl) GetTaskLogList(condition *entity.DbTransferLogQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) { + qd := gormx.NewQuery(new(entity.DbTransferLog)). + Eq("task_id", condition.TaskId) + return gormx.PageQuery(qd, pageParam, toEntity) +} + +func newDbTransferLogRepo() repository.DbTransferLog { + return &dbTransferLogRepoImpl{base.RepoImpl[*entity.DbTransferLog]{M: new(entity.DbTransferLog)}} +} diff --git a/server/internal/db/infrastructure/persistence/persistence.go b/server/internal/db/infrastructure/persistence/persistence.go index 1dc8b266..6e16f148 100644 --- a/server/internal/db/infrastructure/persistence/persistence.go +++ b/server/internal/db/infrastructure/persistence/persistence.go @@ -11,6 +11,8 @@ func InitIoc() { ioc.Register(newDbSqlExecRepo(), ioc.WithComponentName("DbSqlExecRepo")) ioc.Register(newDataSyncTaskRepo(), ioc.WithComponentName("DbDataSyncTaskRepo")) ioc.Register(newDataSyncLogRepo(), ioc.WithComponentName("DbDataSyncLogRepo")) + ioc.Register(newDbTransferTaskRepo(), ioc.WithComponentName("DbTransferTaskRepo")) + ioc.Register(newDbTransferLogRepo(), ioc.WithComponentName("DbTransferLogRepo")) ioc.Register(NewDbBackupRepo(), ioc.WithComponentName("DbBackupRepo")) ioc.Register(NewDbBackupHistoryRepo(), ioc.WithComponentName("DbBackupHistoryRepo")) diff --git a/server/internal/db/router/db_transfer.go b/server/internal/db/router/db_transfer.go new file mode 100644 index 00000000..8e87437b --- /dev/null +++ b/server/internal/db/router/db_transfer.go @@ -0,0 +1,38 @@ +package router + +import ( + "mayfly-go/internal/db/api" + "mayfly-go/pkg/biz" + "mayfly-go/pkg/ioc" + "mayfly-go/pkg/req" + + "github.com/gin-gonic/gin" +) + +func InitDbTransferRouter(router *gin.RouterGroup) { + instances := router.Group("/dbTransfer") + + d := new(api.DbTransferTask) + biz.ErrIsNil(ioc.Inject(d)) + + reqs := [...]*req.Conf{ + // 获取任务列表 /datasync + req.NewGet("", d.Tasks), + + req.NewGet(":taskId/logs", d.Logs).RequiredPermissionCode("db:transfer:log"), + + // 保存任务 /datasync/save + req.NewPost("save", d.SaveTask).Log(req.NewLogSave("datasync-保存数据迁移任务信息")).RequiredPermissionCode("db:transfer:save"), + + // 删除任务 /datasync/:taskId/del + req.NewDelete(":taskId/del", d.DeleteTask).Log(req.NewLogSave("datasync-删除数据迁移任务信息")).RequiredPermissionCode("db:transfer:del"), + + // 立即执行任务 /datasync/run + req.NewPost(":taskId/run", d.Run).Log(req.NewLogSave("datasync-运行数据迁移任务")).RequiredPermissionCode("db:transfer:run"), + + // 停止正在执行中的任务 + req.NewPost(":taskId/stop", d.Stop), + } + + req.BatchSetGroup(instances, reqs[:]) +} diff --git a/server/internal/db/router/router.go b/server/internal/db/router/router.go index ee2713ff..8fcee56a 100644 --- a/server/internal/db/router/router.go +++ b/server/internal/db/router/router.go @@ -10,4 +10,5 @@ func Init(router *gin.RouterGroup) { InitDbBackupRouter(router) InitDbRestoreRouter(router) InitDbDataSyncRouter(router) + InitDbTransferRouter(router) } diff --git a/server/pkg/utils/collx/array.go b/server/pkg/utils/collx/array.go index 7bc1f294..2c3453a1 100644 --- a/server/pkg/utils/collx/array.go +++ b/server/pkg/utils/collx/array.go @@ -1,5 +1,7 @@ package collx +import "strings" + // 数组比较 // 依次返回,新增值,删除值,以及不变值 func ArrayCompare[T comparable](newArr []T, oldArr []T) ([]T, []T, []T) { @@ -143,3 +145,13 @@ func ArrayDeduplicate[T comparable](arr []T) []T { return result } + +// ArrayAnyMatches 给定字符串是否包含指定数组中的任意字符串, 如:["time", "date"] , substr : timestamp,返回true +func ArrayAnyMatches(arr []string, subStr string) bool { + for _, itm := range arr { + if strings.Contains(subStr, itm) { + return true + } + } + return false +} diff --git a/server/resources/script/sql/v1.7/v1.7.5.sql b/server/resources/script/sql/v1.7/v1.7.5.sql index 9ddedcec..987ea0e0 100644 --- a/server/resources/script/sql/v1.7/v1.7.5.sql +++ b/server/resources/script/sql/v1.7/v1.7.5.sql @@ -7,3 +7,44 @@ INSERT INTO `t_sys_config` (`name`, `key`, `params`, `value`, `remark`, `permiss ALTER TABLE t_db_instance CHANGE sid extra varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL COMMENT '连接需要的额外参数,如oracle数据库需要sid等'; ALTER TABLE t_db_instance MODIFY COLUMN extra varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL COMMENT '连接需要的额外参数,如oracle数据库需要sid等'; + + +CREATE TABLE `t_db_transfer_task` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `creator_id` bigint(20) NOT NULL COMMENT '创建人id', + `creator` varchar(100) NOT NULL COMMENT '创建人姓名', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `modifier_id` bigint(20) NOT NULL COMMENT '修改人id', + `modifier` varchar(100) NOT NULL COMMENT '修改人姓名', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', + `is_deleted` tinyint(1) DEFAULT '0' COMMENT '是否删除', + `delete_time` datetime DEFAULT NULL COMMENT '删除时间', + `checked_keys` text NOT NULL COMMENT '选中需要迁移的表', + `delete_table` tinyint(4) NOT NULL COMMENT '创建表前是否删除表 1是 -1否', + `name_case` tinyint(4) NOT NULL COMMENT '表名、字段大小写转换 1无 2大写 3小写', + `strategy` tinyint(4) NOT NULL COMMENT '迁移策略 1全量 2增量', + `running_state` tinyint(1) DEFAULT '2' COMMENT '运行状态 1运行中 2待运行', + `src_db_id` bigint(20) NOT NULL COMMENT '源库id', + `src_db_name` varchar(200) NOT NULL COMMENT '源库名', + `src_tag_path` varchar(200) NOT NULL COMMENT '源库tagPath', + `src_db_type` varchar(200) NOT NULL COMMENT '源库类型', + `src_inst_name` varchar(200) NOT NULL COMMENT '源库实例名', + `target_db_id` bigint(20) NOT NULL COMMENT '目标库id', + `target_db_name` varchar(200) NOT NULL COMMENT '目标库名', + `target_tag_path` varchar(200) NOT NULL COMMENT '目标库类型', + `target_db_type` varchar(200) NOT NULL COMMENT '目标库实例名', + `target_inst_name` varchar(200) NOT NULL COMMENT '目标库tagPath', + PRIMARY KEY (`id`) +) COMMENT='数据库迁移任务表'; + +INSERT INTO `t_sys_resource` (`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`, `ui_path`, `is_deleted`, `delete_time`) VALUES(1709194669, 36, 1, 1, '数据库迁移', 'transfer', 1709194669, '{"component":"ops/db/DbTransferList","icon":"Switch","isKeepAlive":true,"routeName":"DbTransferList"}', 12, 'liuzongyang', 12, 'liuzongyang', '2024-02-29 16:17:50', '2024-02-29 16:24:59', 'SmLcpu6c/', 0, NULL); +INSERT INTO `t_sys_resource` (`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`, `ui_path`, `is_deleted`, `delete_time`) VALUES(1709194694, 1709194669, 2, 1, '基本权限', 'db:transfer', 1709194694, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2024-02-29 16:18:14', '2024-02-29 16:18:14', 'SmLcpu6c/A9vAm4J8/', 0, NULL); +INSERT INTO `t_sys_resource` (`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`, `ui_path`, `is_deleted`, `delete_time`) VALUES(1709196697, 1709194669, 2, 1, '编辑', 'db:transfer:save', 1709196697, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2024-02-29 16:51:37', '2024-02-29 16:51:37', 'SmLcpu6c/5oJwPzNb/', 0, NULL); +INSERT INTO `t_sys_resource` (`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`, `ui_path`, `is_deleted`, `delete_time`) VALUES(1709196707, 1709194669, 2, 1, '删除', 'db:transfer:del', 1709196707, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2024-02-29 16:51:47', '2024-02-29 16:51:47', 'SmLcpu6c/L3ybnAEW/', 0, NULL); +INSERT INTO `t_sys_resource` (`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`, `ui_path`, `is_deleted`, `delete_time`) VALUES(1709196723, 1709194669, 2, 1, '启停', 'db:transfer:status', 1709196723, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2024-02-29 16:52:04', '2024-02-29 16:52:04', 'SmLcpu6c/hGiLN1VT/', 0, NULL); +INSERT INTO `t_sys_resource` (`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`, `ui_path`, `is_deleted`, `delete_time`) VALUES(1709196737, 1709194669, 2, 1, '日志', 'db:transfer:log', 1709196737, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2024-02-29 16:52:17', '2024-02-29 16:52:17', 'SmLcpu6c/CZhNIbWg/', 0, NULL); +INSERT INTO `t_sys_resource` (`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`, `ui_path`, `is_deleted`, `delete_time`) VALUES(1709196755, 1709194669, 2, 1, '运行', 'db:transfer:run', 1709196755, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2024-02-29 16:52:36', '2024-02-29 16:52:36', 'SmLcpu6c/b6yHt6V2/', 0, NULL); + + + +