!108 feat:支持不同源数据库迁移

* feat:支持不同源数据库迁移
This commit is contained in:
zongyangleo
2024-03-15 09:01:51 +00:00
committed by Coder慌
parent 263dfa6be7
commit bd1e83989d
44 changed files with 3064 additions and 104 deletions

View File

@@ -0,0 +1,331 @@
<template>
<div class="db-transfer-edit">
<el-dialog
:title="title"
v-model="dialogVisible"
:before-close="cancel"
:close-on-click-modal="false"
:close-on-press-escape="false"
:destroy-on-close="true"
width="850px"
>
<el-form :model="form" ref="dbForm" :rules="rules" label-width="auto">
<el-tabs v-model="tabActiveName">
<el-tab-pane label="基本信息" :name="basicTab">
<el-form-item prop="srcDbId" label="源数据库" required>
<db-select-tree
placeholder="请选择源数据库"
v-model:db-id="form.srcDbId"
v-model:inst-name="form.srcInstName"
v-model:db-name="form.srcDbName"
v-model:tag-path="form.srcTagPath"
v-model:db-type="form.srcDbType"
@select-db="onSelectSrcDb"
/>
</el-form-item>
<el-form-item prop="targetDbId" label="目标数据库" required>
<db-select-tree
placeholder="请选择目标数据库"
v-model:db-id="form.targetDbId"
v-model:inst-name="form.targetInstName"
v-model:db-name="form.targetDbName"
v-model:tag-path="form.targetTagPath"
v-model:db-type="form.targetDbType"
@select-db="onSelectTargetDb"
/>
</el-form-item>
<el-form-item prop="strategy" label="迁移策略" required>
<el-select v-model="form.strategy" filterable placeholder="迁移策略">
<el-option label="全量" :value="1" />
<el-option label="增量(暂不可用)" disabled :value="2" />
</el-select>
</el-form-item>
<el-form-item prop="nameCase" label="转换表、字段名" required>
<el-select v-model="form.nameCase">
<el-option label="无" :value="1" />
<el-option label="大写" :value="2" />
<el-option label="小写" :value="3" />
</el-select>
</el-form-item>
<el-form-item prop="deleteTable" label="创建前删除表" required>
<el-select v-model="form.deleteTable">
<el-option label="是" :value="1" />
<el-option label="否" :value="2" />
</el-select>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="数据库对象" :name="tableTab" :disabled="!baseFieldCompleted">
<el-form-item>
<el-input v-model="state.filterSrcTableText" style="width: 240px" placeholder="过滤表" />
</el-form-item>
<el-form-item>
<el-tree
ref="srcTreeRef"
style="width: 760px; max-height: 400px; overflow-y: auto"
default-expand-all
:expand-on-click-node="false"
:data="state.srcTableTree"
node-key="id"
show-checkbox
@check-change="handleSrcTableCheckChange"
:filter-node-method="filterSrcTableTreeNode"
/>
</el-form-item>
</el-tab-pane>
</el-tabs>
</el-form>
<template #footer>
<div>
<el-button @click="cancel()"> </el-button>
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { computed, nextTick, reactive, ref, toRefs, watch } from 'vue';
import { dbApi } from './api';
import { ElMessage } from 'element-plus';
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
const props = defineProps({
data: {
type: [Boolean, Object],
},
title: {
type: String,
},
});
//定义事件
const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
const dialogVisible = defineModel<boolean>('visible', { default: false });
const rules = {};
const dbForm: any = ref(null);
const basicTab = 'basic';
const tableTab = 'table';
type FormData = {
id?: number;
srcDbId?: number;
srcDbName?: string;
srcDbType?: string;
srcInstName?: string;
srcTagPath?: string;
srcTableNames?: string;
targetDbId?: number;
targetInstName?: string;
targetDbName?: string;
targetTagPath?: string;
targetDbType?: string;
strategy: 1 | 2;
nameCase: 1 | 2 | 3;
deleteTable?: 1 | 2;
checkedKeys: string;
runningState: 1 | 2;
};
const basicFormData = {
strategy: 1,
nameCase: 1,
deleteTable: 1,
checkedKeys: '',
runningState: 1,
} as FormData;
const srcTableList = ref<{ tableName: string; tableComment: string }[]>([]);
const srcTableListDisabled = ref(false);
const defaultKeys = ['tab-check', 'all', 'table-list'];
const state = reactive({
tabActiveName: 'basic',
form: basicFormData,
submitForm: {} as any,
srcTableFields: [] as string[],
targetColumnList: [] as any[],
filterSrcTableText: '',
srcTableTree: [
{
id: 'tab-check',
label: '表',
children: [
{ id: 'all', label: '全部表(*' },
{
id: 'table-list',
label: '自定义',
disabled: srcTableListDisabled,
children: [] as any[],
},
],
},
],
});
const { tabActiveName, form, submitForm } = toRefs(state);
const { isFetching: saveBtnLoading, execute: saveExec } = dbApi.saveDbTransferTask.useApi(submitForm);
// 基础字段信息是否填写完整
const baseFieldCompleted = computed(() => {
return state.form.srcDbId && state.form.targetDbId && state.form.targetDbName;
});
watch(dialogVisible, async (newValue: boolean) => {
if (!newValue) {
return;
}
state.tabActiveName = 'basic';
const propsData = props.data as any;
if (!propsData?.id) {
let d = {} as FormData;
Object.assign(d, basicFormData);
state.form = d;
await nextTick(() => {
srcTreeRef.value.setCheckedKeys([]);
});
return;
}
state.form = props.data as FormData;
let { srcDbId, targetDbId } = state.form;
// 初始化src数据源
if (srcDbId) {
// 通过tagPath查询实例列表
const dbInfoRes = await dbApi.dbs.request({ id: srcDbId });
const db = dbInfoRes.list[0];
// 初始化实例
db.databases = db.database?.split(' ').sort() || [];
if (srcDbId && state.form.srcDbName) {
await loadDbTables(srcDbId, state.form.srcDbName);
}
}
// 初始化target数据源
if (targetDbId) {
// 通过tagPath查询实例列表
const dbInfoRes = await dbApi.dbs.request({ id: targetDbId });
const db = dbInfoRes.list[0];
// 初始化实例
db.databases = db.database?.split(' ').sort() || [];
}
// 初始化勾选迁移表
srcTreeRef.value.setCheckedKeys(state.form.checkedKeys.split(','));
});
watch(
() => state.filterSrcTableText,
(val) => {
srcTreeRef.value!.filter(val);
}
);
const onSelectSrcDb = async (params: any) => {
// 初始化数据源
params.databases = params.dbs; // 数据源里需要这个值
await loadDbTables(params.id, params.db);
};
const onSelectTargetDb = async (params: any) => {
console.log(params);
};
const loadDbTables = async (dbId: number, db: string) => {
// 加载db下的表
srcTableList.value = await dbApi.tableInfos.request({ id: dbId, db });
handleLoadSrcTableTree();
};
const handleSrcTableCheckChange = (data: { id: string; name: string }, checked: boolean) => {
if (data.id === 'all') {
srcTableListDisabled.value = checked;
if (checked) {
state.form.checkedKeys = 'all';
} else {
state.form.checkedKeys = '';
}
}
if (data.id && (data.id + '').startsWith('list-item')) {
}
};
const filterSrcTableTreeNode = (value: string, data: any) => {
if (!value) return true;
return data.label.includes(value);
};
const handleLoadSrcTableTree = () => {
state.srcTableTree[0].children[1].children = srcTableList.value.map((item) => {
return {
id: item.tableName,
label: item.tableName + (item.tableComment && '-' + item.tableComment),
disabled: srcTableListDisabled,
};
});
};
const getReqForm = async () => {
return { ...state.form };
};
const srcTreeRef = ref();
const getCheckedKeys = () => {
let checks = srcTreeRef.value!.getCheckedKeys(false);
if (checks.indexOf('all') >= 0) {
return ['all'];
}
return checks.filter((item: any) => !defaultKeys.includes(item));
};
const btnOk = async () => {
dbForm.value.validate(async (valid: boolean) => {
if (!valid) {
ElMessage.error('请正确填写信息');
return false;
}
state.submitForm = await getReqForm();
let checkedKeys = getCheckedKeys();
if (checkedKeys.length > 0) {
state.submitForm.checkedKeys = checkedKeys.join(',');
}
if (!state.submitForm.checkedKeys) {
ElMessage.error('请选择需要迁移的表');
return false;
}
await saveExec();
ElMessage.success('保存成功');
emit('val-change', state.form);
cancel();
});
};
const cancel = () => {
dialogVisible.value = false;
emit('cancel');
};
</script>
<style lang="scss">
.db-transfer-edit {
.el-select {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,180 @@
<template>
<div class="db-list">
<page-table
ref="pageTableRef"
:page-api="dbApi.dbTransferTasks"
:searchItems="searchItems"
v-model:query-form="query"
:show-selection="true"
v-model:selection-data="state.selectionData"
:columns="columns"
>
<template #tableHeader>
<el-button v-auth="perms.save" type="primary" icon="plus" @click="edit(false)">添加</el-button>
<el-button v-auth="perms.del" :disabled="selectionData.length < 1" @click="del()" type="danger" icon="delete">删除</el-button>
</template>
<template #srcDb="{ data }">
<el-tooltip :content="`${data.srcTagPath} > ${data.srcInstName} > ${data.srcDbName}`">
<span>
<SvgIcon :name="getDbDialect(data.srcDbType).getInfo().icon" :size="18" />
{{ data.srcInstName }}
</span>
</el-tooltip>
</template>
<template #targetDb="{ data }">
<el-tooltip :content="`${data.targetTagPath} > ${data.targetInstName} > ${data.targetDbName}`">
<span>
<SvgIcon :name="getDbDialect(data.targetDbType).getInfo().icon" :size="18" />
{{ data.targetInstName }}
</span>
</el-tooltip>
</template>
<template #action="{ data }">
<!-- 删除启停用编辑 -->
<el-button v-if="actionBtns[perms.save]" @click="edit(data)" type="primary" link>编辑</el-button>
<el-button v-if="actionBtns[perms.log]" type="primary" link @click="log(data)">日志</el-button>
<el-button v-if="data.runningState === 1" @click="stop(data.id)" type="danger" link>停止</el-button>
<el-button v-if="actionBtns[perms.run] && data.runningState !== 1" type="primary" link @click="reRun(data)">运行</el-button>
</template>
</page-table>
<db-transfer-edit @val-change="search" :title="editDialog.title" v-model:visible="editDialog.visible" v-model:data="editDialog.data" />
<db-transfer-log v-model:visible="logsDialog.visible" v-model:taskId="logsDialog.taskId" :running="state.logsDialog.running" />
</div>
</template>
<script lang="ts" setup>
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { dbApi } from './api';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { hasPerms } from '@/components/auth/auth';
import { SearchItem } from '@/components/SearchForm';
import { getDbDialect } from '@/views/ops/db/dialect';
const DbTransferEdit = defineAsyncComponent(() => import('./DbTransferEdit.vue'));
const DbTransferLog = defineAsyncComponent(() => import('./DbTransferLog.vue'));
const perms = {
save: 'db:transfer:save',
del: 'db:transfer:del',
status: 'db:transfer:status',
log: 'db:transfer:log',
run: 'db:transfer:run',
};
const searchItems = [SearchItem.input('name', '名称')];
const columns = ref([
TableColumn.new('srcDb', '源库').setMinWidth(250).isSlot(),
TableColumn.new('targetDb', '目标库').setMinWidth(250).isSlot(),
TableColumn.new('modifier', '修改人').alignCenter(),
TableColumn.new('updateTime', '修改时间').alignCenter().isTime(),
]);
// 该用户拥有的的操作列按钮权限
const actionBtns = hasPerms([perms.save, perms.del, perms.status, perms.log, perms.run]);
const actionWidth = ((actionBtns[perms.save] ? 1 : 0) + (actionBtns[perms.log] ? 1 : 0) + (actionBtns[perms.run] ? 1 : 0)) * 55;
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(actionWidth).fixedRight().alignCenter();
const pageTableRef: Ref<any> = ref(null);
const state = reactive({
row: {},
dbId: 0,
db: '',
/**
* 选中的数据
*/
selectionData: [],
/**
* 查询条件
*/
query: {
name: null,
pageNum: 1,
pageSize: 0,
},
editDialog: {
visible: false,
data: null as any,
title: '新增数据数据迁移任务',
},
logsDialog: {
taskId: 0,
visible: false,
data: null as any,
running: false,
},
});
const { selectionData, query, editDialog, logsDialog } = toRefs(state);
onMounted(async () => {
if (Object.keys(actionBtns).length > 0) {
columns.value.push(actionColumn);
}
});
const search = () => {
pageTableRef.value.search();
};
const edit = async (data: any) => {
if (!data) {
state.editDialog.data = null;
state.editDialog.title = '新增数据库迁移任务';
} else {
state.editDialog.data = data;
state.editDialog.title = '修改数据库迁移任务';
}
state.editDialog.visible = true;
};
const stop = async (id: any) => {
await ElMessageBox.confirm(`确定停止?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await dbApi.stopDbTransferTask.request({ taskId: id });
ElMessage.success(`停止成功`);
search();
};
const log = async (data: any) => {
state.logsDialog.taskId = data.id;
state.logsDialog.visible = true;
state.logsDialog.running = data.state === 1;
};
const reRun = async (data: any) => {
await ElMessageBox.confirm(`确定运行?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await dbApi.runDbTransferTask.request({ taskId: data.id });
ElMessage.success('运行成功');
search();
};
const del = async () => {
try {
await ElMessageBox.confirm(`确定删除任务?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await dbApi.deleteDbTransferTask.request({ taskId: state.selectionData.map((x: any) => x.id).join(',') });
ElMessage.success('删除成功');
search();
} catch (err) {
//
}
};
</script>
<style lang="scss"></style>

View File

@@ -0,0 +1,117 @@
<template>
<div class="sync-task-logs">
<el-dialog v-model="dialogVisible" :before-close="cancel" :destroy-on-close="false" width="1120px">
<template #header>
<span class="mr10">任务执行日志</span>
<el-switch v-model="realTime" @change="watchPolling" inline-prompt active-text="实时" inactive-text="非实时" />
<el-button @click="search" icon="Refresh" circle size="small" :loading="realTime" class="ml10"></el-button>
</template>
<page-table
ref="logTableRef"
:page-api="dbApi.dbTransferTaskLogs"
v-model:query-form="query"
:tool-button="false"
:columns="columns"
size="small"
/>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { reactive, Ref, ref, toRefs, watch } from 'vue';
import { dbApi } from '@/views/ops/db/api';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { DbDataSyncLogStatusEnum } from './enums';
const props = defineProps({
taskId: {
type: Number,
},
running: {
type: Boolean,
default: false,
},
});
const dialogVisible = defineModel<boolean>('visible', { default: false });
const columns = ref([
// 状态:1.成功 -1.失败
TableColumn.new('status', '状态').alignCenter().typeTag(DbDataSyncLogStatusEnum),
TableColumn.new('createTime', '时间').alignCenter().isTime(),
TableColumn.new('errText', '日志'),
TableColumn.new('dataSqlFull', 'SQL').alignCenter(),
TableColumn.new('resNum', '数据条数'),
]);
watch(dialogVisible, (newValue: any) => {
if (!newValue) {
state.polling = false;
watchPolling(false);
return;
}
state.query.taskId = props.taskId!;
search();
state.realTime = props.running;
watchPolling(props.running);
});
const startPolling = () => {
if (!state.polling) {
state.polling = true;
state.pollingIndex = setInterval(search, 1000);
}
};
const stopPolling = () => {
if (state.polling) {
state.polling = false;
clearInterval(state.pollingIndex);
}
};
const watchPolling = (polling: boolean) => {
if (polling) {
startPolling();
} else {
stopPolling();
}
};
const logTableRef: Ref<any> = ref(null);
const search = () => {
try {
logTableRef.value.search();
} catch (e) {
/* empty */
}
};
const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
//定义事件
const cancel = () => {
dialogVisible.value = false;
emit('cancel');
watchPolling(false);
};
const state = reactive({
polling: false,
pollingIndex: 0 as any,
realTime: props.running,
/**
* 查询条件
*/
query: {
taskId: 0,
name: null,
pageNum: 1,
pageSize: 0,
},
});
const { query, realTime } = toRefs(state);
</script>

View File

@@ -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;
}

View File

@@ -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 = {

View File

@@ -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);
}
/**

View File

@@ -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' },

View File

@@ -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}'; `;

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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"`
}

View File

@@ -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"`
}

View File

@@ -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")
}

View File

@@ -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)
}

View File

@@ -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
}
}

View File

@@ -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)
}

View File

@@ -19,7 +19,7 @@ type MetaData interface {
GetDbNames() ([]string, error)
// 获取表信息
GetTables() ([]Table, error)
GetTables(tableNames ...string) ([]Table, error)
// 获取指定表名的所有列元信息
GetColumns(tableNames ...string) ([]Column, error)

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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",

View File

@@ -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,

View File

@@ -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) {
}

View File

@@ -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 {

View File

@@ -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) {
}

View File

@@ -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 {

View File

@@ -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) {
}

View File

@@ -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 {

View File

@@ -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) {
}

View File

@@ -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 {

View File

@@ -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))
}
}
}

View File

@@ -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 {

View File

@@ -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) {
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 // 手动停止状态
)

View File

@@ -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"`

View File

@@ -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)
}

View File

@@ -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)}}
}

View File

@@ -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"))

View File

@@ -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[:])
}

View File

@@ -10,4 +10,5 @@ func Init(router *gin.RouterGroup) {
InitDbBackupRouter(router)
InitDbRestoreRouter(router)
InitDbDataSyncRouter(router)
InitDbTransferRouter(router)
}

View File

@@ -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
}

View File

@@ -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);