mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-02 23:40:24 +08:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
252fc553f2 | ||
|
|
ac2ceed3f9 | ||
|
|
3f828cc5b0 | ||
|
|
fc1b9ef35d | ||
|
|
d0b71a1c40 | ||
|
|
a743a6a05a | ||
|
|
0e6b9713ce | ||
|
|
b9afbc764d | ||
|
|
923e183a67 | ||
|
|
7e9a381641 | ||
|
|
bed95254d0 | ||
|
|
e4d13f3377 | ||
|
|
d530365ef9 | ||
|
|
070d4ea104 | ||
|
|
3fc86f0fae | ||
|
|
3b77ab2727 | ||
|
|
76cb991282 | ||
|
|
9efd20f1b9 | ||
|
|
de5b9e46d3 | ||
|
|
f27d3d200f | ||
|
|
f4a64b96a9 | ||
|
|
9a59749763 | ||
|
|
b017b902f8 | ||
|
|
7c53353c60 |
@@ -5,7 +5,7 @@ WORKDIR /mayfly
|
||||
|
||||
COPY mayfly_go_web .
|
||||
|
||||
RUN yarn config set registry 'https://registry.npm.taobao.org' && \
|
||||
RUN yarn config set registry 'https://registry.npmmirror.com' && \
|
||||
yarn install && \
|
||||
yarn build
|
||||
|
||||
@@ -24,7 +24,7 @@ COPY --from=fe-builder /mayfly/dist /mayfly/static/static
|
||||
|
||||
# Build
|
||||
RUN GO111MODULE=on CGO_ENABLED=0 GOOS=linux \
|
||||
go build -a \
|
||||
go build -a -ldflags=-w \
|
||||
-o mayfly-go main.go
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
### 介绍
|
||||
|
||||
web 版 **linux(终端[终端回放] 文件 脚本 进程 计划任务)、数据库(mysql postgres oracle 达梦 高斯)、redis(单机 哨兵 集群)、mongo 统一管理操作平台**
|
||||
web 版 **linux(终端[终端回放] 文件 脚本 进程 计划任务)、数据库(mysql postgres oracle 达梦 高斯 sqlite)、redis(单机 哨兵 集群)、mongo 统一管理操作平台**
|
||||
|
||||
### 开发语言与主要框架
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 lyt-Top
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -17,7 +17,7 @@
|
||||
"countup.js": "^2.7.0",
|
||||
"cropperjs": "^1.5.11",
|
||||
"echarts": "^5.4.3",
|
||||
"element-plus": "^2.5.1",
|
||||
"element-plus": "^2.5.3",
|
||||
"js-base64": "^3.7.5",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -33,7 +33,7 @@
|
||||
"splitpanes": "^3.1.5",
|
||||
"sql-formatter": "^15.0.2",
|
||||
"uuid": "^9.0.1",
|
||||
"vue": "^3.4.14",
|
||||
"vue": "^3.4.15",
|
||||
"vue-router": "^4.2.5",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-addon-fit": "^0.8.0",
|
||||
@@ -49,13 +49,14 @@
|
||||
"@typescript-eslint/parser": "^6.7.4",
|
||||
"@vitejs/plugin-vue": "^5.0.3",
|
||||
"@vue/compiler-sfc": "^3.4.14",
|
||||
"code-inspector-plugin": "^0.4.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "^8.35.0",
|
||||
"eslint-plugin-vue": "^9.19.2",
|
||||
"prettier": "^3.1.0",
|
||||
"sass": "^1.69.0",
|
||||
"typescript": "^5.3.2",
|
||||
"vite": "^5.0.11",
|
||||
"vite": "^5.0.12",
|
||||
"vue-eslint-parser": "^9.4.0"
|
||||
},
|
||||
"browserslist": [
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -55,11 +55,11 @@
|
||||
"unicode_decimal": 58905
|
||||
},
|
||||
{
|
||||
"icon_id": "11617944",
|
||||
"icon_id": "25271976",
|
||||
"name": "oracle",
|
||||
"font_class": "oracle",
|
||||
"unicode": "e6ea",
|
||||
"unicode_decimal": 59114
|
||||
"unicode": "e507",
|
||||
"unicode_decimal": 58631
|
||||
},
|
||||
{
|
||||
"icon_id": "8105644",
|
||||
@@ -67,6 +67,27 @@
|
||||
"font_class": "mariadb",
|
||||
"unicode": "e513",
|
||||
"unicode_decimal": 58643
|
||||
},
|
||||
{
|
||||
"icon_id": "13601813",
|
||||
"name": "sqlite",
|
||||
"font_class": "sqlite",
|
||||
"unicode": "e546",
|
||||
"unicode_decimal": 58694
|
||||
},
|
||||
{
|
||||
"icon_id": "29340317",
|
||||
"name": "temp-mssql",
|
||||
"font_class": "MSSQLNATIVE",
|
||||
"unicode": "e600",
|
||||
"unicode_decimal": 58880
|
||||
},
|
||||
{
|
||||
"icon_id": "7699332",
|
||||
"name": "gaussdb",
|
||||
"font_class": "gauss",
|
||||
"unicode": "e683",
|
||||
"unicode_decimal": 59011
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ const config = {
|
||||
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
|
||||
|
||||
// 系统版本
|
||||
version: 'v1.7.0',
|
||||
version: 'v1.7.2',
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<el-input v-model="cron" placeholder="可点击左边按钮进行可视化配置">
|
||||
<el-input v-model="cron" placeholder="可点击左边按钮配置">
|
||||
<template #prepend>
|
||||
<el-button @click="showCron = true" icon="Pointer"></el-button>
|
||||
</template>
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
<template>
|
||||
<div class="layout-search-dialog">
|
||||
<el-dialog v-model="state.isShowSearch" width="300px" destroy-on-close :modal="false" fullscreen :show-close="false">
|
||||
<el-autocomplete v-model="state.menuQuery" :fetch-suggestions="menuSearch" placeholder="菜单搜索"
|
||||
prefix-icon="el-icon-search" ref="layoutMenuAutocompleteRef" @select="onHandleSelect" @blur="onSearchBlur">
|
||||
<el-autocomplete
|
||||
v-model="state.menuQuery"
|
||||
:fetch-suggestions="menuSearch"
|
||||
placeholder="菜单搜索"
|
||||
prefix-icon="el-icon-search"
|
||||
ref="layoutMenuAutocompleteRef"
|
||||
@select="onHandleSelect"
|
||||
@blur="onSearchBlur"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="el-input__icon">
|
||||
<search />
|
||||
</el-icon>
|
||||
</template>
|
||||
<template #default="{ item }">
|
||||
<div>
|
||||
<SvgIcon :name="item.meta.icon" class="mr5" />{{ item.meta.title }}
|
||||
</div>
|
||||
<div><SvgIcon :name="item.meta.icon" class="mr5" />{{ item.meta.title }}</div>
|
||||
</template>
|
||||
</el-autocomplete>
|
||||
</el-dialog>
|
||||
@@ -23,7 +28,7 @@ import { reactive, ref, nextTick } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRoutesList } from '@/store/routesList';
|
||||
|
||||
const layoutMenuAutocompleteRef: any = ref(null);;
|
||||
const layoutMenuAutocompleteRef: any = ref(null);
|
||||
const router = useRouter();
|
||||
const state: any = reactive({
|
||||
isShowSearch: false,
|
||||
@@ -54,8 +59,7 @@ const menuSearch = (queryString: any, cb: any) => {
|
||||
const createFilter = (queryString: any) => {
|
||||
return (restaurant: any) => {
|
||||
return (
|
||||
restaurant.path.toLowerCase().indexOf(queryString.toLowerCase()) > -1 ||
|
||||
restaurant.meta.title.toLowerCase().indexOf(queryString.toLowerCase()) > -1
|
||||
restaurant.path.toLowerCase().indexOf(queryString.toLowerCase()) > -1 || restaurant.meta.title.toLowerCase().indexOf(queryString.toLowerCase()) > -1
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -97,7 +101,7 @@ const onSearchBlur = () => {
|
||||
closeSearch();
|
||||
};
|
||||
|
||||
defineExpose({openSearch})
|
||||
defineExpose({ openSearch });
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -73,12 +73,28 @@ const currentTime = computed(() => {
|
||||
|
||||
// 初始化数字滚动
|
||||
const initNumCountUp = async () => {
|
||||
const res: any = await indexApi.getIndexCount.request();
|
||||
nextTick(() => {
|
||||
new CountUp('mongoNum', res.mongoNum).start();
|
||||
new CountUp('machineNum', res.machineNum).start();
|
||||
new CountUp('dbNum', res.dbNum).start();
|
||||
new CountUp('redisNum', res.redisNum).start();
|
||||
indexApi.machineDashbord.request().then((res: any) => {
|
||||
nextTick(() => {
|
||||
new CountUp('machineNum', res.machineNum).start();
|
||||
});
|
||||
});
|
||||
|
||||
indexApi.dbDashbord.request().then((res: any) => {
|
||||
nextTick(() => {
|
||||
new CountUp('dbNum', res.dbNum).start();
|
||||
});
|
||||
});
|
||||
|
||||
indexApi.redisDashbord.request().then((res: any) => {
|
||||
nextTick(() => {
|
||||
new CountUp('redisNum', res.redisNum).start();
|
||||
});
|
||||
});
|
||||
|
||||
indexApi.mongoDashbord.request().then((res: any) => {
|
||||
nextTick(() => {
|
||||
new CountUp('mongoNum', res.mongoNum).start();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import Api from '@/common/Api';
|
||||
|
||||
export const indexApi = {
|
||||
getIndexCount: Api.newGet("/common/index/count"),
|
||||
}
|
||||
|
||||
machineDashbord: Api.newGet('/machines/dashbord'),
|
||||
dbDashbord: Api.newGet('/dbs/dashbord'),
|
||||
redisDashbord: Api.newGet('/redis/dashbord'),
|
||||
mongoDashbord: Api.newGet('/mongos/dashbord'),
|
||||
};
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="name" label="任务名称">
|
||||
<el-input v-model.number="state.form.name" type="text" placeholder="任务名称"></el-input>
|
||||
<el-input v-model="state.form.name" type="text" placeholder="任务名称"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="startTime" label="开始时间">
|
||||
<el-date-picker v-model="state.form.startTime" type="datetime" placeholder="开始时间" />
|
||||
@@ -101,7 +101,7 @@ const state = reactive({
|
||||
id: 0,
|
||||
dbId: 0,
|
||||
dbNames: '',
|
||||
name: null as any,
|
||||
name: '',
|
||||
intervalDay: null,
|
||||
startTime: null as any,
|
||||
repeated: null as any,
|
||||
|
||||
155
mayfly_go_web/src/views/ops/db/DbBackupHistoryList.vue
Normal file
155
mayfly_go_web/src/views/ops/db/DbBackupHistoryList.vue
Normal file
@@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<div class="db-backup-history">
|
||||
<page-table
|
||||
height="100%"
|
||||
ref="pageTableRef"
|
||||
:page-api="dbApi.getDbBackupHistories"
|
||||
:show-selection="true"
|
||||
v-model:selection-data="state.selectedData"
|
||||
:searchItems="searchItems"
|
||||
:before-query-fn="beforeQueryFn"
|
||||
v-model:query-form="query"
|
||||
:columns="columns"
|
||||
>
|
||||
<template #dbSelect>
|
||||
<el-select v-model="query.dbName" placeholder="请选择数据库" style="width: 200px" filterable clearable>
|
||||
<el-option v-for="item in props.dbNames" :key="item" :label="`${item}`" :value="item"> </el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<template #tableHeader>
|
||||
<el-button type="primary" icon="back" @click="restoreDbBackupHistory(null)">立即恢复</el-button>
|
||||
<el-button type="danger" icon="delete" @click="deleteDbBackupHistory(null)">删除</el-button>
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<div>
|
||||
<el-button @click="restoreDbBackupHistory(data)" type="primary" link>立即恢复</el-button>
|
||||
<el-button @click="deleteDbBackupHistory(data)" type="danger" link>删除</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</page-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, reactive, Ref, ref } from 'vue';
|
||||
import { dbApi } from './api';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
const pageTableRef: Ref<any> = ref(null);
|
||||
|
||||
const props = defineProps({
|
||||
dbId: {
|
||||
type: [Number],
|
||||
required: true,
|
||||
},
|
||||
dbNames: {
|
||||
type: [Array<String>],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const searchItems = [SearchItem.slot('dbName', '数据库名称', 'dbSelect')];
|
||||
|
||||
const columns = [
|
||||
TableColumn.new('dbName', '数据库名称'),
|
||||
TableColumn.new('name', '备份名称'),
|
||||
TableColumn.new('createTime', '创建时间').isTime(),
|
||||
TableColumn.new('lastResult', '恢复结果'),
|
||||
TableColumn.new('lastTime', '恢复时间').isTime(),
|
||||
TableColumn.new('action', '操作').isSlot().setMinWidth(160).fixedRight(),
|
||||
];
|
||||
|
||||
const emptyQuery = {
|
||||
dbId: 0,
|
||||
dbName: '',
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
};
|
||||
|
||||
const state = reactive({
|
||||
data: [],
|
||||
total: 0,
|
||||
query: emptyQuery,
|
||||
/**
|
||||
* 选中的数据
|
||||
*/
|
||||
selectedData: [],
|
||||
});
|
||||
|
||||
const { query } = toRefs(state);
|
||||
|
||||
const beforeQueryFn = (query: any) => {
|
||||
query.dbId = props.dbId;
|
||||
return query;
|
||||
};
|
||||
|
||||
const search = async () => {
|
||||
await pageTableRef.value.search();
|
||||
};
|
||||
|
||||
const deleteDbBackupHistory = async (data: any) => {
|
||||
let backupHistoryId: string;
|
||||
if (data) {
|
||||
backupHistoryId = data.id;
|
||||
} else if (state.selectedData.length > 0) {
|
||||
backupHistoryId = state.selectedData.map((x: any) => x.id).join(' ');
|
||||
} else {
|
||||
ElMessage.error('请选择需要删除的数据库备份历史');
|
||||
return;
|
||||
}
|
||||
await ElMessageBox.confirm(`确定删除 “数据库备份历史” 吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
await dbApi.deleteDbBackupHistory.request({ dbId: props.dbId, backupHistoryId: backupHistoryId });
|
||||
await search();
|
||||
ElMessage.success('删除成功');
|
||||
};
|
||||
|
||||
const restoreDbBackupHistory = async (data: any) => {
|
||||
let backupHistoryId: string;
|
||||
if (data) {
|
||||
backupHistoryId = data.id;
|
||||
} else if (state.selectedData.length > 0) {
|
||||
const pluralDbNames: string[] = [];
|
||||
const dbNames: Map<string, boolean> = new Map();
|
||||
state.selectedData.forEach((item: any) => {
|
||||
if (!dbNames.has(item.dbName)) {
|
||||
dbNames.set(item.dbName, false);
|
||||
return;
|
||||
}
|
||||
if (!dbNames.get(item.dbName)) {
|
||||
dbNames.set(item.dbName, true);
|
||||
pluralDbNames.push(item.dbName);
|
||||
}
|
||||
});
|
||||
if (pluralDbNames.length > 0) {
|
||||
ElMessage.error('多次选择相同数据库:' + pluralDbNames.join(', '));
|
||||
return;
|
||||
}
|
||||
backupHistoryId = state.selectedData.map((x: any) => x.id).join(' ');
|
||||
} else {
|
||||
ElMessage.error('请选择需要恢复的数据库备份历史');
|
||||
return;
|
||||
}
|
||||
await ElMessageBox.confirm(`确定从 “数据库备份历史” 中恢复数据库吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
|
||||
await dbApi.restoreDbBackupHistory.request({
|
||||
dbId: props.dbId,
|
||||
backupHistoryId: backupHistoryId,
|
||||
});
|
||||
await search();
|
||||
ElMessage.success('成功创建数据库恢复任务');
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
@@ -21,6 +21,7 @@
|
||||
<el-button type="primary" icon="plus" @click="createDbBackup()">添加</el-button>
|
||||
<el-button type="primary" icon="video-play" @click="enableDbBackup(null)">启用</el-button>
|
||||
<el-button type="primary" icon="video-pause" @click="disableDbBackup(null)">禁用</el-button>
|
||||
<el-button type="danger" icon="delete" @click="deleteDbBackup(null)">删除</el-button>
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
@@ -29,6 +30,7 @@
|
||||
<el-button v-if="!data.enabled" @click="enableDbBackup(data)" type="primary" link>启用</el-button>
|
||||
<el-button v-if="data.enabled" @click="disableDbBackup(data)" type="primary" link>禁用</el-button>
|
||||
<el-button v-if="data.enabled" @click="startDbBackup(data)" type="primary" link>立即备份</el-button>
|
||||
<el-button @click="deleteDbBackup(data)" type="danger" link>删除</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</page-table>
|
||||
@@ -49,7 +51,7 @@ import { dbApi } from './api';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
const DbBackupEdit = defineAsyncComponent(() => import('./DbBackupEdit.vue'));
|
||||
const pageTableRef: Ref<any> = ref(null);
|
||||
@@ -72,10 +74,10 @@ const columns = [
|
||||
TableColumn.new('name', '任务名称'),
|
||||
TableColumn.new('startTime', '启动时间').isTime(),
|
||||
TableColumn.new('intervalDay', '备份周期'),
|
||||
TableColumn.new('enabled', '是否启用'),
|
||||
TableColumn.new('enabledDesc', '是否启用'),
|
||||
TableColumn.new('lastResult', '执行结果'),
|
||||
TableColumn.new('lastTime', '执行时间').isTime(),
|
||||
TableColumn.new('action', '操作').isSlot().setMinWidth(180).fixedRight(),
|
||||
TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight(),
|
||||
];
|
||||
|
||||
const emptyQuery = {
|
||||
@@ -168,5 +170,25 @@ const startDbBackup = async (data: any) => {
|
||||
await search();
|
||||
ElMessage.success('备份任务启动成功');
|
||||
};
|
||||
|
||||
const deleteDbBackup = async (data: any) => {
|
||||
let backupId: string;
|
||||
if (data) {
|
||||
backupId = data.id;
|
||||
} else if (state.selectedData.length > 0) {
|
||||
backupId = state.selectedData.map((x: any) => x.id).join(' ');
|
||||
} else {
|
||||
ElMessage.error('请选择需要删除的数据库备份任务');
|
||||
return;
|
||||
}
|
||||
await ElMessageBox.confirm(`确定删除 “数据库备份任务” 吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
await dbApi.deleteDbBackup.request({ dbId: props.dbId, backupId: backupId });
|
||||
await search();
|
||||
ElMessage.success('删除成功');
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
|
||||
@@ -62,8 +62,21 @@
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item :command="{ type: 'detail', data }"> 详情 </el-dropdown-item>
|
||||
<el-dropdown-item :command="{ type: 'dumpDb', data }" v-if="supportAction('dumpDb', data.type)"> 导出 </el-dropdown-item>
|
||||
<el-dropdown-item :command="{ type: 'dbBackup', data }" v-if="supportAction('dbBackup', data.type)"> 备份 </el-dropdown-item>
|
||||
<el-dropdown-item :command="{ type: 'dbRestore', data }" v-if="supportAction('dbRestore', data.type)"> 恢复 </el-dropdown-item>
|
||||
<el-dropdown-item :command="{ type: 'backupDb', data }" v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)">
|
||||
备份任务
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
:command="{ type: 'backupHistory', data }"
|
||||
v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)"
|
||||
>
|
||||
备份历史
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
:command="{ type: 'restoreDb', data }"
|
||||
v-if="actionBtns[perms.restoreDb] && supportAction('restoreDb', data.type)"
|
||||
>
|
||||
恢复任务
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
@@ -131,6 +144,16 @@
|
||||
<db-backup-list :dbId="dbBackupDialog.dbId" :dbNames="dbBackupDialog.dbs" />
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
width="80%"
|
||||
:title="`${dbBackupHistoryDialog.title} - 数据库备份历史`"
|
||||
:close-on-click-modal="false"
|
||||
:destroy-on-close="true"
|
||||
v-model="dbBackupHistoryDialog.visible"
|
||||
>
|
||||
<db-backup-history-list :dbId="dbBackupHistoryDialog.dbId" :dbNames="dbBackupHistoryDialog.dbs" />
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
width="80%"
|
||||
:title="`${dbRestoreDialog.title} - 数据库恢复`"
|
||||
@@ -185,6 +208,7 @@ import { getDbDialect } from './dialect/index';
|
||||
import { getTagPathSearchItem } from '../component/tag';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import DbBackupList from './DbBackupList.vue';
|
||||
import DbBackupHistoryList from './DbBackupHistoryList.vue';
|
||||
import DbRestoreList from './DbRestoreList.vue';
|
||||
|
||||
const DbEdit = defineAsyncComponent(() => import('./DbEdit.vue'));
|
||||
@@ -193,6 +217,8 @@ const perms = {
|
||||
base: 'db',
|
||||
saveDb: 'db:save',
|
||||
delDb: 'db:del',
|
||||
backupDb: 'db:backup',
|
||||
restoreDb: 'db:restore',
|
||||
};
|
||||
|
||||
const searchItems = [getTagPathSearchItem(TagResourceTypeEnum.Db.value), SearchItem.slot('instanceId', '实例', 'instanceSelect')];
|
||||
@@ -208,7 +234,8 @@ const columns = ref([
|
||||
]);
|
||||
|
||||
// 该用户拥有的的操作列按钮权限
|
||||
const actionBtns = hasPerms([perms.base, perms.saveDb]);
|
||||
// const actionBtns = hasPerms([perms.base, perms.saveDb]);
|
||||
const actionBtns = hasPerms(Object.values(perms));
|
||||
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight().alignCenter();
|
||||
|
||||
const route = useRoute();
|
||||
@@ -253,6 +280,13 @@ const state = reactive({
|
||||
dbs: [],
|
||||
dbId: 0,
|
||||
},
|
||||
// 数据库备份历史弹框
|
||||
dbBackupHistoryDialog: {
|
||||
title: '',
|
||||
visible: false,
|
||||
dbs: [],
|
||||
dbId: 0,
|
||||
},
|
||||
// 数据库恢复弹框
|
||||
dbRestoreDialog: {
|
||||
title: '',
|
||||
@@ -285,7 +319,8 @@ const state = reactive({
|
||||
},
|
||||
});
|
||||
|
||||
const { db, selectionData, query, infoDialog, sqlExecLogDialog, exportDialog, dbEditDialog, dbBackupDialog, dbRestoreDialog } = toRefs(state);
|
||||
const { db, selectionData, query, infoDialog, sqlExecLogDialog, exportDialog, dbEditDialog, dbBackupDialog, dbBackupHistoryDialog, dbRestoreDialog } =
|
||||
toRefs(state);
|
||||
|
||||
onMounted(async () => {
|
||||
if (Object.keys(actionBtns).length > 0) {
|
||||
@@ -345,11 +380,15 @@ const handleMoreActionCommand = (commond: any) => {
|
||||
onDumpDbs(data);
|
||||
return;
|
||||
}
|
||||
case 'dbBackup': {
|
||||
case 'backupDb': {
|
||||
onShowDbBackupDialog(data);
|
||||
return;
|
||||
}
|
||||
case 'dbRestore': {
|
||||
case 'backupHistory': {
|
||||
onShowDbBackupHistoryDialog(data);
|
||||
return;
|
||||
}
|
||||
case 'restoreDb': {
|
||||
onShowDbRestoreDialog(data);
|
||||
return;
|
||||
}
|
||||
@@ -402,6 +441,13 @@ const onShowDbBackupDialog = async (row: any) => {
|
||||
state.dbBackupDialog.visible = true;
|
||||
};
|
||||
|
||||
const onShowDbBackupHistoryDialog = async (row: any) => {
|
||||
state.dbBackupHistoryDialog.title = `${row.name}`;
|
||||
state.dbBackupHistoryDialog.dbId = row.id;
|
||||
state.dbBackupHistoryDialog.dbs = row.database.split(' ');
|
||||
state.dbBackupHistoryDialog.visible = true;
|
||||
};
|
||||
|
||||
const onShowDbRestoreDialog = async (row: any) => {
|
||||
state.dbRestoreDialog.title = `${row.name}`;
|
||||
state.dbRestoreDialog.dbId = row.id;
|
||||
@@ -455,7 +501,7 @@ const supportAction = (action: string, dbType: string): boolean => {
|
||||
switch (dbType) {
|
||||
case DbType.mysql:
|
||||
case DbType.mariadb:
|
||||
actions = ['dumpDb', 'dbBackup', 'dbRestore'];
|
||||
actions = ['dumpDb', 'backupDb', 'restoreDb'];
|
||||
}
|
||||
return actions.includes(action);
|
||||
};
|
||||
|
||||
@@ -35,7 +35,13 @@
|
||||
clearable
|
||||
class="w100"
|
||||
>
|
||||
<el-option v-for="item in state.histories" :key="item.id" :label="item.name" :value="item"> </el-option>
|
||||
<el-option
|
||||
v-for="item in state.histories"
|
||||
:key="item.id"
|
||||
:label="item.name + (item.binlogFileName ? ' ' : ' 不') + '支持指定时间点恢复'"
|
||||
:value="item"
|
||||
>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item prop="startTime" label="开始时间">
|
||||
@@ -56,7 +62,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, reactive, ref, watch } from 'vue';
|
||||
import { dbApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
@@ -83,20 +89,30 @@ const visible = defineModel<boolean>('visible', {
|
||||
});
|
||||
|
||||
const validatePointInTime = (rule: any, value: any, callback: any) => {
|
||||
if (!state.histories || state.histories.length == 0) {
|
||||
callback(new Error('数据库没有备份记录'));
|
||||
return;
|
||||
}
|
||||
const history = state.histories[state.histories.length - 1];
|
||||
if (value < new Date(history.createTime)) {
|
||||
callback(new Error('在此之前数据库没有备份记录'));
|
||||
return;
|
||||
}
|
||||
if (value > new Date()) {
|
||||
callback(new Error('恢复时间点晚于当前时间'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
if (!state.histories || state.histories.length == 0) {
|
||||
callback(new Error('数据库没有备份记录'));
|
||||
return;
|
||||
}
|
||||
let last = null;
|
||||
for (const history of state.histories) {
|
||||
if (!history.binlogFileName || history.binlogFileName.length === 0) {
|
||||
break;
|
||||
}
|
||||
if (new Date(history.createTime) < value) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
last = history;
|
||||
}
|
||||
if (!last) {
|
||||
callback(new Error('现有数据库备份不支持指定时间恢复'));
|
||||
return;
|
||||
}
|
||||
callback(last.name + ' 之前的数据库备份不支持指定时间恢复');
|
||||
};
|
||||
|
||||
const rules = {
|
||||
@@ -110,7 +126,6 @@ const rules = {
|
||||
pointInTime: [
|
||||
{
|
||||
required: true,
|
||||
// message: '请选择恢复时间点',
|
||||
validator: validatePointInTime,
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
@@ -146,7 +161,7 @@ const state = reactive({
|
||||
id: 0,
|
||||
dbId: 0,
|
||||
dbName: null as any,
|
||||
intervalDay: 1,
|
||||
intervalDay: 0,
|
||||
startTime: null as any,
|
||||
repeated: null as any,
|
||||
dbBackupId: null as any,
|
||||
@@ -218,7 +233,8 @@ const init = async (data: any) => {
|
||||
} else {
|
||||
state.form.dbName = '';
|
||||
state.editOrCreate = false;
|
||||
state.form.intervalDay = 1;
|
||||
state.form.intervalDay = 0;
|
||||
state.form.repeated = false;
|
||||
state.form.pointInTime = new Date();
|
||||
state.form.startTime = new Date();
|
||||
state.histories = [];
|
||||
@@ -237,6 +253,12 @@ const getDbNamesWithoutRestore = async () => {
|
||||
const btnOk = async () => {
|
||||
restoreForm.value.validate(async (valid: any) => {
|
||||
if (valid) {
|
||||
await ElMessageBox.confirm(`确定恢复数据库吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
|
||||
if (state.restoreMode == 'point-in-time') {
|
||||
state.form.dbBackupId = 0;
|
||||
state.form.dbBackupHistoryId = 0;
|
||||
@@ -245,13 +267,14 @@ const btnOk = async () => {
|
||||
state.form.pointInTime = null;
|
||||
}
|
||||
state.form.repeated = false;
|
||||
state.form.intervalDay = 0;
|
||||
const reqForm = { ...state.form };
|
||||
let api = dbApi.createDbRestore;
|
||||
if (props.data) {
|
||||
api = dbApi.saveDbRestore;
|
||||
}
|
||||
api.request(reqForm).then(() => {
|
||||
ElMessage.success('保存成功');
|
||||
ElMessage.success('成功创建数据库恢复任务');
|
||||
emit('val-change', state.form);
|
||||
state.btnLoading = true;
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -21,12 +21,14 @@
|
||||
<el-button type="primary" icon="plus" @click="createDbRestore()">添加</el-button>
|
||||
<el-button type="primary" icon="video-play" @click="enableDbRestore(null)">启用</el-button>
|
||||
<el-button type="primary" icon="video-pause" @click="disableDbRestore(null)">禁用</el-button>
|
||||
<el-button type="danger" icon="delete" @click="deleteDbRestore(null)">删除</el-button>
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<el-button @click="showDbRestore(data)" type="primary" link>详情</el-button>
|
||||
<el-button @click="enableDbRestore(data)" v-if="!data.enabled" type="primary" link>启用</el-button>
|
||||
<el-button @click="disableDbRestore(data)" v-if="data.enabled" type="primary" link>禁用</el-button>
|
||||
<el-button @click="deleteDbRestore(data)" type="danger" link>删除</el-button>
|
||||
</template>
|
||||
</page-table>
|
||||
|
||||
@@ -49,7 +51,7 @@
|
||||
infoDialog.data.dbBackupHistoryName
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="开始时间">{{ dateFormat(infoDialog.data.startTime) }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="是否启用">{{ infoDialog.data.enabled }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="是否启用">{{ infoDialog.data.enabledDesc }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="执行时间">{{ dateFormat(infoDialog.data.lastTime) }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="执行结果">{{ infoDialog.data.lastResult }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
@@ -63,7 +65,7 @@ import { dbApi } from './api';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { dateFormat } from '@/common/utils/date';
|
||||
const DbRestoreEdit = defineAsyncComponent(() => import('./DbRestoreEdit.vue'));
|
||||
const pageTableRef: Ref<any> = ref(null);
|
||||
@@ -85,7 +87,7 @@ const searchItems = [SearchItem.slot('dbName', '数据库名称', 'dbSelect')];
|
||||
const columns = [
|
||||
TableColumn.new('dbName', '数据库名称'),
|
||||
TableColumn.new('startTime', '启动时间').isTime(),
|
||||
TableColumn.new('enabled', '是否启用'),
|
||||
TableColumn.new('enabledDesc', '是否启用'),
|
||||
TableColumn.new('lastTime', '执行时间').isTime(),
|
||||
TableColumn.new('lastResult', '执行结果'),
|
||||
TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight().alignCenter(),
|
||||
@@ -135,19 +137,39 @@ const createDbRestore = async () => {
|
||||
state.dbRestoreEditDialog.visible = true;
|
||||
};
|
||||
|
||||
const deleteDbRestore = async (data: any) => {
|
||||
let restoreId: string;
|
||||
if (data) {
|
||||
restoreId = data.id;
|
||||
} else if (state.selectedData.length > 0) {
|
||||
restoreId = state.selectedData.map((x: any) => x.id).join(' ');
|
||||
} else {
|
||||
ElMessage.error('请选择需要删除的数据库恢复任务');
|
||||
return;
|
||||
}
|
||||
await ElMessageBox.confirm(`确定删除 “数据库恢复任务” 吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
await dbApi.deleteDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
|
||||
await search();
|
||||
ElMessage.success('删除成功');
|
||||
};
|
||||
|
||||
const showDbRestore = async (data: any) => {
|
||||
state.infoDialog.data = data;
|
||||
state.infoDialog.visible = true;
|
||||
};
|
||||
|
||||
const enableDbRestore = async (data: any) => {
|
||||
let restoreId: String;
|
||||
let restoreId: string;
|
||||
if (data) {
|
||||
restoreId = data.id;
|
||||
} else if (state.selectedData.length > 0) {
|
||||
restoreId = state.selectedData.map((x: any) => x.id).join(' ');
|
||||
} else {
|
||||
ElMessage.error('请选择需要启用的恢复任务');
|
||||
ElMessage.error('请选择需要启用的数据库恢复任务');
|
||||
return;
|
||||
}
|
||||
await dbApi.enableDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
|
||||
@@ -156,13 +178,13 @@ const enableDbRestore = async (data: any) => {
|
||||
};
|
||||
|
||||
const disableDbRestore = async (data: any) => {
|
||||
let restoreId: String;
|
||||
let restoreId: string;
|
||||
if (data) {
|
||||
restoreId = data.id;
|
||||
} else if (state.selectedData.length > 0) {
|
||||
restoreId = state.selectedData.map((x: any) => x.id).join(' ');
|
||||
} else {
|
||||
ElMessage.error('请选择需要禁用的恢复任务');
|
||||
ElMessage.error('请选择需要禁用的数据库恢复任务');
|
||||
return;
|
||||
}
|
||||
await dbApi.disableDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, watch, reactive, onMounted, Ref, ref } from 'vue';
|
||||
import { onMounted, reactive, Ref, ref, toRefs, watch } from 'vue';
|
||||
import { dbApi } from './api';
|
||||
import { DbSqlExecTypeEnum } from './enums';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
@@ -120,6 +120,12 @@ const onShowRollbackSql = async (sqlExecLog: any) => {
|
||||
const primaryKey = getPrimaryKey(columns);
|
||||
const oldValue = JSON.parse(sqlExecLog.oldValue);
|
||||
|
||||
let schema = '';
|
||||
let dbArr = sqlExecLog.db.split('/');
|
||||
if (dbArr.length == 2) {
|
||||
schema = dbArr[1] + '.';
|
||||
}
|
||||
|
||||
const rollbackSqls = [];
|
||||
if (sqlExecLog.type == DbSqlExecTypeEnum.Update.value) {
|
||||
for (let ov of oldValue) {
|
||||
@@ -130,7 +136,7 @@ const onShowRollbackSql = async (sqlExecLog: any) => {
|
||||
}
|
||||
setItems.push(`${key} = ${wrapValue(ov[key])}`);
|
||||
}
|
||||
rollbackSqls.push(`UPDATE ${sqlExecLog.table} SET ${setItems.join(', ')} WHERE ${primaryKey} = ${wrapValue(ov[primaryKey])};`);
|
||||
rollbackSqls.push(`UPDATE ${schema}${sqlExecLog.table} SET ${setItems.join(', ')} WHERE ${primaryKey} = ${wrapValue(ov[primaryKey])};`);
|
||||
}
|
||||
} else if (sqlExecLog.type == DbSqlExecTypeEnum.Delete.value) {
|
||||
const columnNames = columns.map((c: any) => c.columnName);
|
||||
@@ -139,7 +145,7 @@ const onShowRollbackSql = async (sqlExecLog: any) => {
|
||||
for (let column of columnNames) {
|
||||
values.push(wrapValue(ov[column]));
|
||||
}
|
||||
rollbackSqls.push(`INSERT INTO ${sqlExecLog.table} (${columnNames.join(', ')}) VALUES (${values.join(', ')});`);
|
||||
rollbackSqls.push(`INSERT INTO ${schema}${sqlExecLog.table} (${columnNames.join(', ')}) VALUES (${values.join(', ')});`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +154,7 @@ const onShowRollbackSql = async (sqlExecLog: any) => {
|
||||
};
|
||||
|
||||
const getPrimaryKey = (columns: any) => {
|
||||
const col = columns.find((c: any) => c.columnKey == 'PRI');
|
||||
const col = columns.find((c: any) => c.isPrimaryKey);
|
||||
if (col) {
|
||||
return col.columnName;
|
||||
}
|
||||
|
||||
@@ -9,13 +9,22 @@
|
||||
</el-form-item>
|
||||
<el-form-item prop="type" label="类型" required>
|
||||
<el-select @change="changeDbType" style="width: 100%" v-model="form.type" placeholder="请选择数据库类型">
|
||||
<el-option v-for="dt in dbTypes" :key="dt.type" :value="dt.type" :label="dt.label">
|
||||
<SvgIcon :name="getDbDialect(dt.type).getInfo().icon" :size="18" />
|
||||
{{ dt.label }}
|
||||
<el-option
|
||||
v-for="(dbTypeAndDialect, key) in getDbDialectMap()"
|
||||
:key="key"
|
||||
:value="dbTypeAndDialect[0]"
|
||||
:label="dbTypeAndDialect[1].getInfo().name"
|
||||
>
|
||||
<SvgIcon :name="dbTypeAndDialect[1].getInfo().icon" :size="20" />
|
||||
{{ dbTypeAndDialect[1].getInfo().name }}
|
||||
</el-option>
|
||||
|
||||
<template #prefix>
|
||||
<SvgIcon :name="getDbDialect(form.type).getInfo().icon" :size="20" />
|
||||
</template>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item prop="host" label="host" required>
|
||||
<el-form-item v-if="form.type !== DbType.sqlite" prop="host" label="host" required>
|
||||
<el-col :span="18">
|
||||
<el-input :disabled="form.id !== undefined" v-model.trim="form.host" placeholder="请输入主机ip" auto-complete="off"></el-input>
|
||||
</el-col>
|
||||
@@ -24,13 +33,18 @@
|
||||
<el-input type="number" v-model.number="form.port" placeholder="端口"></el-input>
|
||||
</el-col>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="form.type === DbType.sqlite" prop="host" label="sqlite地址">
|
||||
<el-input v-model.trim="form.host" placeholder="请输入sqlite文件在服务器的绝对地址"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="form.type === DbType.oracle" prop="sid" label="SID">
|
||||
<el-input v-model.trim="form.sid" placeholder="请输入服务id"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="username" label="用户名" required>
|
||||
<el-form-item v-if="form.type !== DbType.sqlite" prop="username" label="用户名" required>
|
||||
<el-input v-model.trim="form.username" placeholder="请输入用户名"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password" label="密码">
|
||||
<el-form-item v-if="form.type !== DbType.sqlite" prop="password" label="密码">
|
||||
<el-input type="password" show-password v-model.trim="form.password" placeholder="请输入密码" autocomplete="new-password">
|
||||
<template v-if="form.id && form.id != 0" #suffix>
|
||||
<el-popover @hide="pwd = ''" placement="right" title="原密码" :width="200" trigger="click" :content="pwd">
|
||||
@@ -90,7 +104,7 @@ import { ElMessage } from 'element-plus';
|
||||
import { notBlank } from '@/common/assert';
|
||||
import { RsaEncrypt } from '@/common/rsa';
|
||||
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
|
||||
import { DbType, getDbDialect } from './dialect';
|
||||
import { DbType, getDbDialect, getDbDialectMap } from './dialect';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -148,35 +162,12 @@ const rules = {
|
||||
|
||||
const dbForm: any = ref(null);
|
||||
|
||||
const dbTypes = [
|
||||
{
|
||||
type: 'mysql',
|
||||
label: 'mysql',
|
||||
},
|
||||
{
|
||||
type: 'mariadb',
|
||||
label: 'mariadb',
|
||||
},
|
||||
{
|
||||
type: 'postgres',
|
||||
label: 'postgres',
|
||||
},
|
||||
{
|
||||
type: 'dm',
|
||||
label: '达梦',
|
||||
},
|
||||
{
|
||||
type: 'oracle',
|
||||
label: 'oracle',
|
||||
},
|
||||
];
|
||||
|
||||
const state = reactive({
|
||||
dialogVisible: false,
|
||||
tabActiveName: 'basic',
|
||||
form: {
|
||||
id: null,
|
||||
type: null,
|
||||
type: '',
|
||||
name: null,
|
||||
host: '',
|
||||
port: null,
|
||||
@@ -187,17 +178,17 @@ const state = reactive({
|
||||
remark: '',
|
||||
sshTunnelMachineId: null as any,
|
||||
},
|
||||
subimtForm: {},
|
||||
submitForm: {},
|
||||
// 原密码
|
||||
pwd: '',
|
||||
// 原用户名
|
||||
oldUserName: null,
|
||||
});
|
||||
|
||||
const { dialogVisible, tabActiveName, form, subimtForm, pwd } = toRefs(state);
|
||||
const { dialogVisible, tabActiveName, form, submitForm, pwd } = toRefs(state);
|
||||
|
||||
const { isFetching: saveBtnLoading, execute: saveInstanceExec } = dbApi.saveInstance.useApi(subimtForm);
|
||||
const { isFetching: testConnBtnLoading, execute: testConnExec } = dbApi.testConn.useApi(subimtForm);
|
||||
const { isFetching: saveBtnLoading, execute: saveInstanceExec } = dbApi.saveInstance.useApi(submitForm);
|
||||
const { isFetching: testConnBtnLoading, execute: testConnExec } = dbApi.testConn.useApi(submitForm);
|
||||
|
||||
watch(props, (newValue: any) => {
|
||||
state.dialogVisible = newValue.visible;
|
||||
@@ -209,7 +200,7 @@ watch(props, (newValue: any) => {
|
||||
state.form = { ...newValue.data };
|
||||
state.oldUserName = state.form.username;
|
||||
} else {
|
||||
state.form = { port: null } as any;
|
||||
state.form = { port: null, type: DbType.mysql } as any;
|
||||
state.oldUserName = null;
|
||||
}
|
||||
});
|
||||
@@ -240,17 +231,19 @@ const testConn = async () => {
|
||||
return false;
|
||||
}
|
||||
|
||||
state.subimtForm = await getReqForm();
|
||||
state.submitForm = await getReqForm();
|
||||
await testConnExec();
|
||||
ElMessage.success('连接成功');
|
||||
});
|
||||
};
|
||||
|
||||
const btnOk = async () => {
|
||||
if (!state.form.id) {
|
||||
notBlank(state.form.password, '新增操作,密码不可为空');
|
||||
} else if (state.form.username != state.oldUserName) {
|
||||
notBlank(state.form.password, '已修改用户名,请输入密码');
|
||||
if (state.form.type !== DbType.sqlite) {
|
||||
if (!state.form.id) {
|
||||
notBlank(state.form.password, '新增操作,密码不可为空');
|
||||
} else if (state.form.username != state.oldUserName) {
|
||||
notBlank(state.form.password, '已修改用户名,请输入密码');
|
||||
}
|
||||
}
|
||||
|
||||
dbForm.value.validate(async (valid: boolean) => {
|
||||
@@ -259,7 +252,7 @@ const btnOk = async () => {
|
||||
return false;
|
||||
}
|
||||
|
||||
state.subimtForm = await getReqForm();
|
||||
state.submitForm = await getReqForm();
|
||||
await saveInstanceExec();
|
||||
ElMessage.success('保存成功');
|
||||
emit('val-change', state.form);
|
||||
|
||||
@@ -151,12 +151,22 @@
|
||||
</div>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
<db-table-op
|
||||
:title="tableCreateDialog.title"
|
||||
:active-name="tableCreateDialog.activeName"
|
||||
:dbId="tableCreateDialog.dbId"
|
||||
:db="tableCreateDialog.db"
|
||||
:dbType="tableCreateDialog.dbType"
|
||||
:data="tableCreateDialog.data"
|
||||
v-model:visible="tableCreateDialog.visible"
|
||||
@submit-sql="onSubmitEditTableSql"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, onBeforeUnmount, onMounted, reactive, ref, toRefs } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { defineAsyncComponent, h, onBeforeUnmount, onMounted, reactive, ref, toRefs } from 'vue';
|
||||
import { ElCheckbox, ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { formatByteSize } from '@/common/utils/format';
|
||||
import { DbInst, registerDbCompletionItemProvider, TabInfo, TabType } from './db';
|
||||
import { NodeType, TagTreeNode } from '../component/tag';
|
||||
@@ -165,12 +175,13 @@ import { dbApi } from './api';
|
||||
import { dispposeCompletionItemProvider } from '@/components/monaco/completionItemProvider';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import { ContextmenuItem } from '@/components/contextmenu';
|
||||
import { DbType, getDbDialect } from './dialect/index';
|
||||
import { getDbDialect, schemaDbTypes } from './dialect/index';
|
||||
import { sleep } from '@/common/utils/loading';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import { Pane, Splitpanes } from 'splitpanes';
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
|
||||
const DbTableOp = defineAsyncComponent(() => import('./component/table/DbTableOp.vue'));
|
||||
const DbSqlEditor = defineAsyncComponent(() => import('./component/sqleditor/DbSqlEditor.vue'));
|
||||
const DbTableDataOp = defineAsyncComponent(() => import('./component/table/DbTableDataOp.vue'));
|
||||
const DbTablesOp = defineAsyncComponent(() => import('./component/table/DbTablesOp.vue'));
|
||||
@@ -218,21 +229,25 @@ const nodeClickChangeDb = (nodeData: TagTreeNode) => {
|
||||
}
|
||||
};
|
||||
|
||||
// tagpath 节点类型
|
||||
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const dbInfoRes = await dbApi.dbs.request({ tagPath: parentNode.key });
|
||||
const dbInfos = dbInfoRes.list;
|
||||
if (!dbInfos) {
|
||||
return [];
|
||||
}
|
||||
const ContextmenuItemRefresh = new ContextmenuItem('refresh', '刷新').withIcon('RefreshRight').withOnClick((data: any) => reloadNode(data.key));
|
||||
|
||||
// 防止过快加载会出现一闪而过,对眼睛不好
|
||||
await sleep(100);
|
||||
return dbInfos?.map((x: any) => {
|
||||
x.tagPath = parentNode.key;
|
||||
return new TagTreeNode(`${parentNode.key}.${x.id}`, x.name, NodeTypeDbInst).withParams(x);
|
||||
});
|
||||
});
|
||||
// tagpath 节点类型
|
||||
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath)
|
||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const dbInfoRes = await dbApi.dbs.request({ tagPath: parentNode.key });
|
||||
const dbInfos = dbInfoRes.list;
|
||||
if (!dbInfos) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 防止过快加载会出现一闪而过,对眼睛不好
|
||||
await sleep(100);
|
||||
return dbInfos?.map((x: any) => {
|
||||
x.tagPath = parentNode.key;
|
||||
return new TagTreeNode(`${parentNode.key}.${x.id}`, x.name, NodeTypeDbInst).withParams(x);
|
||||
});
|
||||
})
|
||||
.withContextMenuItems([ContextmenuItemRefresh]);
|
||||
|
||||
// 数据库实例节点类型
|
||||
const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((parentNode: TagTreeNode) => {
|
||||
@@ -255,12 +270,12 @@ const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((p
|
||||
|
||||
// 数据库节点
|
||||
const NodeTypeDb = new NodeType(SqlExecNodeType.Db)
|
||||
.withContextMenuItems([new ContextmenuItem('reloadTables', '刷新').withIcon('RefreshRight').withOnClick((data: any) => reloadNode(data.key))])
|
||||
.withContextMenuItems([ContextmenuItemRefresh])
|
||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
params.parentKey = parentNode.key;
|
||||
// pg类数据库会多一层schema
|
||||
if (params.type == DbType.postgresql || params.type === DbType.dm || params.type === DbType.oracle) {
|
||||
const params = parentNode.params;
|
||||
if (schemaDbTypes.includes(params.type)) {
|
||||
const { id, db } = params;
|
||||
const schemaNames = await dbApi.pgSchemas.request({ id, db });
|
||||
return schemaNames.map((sn: any) => {
|
||||
@@ -269,33 +284,37 @@ const NodeTypeDb = new NodeType(SqlExecNodeType.Db)
|
||||
nParams.schema = sn;
|
||||
nParams.db = nParams.db + '/' + sn;
|
||||
nParams.dbs = schemaNames;
|
||||
return new TagTreeNode(`${params.id}.${params.db}.schema.${sn}`, sn, NodeTypePostgresScheam).withParams(nParams).withIcon(SchemaIcon);
|
||||
return new TagTreeNode(`${params.id}.${params.db}.schema.${sn}`, sn, NodeTypePostgresSchema).withParams(nParams).withIcon(SchemaIcon);
|
||||
});
|
||||
}
|
||||
return [
|
||||
new TagTreeNode(`${params.id}.${params.db}.table-menu`, '表', NodeTypeTableMenu).withParams(params).withIcon(TableIcon),
|
||||
new TagTreeNode(getSqlMenuNodeKey(params.id, params.db), 'SQL', NodeTypeSqlMenu).withParams(params).withIcon(SqlIcon),
|
||||
];
|
||||
return NodeTypeTables(params);
|
||||
})
|
||||
.withNodeClickFunc(nodeClickChangeDb);
|
||||
|
||||
const NodeTypeTables = (params: any) => {
|
||||
let tableKey = `${params.id}.${params.db}.table-menu`;
|
||||
let sqlKey = getSqlMenuNodeKey(params.id, params.db);
|
||||
return [
|
||||
new TagTreeNode(`${params.id}.${params.db}.table-menu`, '表', NodeTypeTableMenu).withParams({ ...params, key: tableKey }).withIcon(TableIcon),
|
||||
new TagTreeNode(sqlKey, 'SQL', NodeTypeSqlMenu).withParams({ ...params, key: sqlKey }).withIcon(SqlIcon),
|
||||
];
|
||||
};
|
||||
|
||||
// postgres schema模式
|
||||
const NodeTypePostgresScheam = new NodeType(SqlExecNodeType.PgSchema)
|
||||
.withContextMenuItems([new ContextmenuItem('reloadTables', '刷新').withIcon('RefreshRight').withOnClick((data: any) => reloadNode(data.key))])
|
||||
const NodeTypePostgresSchema = new NodeType(SqlExecNodeType.PgSchema)
|
||||
.withContextMenuItems([ContextmenuItemRefresh])
|
||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
return [
|
||||
new TagTreeNode(`${params.id}.${params.db}.table-menu`, '表', NodeTypeTableMenu).withParams(params).withIcon(TableIcon),
|
||||
new TagTreeNode(getSqlMenuNodeKey(params.id, params.db), 'SQL', NodeTypeSqlMenu).withParams(params).withIcon(SqlIcon),
|
||||
];
|
||||
params.parentKey = parentNode.key;
|
||||
return NodeTypeTables(params);
|
||||
})
|
||||
.withNodeClickFunc(nodeClickChangeDb);
|
||||
|
||||
// 数据库表菜单节点
|
||||
const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
|
||||
.withContextMenuItems([
|
||||
new ContextmenuItem('reloadTables', '刷新').withIcon('RefreshRight').withOnClick((data: any) => reloadNode(data.key)),
|
||||
|
||||
ContextmenuItemRefresh,
|
||||
new ContextmenuItem('createTable', '创建表').withIcon('Plus').withOnClick((data: any) => onEditTable(data)),
|
||||
new ContextmenuItem('tablesOp', '表操作').withIcon('Setting').withOnClick((data: any) => {
|
||||
const params = data.params;
|
||||
addTablesOpTab({ id: params.id, db: params.db, type: params.type, nodeKey: data.key });
|
||||
@@ -303,27 +322,32 @@ const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
|
||||
])
|
||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
let { id, db } = params;
|
||||
let { id, db, type } = params;
|
||||
// 获取当前库的所有表信息
|
||||
let tables = await DbInst.getInst(id).loadTables(db, state.reloadStatus);
|
||||
state.reloadStatus = false;
|
||||
let dbTableSize = 0;
|
||||
const tablesNode = tables.map((x: any) => {
|
||||
dbTableSize += x.dataLength + x.indexLength;
|
||||
return new TagTreeNode(`${id}.${db}.${x.tableName}`, x.tableName, NodeTypeTable)
|
||||
const tableSize = x.dataLength + x.indexLength;
|
||||
dbTableSize += tableSize;
|
||||
const key = `${id}.${db}.${x.tableName}`;
|
||||
return new TagTreeNode(key, x.tableName, NodeTypeTable)
|
||||
.withIsLeaf(true)
|
||||
.withParams({
|
||||
id,
|
||||
db,
|
||||
type,
|
||||
key: key,
|
||||
parentKey: parentNode.key,
|
||||
tableName: x.tableName,
|
||||
tableComment: x.tableComment,
|
||||
size: formatByteSize(x.dataLength + x.indexLength, 1),
|
||||
size: tableSize == 0 ? '' : formatByteSize(tableSize, 1),
|
||||
})
|
||||
.withIcon(TableIcon)
|
||||
.withLabelRemark(`${x.tableName} ${x.tableComment ? '| ' + x.tableComment : ''}`);
|
||||
});
|
||||
// 设置父节点参数的表大小
|
||||
parentNode.params.dbTableSize = formatByteSize(dbTableSize);
|
||||
parentNode.params.dbTableSize = dbTableSize == 0 ? '' : formatByteSize(dbTableSize);
|
||||
return tablesNode;
|
||||
})
|
||||
.withNodeClickFunc(nodeClickChangeDb);
|
||||
@@ -340,22 +364,23 @@ const NodeTypeSqlMenu = new NodeType(SqlExecNodeType.SqlMenu)
|
||||
return sqls.map((x: any) => {
|
||||
return new TagTreeNode(`${id}.${db}.${x.name}`, x.name, NodeTypeSql)
|
||||
.withIsLeaf(true)
|
||||
.withParams({
|
||||
id,
|
||||
db,
|
||||
dbs,
|
||||
sqlName: x.name,
|
||||
})
|
||||
.withParams({ id, db, dbs, sqlName: x.name })
|
||||
.withIcon(SqlIcon);
|
||||
});
|
||||
})
|
||||
.withNodeClickFunc(nodeClickChangeDb);
|
||||
|
||||
// 表节点类型
|
||||
const NodeTypeTable = new NodeType(SqlExecNodeType.Table).withNodeClickFunc((nodeData: TagTreeNode) => {
|
||||
const params = nodeData.params;
|
||||
loadTableData({ id: params.id, nodeKey: nodeData.key }, params.db, params.tableName);
|
||||
});
|
||||
const NodeTypeTable = new NodeType(SqlExecNodeType.Table)
|
||||
.withContextMenuItems([
|
||||
new ContextmenuItem('copyTable', '复制表').withIcon('copyDocument').withOnClick((data: any) => onCopyTable(data)),
|
||||
new ContextmenuItem('editTable', '编辑表').withIcon('edit').withOnClick((data: any) => onEditTable(data)),
|
||||
new ContextmenuItem('delTable', '删除表').withIcon('Delete').withOnClick((data: any) => onDeleteTable(data)),
|
||||
])
|
||||
.withNodeClickFunc((nodeData: TagTreeNode) => {
|
||||
const params = nodeData.params;
|
||||
loadTableData({ id: params.id, nodeKey: nodeData.key }, params.db, params.tableName);
|
||||
});
|
||||
|
||||
// sql模板节点类型
|
||||
const NodeTypeSql = new NodeType(SqlExecNodeType.Sql)
|
||||
@@ -385,9 +410,19 @@ const state = reactive({
|
||||
loading: true,
|
||||
version: '',
|
||||
},
|
||||
tableCreateDialog: {
|
||||
visible: false,
|
||||
title: '',
|
||||
activeName: '',
|
||||
dbId: 0,
|
||||
db: '',
|
||||
dbType: '',
|
||||
data: {},
|
||||
parentKey: '',
|
||||
},
|
||||
});
|
||||
|
||||
const { nowDbInst } = toRefs(state);
|
||||
const { nowDbInst, tableCreateDialog } = toRefs(state);
|
||||
|
||||
const serverInfoReqParam = ref({
|
||||
instanceId: 0,
|
||||
@@ -408,7 +443,7 @@ onBeforeUnmount(() => {
|
||||
* 设置editor高度和数据表高度
|
||||
*/
|
||||
const setHeight = () => {
|
||||
state.dataTabsTableHeight = window.innerHeight - 270 + 'px';
|
||||
state.dataTabsTableHeight = window.innerHeight - 253 + 'px';
|
||||
state.tablesOpHeight = window.innerHeight - 225 + 'px';
|
||||
};
|
||||
|
||||
@@ -603,6 +638,85 @@ const reloadNode = (nodeKey: string) => {
|
||||
tagTreeRef.value.reloadNode(nodeKey);
|
||||
};
|
||||
|
||||
const onEditTable = async (data: any) => {
|
||||
let { db, id, tableName, tableComment, type, parentKey, key } = data.params;
|
||||
// data.label就是表名
|
||||
if (tableName) {
|
||||
state.tableCreateDialog.title = '修改表';
|
||||
let indexs = await dbApi.tableIndex.request({ id, db, tableName });
|
||||
let columns = await dbApi.columnMetadata.request({ id, db, tableName });
|
||||
let row = { tableName, tableComment };
|
||||
state.tableCreateDialog.data = { edit: true, row, indexs, columns };
|
||||
state.tableCreateDialog.parentKey = parentKey;
|
||||
} else {
|
||||
state.tableCreateDialog.title = '创建表';
|
||||
state.tableCreateDialog.data = { edit: false, row: {} };
|
||||
state.tableCreateDialog.parentKey = key;
|
||||
}
|
||||
|
||||
state.tableCreateDialog.visible = true;
|
||||
state.tableCreateDialog.activeName = '1';
|
||||
state.tableCreateDialog.dbId = id;
|
||||
state.tableCreateDialog.db = db;
|
||||
state.tableCreateDialog.dbType = type;
|
||||
};
|
||||
|
||||
const onDeleteTable = async (data: any) => {
|
||||
let { db, id, tableName, parentKey } = data.params;
|
||||
await ElMessageBox.confirm(`此操作是永久性且无法撤销,确定删除【${tableName}】? `, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
// 执行sql
|
||||
dbApi.sqlExec.request({ id, db, sql: `drop table ${tableName}` }).then(() => {
|
||||
ElMessage.success('删除成功');
|
||||
setTimeout(() => {
|
||||
parentKey && reloadNode(parentKey);
|
||||
}, 1000);
|
||||
});
|
||||
};
|
||||
|
||||
const onCopyTable = async (data: any) => {
|
||||
let { db, id, tableName, parentKey } = data.params;
|
||||
|
||||
let checked = ref(false);
|
||||
|
||||
// 弹出确认框,并选择是否复制数据
|
||||
await ElMessageBox({
|
||||
title: `复制表【${tableName}】`,
|
||||
type: 'warning',
|
||||
// icon: markRaw(Delete),
|
||||
message: () =>
|
||||
h(ElCheckbox, {
|
||||
label: '是否复制数据?',
|
||||
modelValue: checked.value,
|
||||
'onUpdate:modelValue': (val: boolean | string | number) => {
|
||||
if (typeof val === 'boolean') {
|
||||
checked.value = val;
|
||||
}
|
||||
},
|
||||
}),
|
||||
callback: (action: string) => {
|
||||
if (action === 'confirm') {
|
||||
// 执行sql
|
||||
dbApi.copyTable.request({ id, db, tableName, copyData: checked.value }).then(() => {
|
||||
ElMessage.success('复制成功');
|
||||
setTimeout(() => {
|
||||
parentKey && reloadNode(parentKey);
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmitEditTableSql = () => {
|
||||
state.tableCreateDialog.visible = false;
|
||||
state.tableCreateDialog.data = { edit: false, row: {} };
|
||||
reloadNode(state.tableCreateDialog.parentKey);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取当前操作的数据库信息
|
||||
*/
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
v-model:db-id="form.srcDbId"
|
||||
v-model:db-name="form.srcDbName"
|
||||
v-model:tag-path="form.srcTagPath"
|
||||
v-model:db-type="form.srcDbType"
|
||||
@select-db="onSelectSrcDb"
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -181,7 +182,7 @@ import { ElMessage } from 'element-plus';
|
||||
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||
import { DbInst, registerDbCompletionItemProvider } from '@/views/ops/db/db';
|
||||
import { getDbDialect } from '@/views/ops/db/dialect';
|
||||
import {DbType, getDbDialect} from '@/views/ops/db/dialect'
|
||||
import CrontabInput from '@/components/crontab/CrontabInput.vue';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -227,6 +228,7 @@ type FormData = {
|
||||
taskCron: string;
|
||||
srcDbId?: number;
|
||||
srcDbName?: string;
|
||||
srcDbType?: string;
|
||||
srcTagPath?: string;
|
||||
targetDbId?: number;
|
||||
targetDbName?: string;
|
||||
@@ -245,7 +247,7 @@ const basicFormData = {
|
||||
targetDbId: -1,
|
||||
dataSql: 'select * from',
|
||||
pageSize: 1000,
|
||||
updField: 'id',
|
||||
updField: '',
|
||||
updFieldVal: '0',
|
||||
fieldMap: [{ src: 'a', target: 'b' }],
|
||||
status: 1,
|
||||
@@ -302,6 +304,7 @@ watch(dialogVisible, async (newValue: boolean) => {
|
||||
// 初始化实例
|
||||
db.databases = db.database?.split(' ').sort() || [];
|
||||
state.srcDbInst = DbInst.getOrNewInst(db);
|
||||
state.form.srcDbType = state.srcDbInst.type
|
||||
}
|
||||
|
||||
// 初始化target数据源
|
||||
@@ -396,8 +399,8 @@ const handleGetSrcFields = async () => {
|
||||
}
|
||||
|
||||
// 判断sql是否是查询语句
|
||||
if (!/^select/i.test(state.form.dataSql!)) {
|
||||
let msg = 'sql语句错误,请输入查询语句';
|
||||
if (!/^select/i.test(state.form.dataSql.trim()!)) {
|
||||
let msg = 'sql语句错误,请输入select语句';
|
||||
ElMessage.warning(msg);
|
||||
return;
|
||||
}
|
||||
@@ -410,10 +413,16 @@ const handleGetSrcFields = async () => {
|
||||
}
|
||||
|
||||
// 执行sql
|
||||
// oracle的分页关键字不一样
|
||||
let limit = ' limit 1'
|
||||
if(state.form.srcDbType === DbType.oracle){
|
||||
limit = ' where rownum <= 1'
|
||||
}
|
||||
|
||||
const res = await dbApi.sqlExec.request({
|
||||
id: state.form.srcDbId,
|
||||
db: state.form.srcDbName,
|
||||
sql: state.form.dataSql.trim() + ' limit 1',
|
||||
sql: `select * from (${state.form.dataSql}) t ${limit}`
|
||||
});
|
||||
|
||||
if (!res.columns) {
|
||||
|
||||
@@ -11,6 +11,7 @@ export const dbApi = {
|
||||
tableInfos: Api.newGet('/dbs/{id}/t-infos'),
|
||||
tableIndex: Api.newGet('/dbs/{id}/t-index'),
|
||||
tableDdl: Api.newGet('/dbs/{id}/t-create-ddl'),
|
||||
copyTable: Api.newPost('/dbs/{id}/copy-table'),
|
||||
columnMetadata: Api.newGet('/dbs/{id}/c-metadata'),
|
||||
pgSchemas: Api.newGet('/dbs/{id}/pg/schemas'),
|
||||
// 获取表即列提示
|
||||
@@ -48,16 +49,20 @@ export const dbApi = {
|
||||
// 获取数据库备份列表
|
||||
getDbBackups: Api.newGet('/dbs/{dbId}/backups'),
|
||||
createDbBackup: Api.newPost('/dbs/{dbId}/backups'),
|
||||
deleteDbBackup: Api.newDelete('/dbs/{dbId}/backups/{backupId}'),
|
||||
getDbNamesWithoutBackup: Api.newGet('/dbs/{dbId}/db-names-without-backup'),
|
||||
enableDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/enable'),
|
||||
disableDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/disable'),
|
||||
startDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/start'),
|
||||
saveDbBackup: Api.newPut('/dbs/{dbId}/backups/{id}'),
|
||||
getDbBackupHistories: Api.newGet('/dbs/{dbId}/backup-histories'),
|
||||
restoreDbBackupHistory: Api.newPost('/dbs/{dbId}/backup-histories/{backupHistoryId}/restore'),
|
||||
deleteDbBackupHistory: Api.newDelete('/dbs/{dbId}/backup-histories/{backupHistoryId}'),
|
||||
|
||||
// 获取数据库备份列表
|
||||
// 获取数据库恢复列表
|
||||
getDbRestores: Api.newGet('/dbs/{dbId}/restores'),
|
||||
createDbRestore: Api.newPost('/dbs/{dbId}/restores'),
|
||||
deleteDbRestore: Api.newDelete('/dbs/{dbId}/restores/{restoreId}'),
|
||||
getDbNamesWithoutRestore: Api.newGet('/dbs/{dbId}/db-names-without-restore'),
|
||||
enableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/enable'),
|
||||
disableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/disable'),
|
||||
|
||||
@@ -19,7 +19,7 @@ import { NodeType, TagTreeNode } from '@/views/ops/component/tag';
|
||||
import { dbApi } from '@/views/ops/db/api';
|
||||
import { sleep } from '@/common/utils/loading';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import { DbType, getDbDialect } from '@/views/ops/db/dialect';
|
||||
import { getDbDialect, noSchemaTypes } from '@/views/ops/db/dialect';
|
||||
import TagTreeResourceSelect from '../../component/TagTreeResourceSelect.vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
@@ -33,9 +33,12 @@ const props = defineProps({
|
||||
tagPath: {
|
||||
type: String,
|
||||
},
|
||||
dbType: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(['update:dbName', 'update:tagPath', 'update:dbId', 'selectDb']);
|
||||
const emits = defineEmits(['update:dbName', 'update:tagPath', 'update:dbId', 'update:dbType', 'selectDb']);
|
||||
|
||||
/**
|
||||
* 树节点类型
|
||||
@@ -87,8 +90,8 @@ const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(asyn
|
||||
});
|
||||
|
||||
/** mysql类型的数据库,没有schema层 */
|
||||
const mysqlType = (type: string) => {
|
||||
return type === DbType.mysql;
|
||||
const noSchemaType = (type: string) => {
|
||||
return noSchemaTypes.includes(type);
|
||||
};
|
||||
|
||||
// 数据库实例节点类型
|
||||
@@ -96,7 +99,7 @@ const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((p
|
||||
const params = parentNode.params;
|
||||
const dbs = params.database.split(' ')?.sort();
|
||||
let fn: NodeType;
|
||||
if (mysqlType(params.type)) {
|
||||
if (noSchemaType(params.type)) {
|
||||
fn = MysqlNodeTypes;
|
||||
} else {
|
||||
fn = PgNodeTypes;
|
||||
@@ -114,7 +117,7 @@ const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((p
|
||||
db: x,
|
||||
})
|
||||
.withIcon(DbIcon);
|
||||
if (mysqlType(params.type)) {
|
||||
if (noSchemaType(params.type)) {
|
||||
tagTreeNode.isLeaf = true;
|
||||
}
|
||||
return tagTreeNode;
|
||||
@@ -150,6 +153,7 @@ const changeNode = (nodeData: TagTreeNode) => {
|
||||
emits('update:dbName', params.db);
|
||||
emits('update:dbId', params.id);
|
||||
emits('update:tagPath', params.tagPath);
|
||||
emits('update:dbType', params.type);
|
||||
emits('selectDb', params);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -128,12 +128,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { h, nextTick, onMounted, reactive, toRefs, ref, unref } from 'vue';
|
||||
import { h, nextTick, onMounted, reactive, ref, toRefs, unref } from 'vue';
|
||||
import { getToken } from '@/common/utils/storage';
|
||||
import { notBlank } from '@/common/assert';
|
||||
import { format as sqlFormatter } from 'sql-formatter';
|
||||
import config from '@/common/config';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus';
|
||||
|
||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
import { editor } from 'monaco-editor';
|
||||
@@ -146,11 +146,10 @@ import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||
import { joinClientParams } from '@/common/request';
|
||||
import { buildProgressProps } from '@/components/progress-notify/progress-notify';
|
||||
import ProgressNotify from '@/components/progress-notify/progress-notify.vue';
|
||||
import { ElNotification } from 'element-plus';
|
||||
import syssocket from '@/common/syssocket';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import { getDbDialect } from '../../dialect';
|
||||
import { Splitpanes, Pane } from 'splitpanes';
|
||||
import { Pane, Splitpanes } from 'splitpanes';
|
||||
|
||||
const emits = defineEmits(['saveSqlSuccess']);
|
||||
|
||||
@@ -357,6 +356,7 @@ const onRunSql = async (newTab = false) => {
|
||||
const colAndData: any = data.value;
|
||||
if (!colAndData.res || colAndData.res.length === 0) {
|
||||
ElMessage.warning('未查询到结果集');
|
||||
return;
|
||||
}
|
||||
|
||||
// 要实时响应,故需要用索引改变数据才生效
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<el-input
|
||||
v-if="dataType == DataType.String"
|
||||
:ref="(el: any) => focus && el?.focus()"
|
||||
:disabled="disabled"
|
||||
@blur="handleBlur"
|
||||
:class="`w100 mb4 ${showEditorIcon ? 'string-input-container-show-icon' : ''}`"
|
||||
input-style="text-align: center; height: 26px;"
|
||||
@@ -16,6 +17,7 @@
|
||||
<el-input
|
||||
v-else-if="dataType == DataType.Number"
|
||||
:ref="(el: any) => focus && el?.focus()"
|
||||
:disabled="disabled"
|
||||
@blur="handleBlur"
|
||||
class="w100 mb4"
|
||||
input-style="text-align: center; height: 26px;"
|
||||
@@ -28,6 +30,7 @@
|
||||
<el-date-picker
|
||||
v-else-if="dataType == DataType.Date"
|
||||
:ref="(el: any) => focus && el?.focus()"
|
||||
:disabled="disabled"
|
||||
@change="emit('blur')"
|
||||
@blur="handleBlur"
|
||||
class="edit-time-picker mb4"
|
||||
@@ -43,6 +46,7 @@
|
||||
<el-date-picker
|
||||
v-else-if="dataType == DataType.DateTime"
|
||||
:ref="(el: any) => focus && el?.focus()"
|
||||
:disabled="disabled"
|
||||
@change="handleBlur"
|
||||
@blur="handleBlur"
|
||||
class="edit-time-picker mb4"
|
||||
@@ -58,6 +62,7 @@
|
||||
<el-time-picker
|
||||
v-else-if="dataType == DataType.Time"
|
||||
:ref="(el: any) => focus && el?.focus()"
|
||||
:disabled="disabled"
|
||||
@change="handleBlur"
|
||||
@blur="handleBlur"
|
||||
class="edit-time-picker mb4"
|
||||
@@ -71,7 +76,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Ref, ref, computed } from 'vue';
|
||||
import { computed, ref, Ref } from 'vue';
|
||||
import { ElInput } from 'element-plus';
|
||||
import { DataType } from '../../dialect/index';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
@@ -83,11 +88,13 @@ export interface ColumnFormItemProps {
|
||||
focus?: boolean; // 是否获取焦点
|
||||
placeholder?: string;
|
||||
columnName?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<ColumnFormItemProps>(), {
|
||||
focus: false,
|
||||
dataType: DataType.String,
|
||||
disabled: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'blur']);
|
||||
|
||||
@@ -46,14 +46,6 @@
|
||||
<b :title="column.remark" class="el-text" style="cursor: pointer">
|
||||
{{ column.title }}
|
||||
</b>
|
||||
|
||||
<span>
|
||||
<SvgIcon
|
||||
color="var(--el-color-primary)"
|
||||
v-if="column.title == nowSortColumn?.columnName"
|
||||
:name="nowSortColumn?.order == 'asc' ? 'top' : 'bottom'"
|
||||
></SvgIcon>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 字段备注信息 -->
|
||||
@@ -71,6 +63,13 @@
|
||||
{{ column.title }}
|
||||
</b>
|
||||
</div>
|
||||
|
||||
<!-- 字段列右部分内容 -->
|
||||
<div class="column-right">
|
||||
<span v-if="column.title == nowSortColumn?.columnName">
|
||||
<SvgIcon color="var(--el-color-primary)" :name="nowSortColumn?.order == 'asc' ? 'top' : 'bottom'"></SvgIcon>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -715,9 +714,13 @@ const submitUpdateFields = async () => {
|
||||
const db = state.db;
|
||||
let res = '';
|
||||
const dbDialect = getDbDialect(dbInst.type);
|
||||
|
||||
let schema = '';
|
||||
let dbArr = db.split('/');
|
||||
if (dbArr.length == 2) {
|
||||
schema = dbInst.wrapName(dbArr[1]) + '.';
|
||||
}
|
||||
for (let updateRow of cellUpdateMap.values()) {
|
||||
let sql = `UPDATE ${dbInst.wrapName(state.table)} SET `;
|
||||
let sql = `UPDATE ${schema}${dbInst.wrapName(state.table)} SET `;
|
||||
const rowData = updateRow.rowData;
|
||||
// 主键列信息
|
||||
const primaryKey = await dbInst.loadTableColumn(db, state.table);
|
||||
@@ -868,9 +871,15 @@ defineExpose({
|
||||
color: var(--el-color-info-light-3);
|
||||
font-weight: bold;
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
top: -5px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.column-right {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 0;
|
||||
padding: 2px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -158,21 +158,52 @@
|
||||
@data-delete="onRefresh"
|
||||
></db-table-data>
|
||||
|
||||
<el-row type="flex" class="mt5" justify="center">
|
||||
<el-pagination
|
||||
small
|
||||
:total="count"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="pageChange()"
|
||||
layout="prev, pager, next, total, sizes, jumper"
|
||||
v-model:current-page="pageNum"
|
||||
v-model:page-size="pageSize"
|
||||
:page-sizes="pageSizes"
|
||||
></el-pagination>
|
||||
<el-row type="flex" class="mt5" :gutter="10" justify="space-between" style="user-select: none">
|
||||
<el-col :span="12">
|
||||
<el-text
|
||||
id="copyValue"
|
||||
style="color: var(--el-color-info-light-3)"
|
||||
class="is-truncated font12 mt5"
|
||||
@click="copyToClipboard(sql)"
|
||||
:title="sql"
|
||||
>{{ sql }}</el-text
|
||||
>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-row :gutter="10" justify="left">
|
||||
<el-link class="op-page" :underline="false" @click="pageNum = 1" :disabled="pageNum == 1" icon="DArrowLeft" title="首页" />
|
||||
<el-link class="op-page" :underline="false" @click="pageNum = --pageNum || 1" :disabled="pageNum == 1" icon="Back" title="上一页" />
|
||||
<div class="op-page">
|
||||
<el-input-number
|
||||
style="width: 50px"
|
||||
:controls="false"
|
||||
:min="1"
|
||||
v-model="state.setPageNum"
|
||||
size="small"
|
||||
@blur="handleSetPageNum"
|
||||
@keydown.enter="handleSetPageNum"
|
||||
/>
|
||||
</div>
|
||||
<el-link class="op-page" :underline="false" @click="++pageNum" :disabled="datas.length < pageSize" icon="Right" />
|
||||
<el-link class="op-page" :underline="false" @click="handleEndPage" :disabled="datas.length < pageSize" icon="DArrowRight" />
|
||||
<div style="width: 90px" class="op-page ml10">
|
||||
<el-select size="small" :default-first-option="true" v-model="pageSize" @change="handleSizeChange">
|
||||
<el-option
|
||||
style="font-size: 12px; height: 24px; line-height: 24px"
|
||||
v-for="(op, i) in pageSizes"
|
||||
:key="i"
|
||||
:label="op + '条/页'"
|
||||
:value="op"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<el-button @click="handleCount" :loading="state.counting" class="ml10" text bg size="small">
|
||||
{{ state.showTotal ? `${state.total} 条` : 'count' }}
|
||||
</el-button>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div style="font-size: 12px; padding: 0 10px; color: #606266">
|
||||
<span>{{ state.sql }}</span>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="conditionDialog.visible" :title="conditionDialog.title" width="420px">
|
||||
<el-row>
|
||||
@@ -211,13 +242,14 @@
|
||||
class="w100 mb5"
|
||||
:prop="column.columnName"
|
||||
:label="column.columnName"
|
||||
:required="column.nullable != 'YES' && column.columnKey != 'PRI'"
|
||||
:required="column.nullable != 'YES' && !column.isPrimaryKey && !column.isIdentity"
|
||||
>
|
||||
<ColumnFormItem
|
||||
v-model="addDataDialog.data[`${column.columnName}`]"
|
||||
:data-type="dbDialect.getDataType(column.columnType)"
|
||||
:placeholder="`${column.columnType} ${column.columnComment}`"
|
||||
:column-name="column.columnName"
|
||||
:disabled="column.isIdentity"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@@ -241,6 +273,7 @@ import { DbDialect, getDbDialect } from '@/views/ops/db/dialect';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import ColumnFormItem from './ColumnFormItem.vue';
|
||||
import { useEventListener, useStorage } from '@vueuse/core';
|
||||
import { copyToClipboard } from '@/common/utils/string';
|
||||
|
||||
const props = defineProps({
|
||||
dbId: {
|
||||
@@ -289,7 +322,10 @@ const state = reactive({
|
||||
defaultPageSize * 40,
|
||||
defaultPageSize * 80,
|
||||
],
|
||||
count: 0,
|
||||
setPageNum: 0,
|
||||
total: 0,
|
||||
showTotal: false,
|
||||
counting: false,
|
||||
selectionDatas: [] as any,
|
||||
condPopVisible: false,
|
||||
columnNameSearch: '',
|
||||
@@ -313,7 +349,7 @@ const state = reactive({
|
||||
dbDialect: {} as DbDialect,
|
||||
});
|
||||
|
||||
const { datas, condition, loading, columns, pageNum, pageSize, pageSizes, count, hasUpdatedFileds, conditionDialog, addDataDialog, dbDialect } = toRefs(state);
|
||||
const { datas, condition, loading, columns, pageNum, pageSize, pageSizes, sql, hasUpdatedFileds, conditionDialog, addDataDialog, dbDialect } = toRefs(state);
|
||||
|
||||
watch(
|
||||
() => props.tableHeight,
|
||||
@@ -346,18 +382,19 @@ const onRefresh = async () => {
|
||||
await selectData();
|
||||
};
|
||||
|
||||
/**
|
||||
* 数据tab修改页数
|
||||
*/
|
||||
const pageChange = async () => {
|
||||
await selectData();
|
||||
};
|
||||
watch(
|
||||
() => state.pageNum,
|
||||
async () => {
|
||||
await selectData();
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 单表数据信息查询数据
|
||||
*/
|
||||
const selectData = async () => {
|
||||
state.loading = true;
|
||||
state.setPageNum = state.pageNum;
|
||||
const dbInst = getNowDbInst();
|
||||
const db = props.dbName;
|
||||
const table = props.tableName;
|
||||
@@ -370,16 +407,10 @@ const selectData = async () => {
|
||||
state.columns = columns;
|
||||
}
|
||||
|
||||
const countRes = await dbInst.runSql(db, dbInst.getDefaultCountSql(table, state.condition));
|
||||
state.count = countRes.res[0].count || countRes.res[0].COUNT || 0;
|
||||
let sql = dbInst.getDefaultSelectSql(table, state.condition, state.orderBy, state.pageNum, state.pageSize);
|
||||
let sql = dbInst.getDefaultSelectSql(db, table, state.condition, state.orderBy, state.pageNum, state.pageSize);
|
||||
state.sql = sql;
|
||||
if (state.count > 0) {
|
||||
const colAndData: any = await dbInst.runSql(db, sql);
|
||||
state.datas = colAndData.res;
|
||||
} else {
|
||||
state.datas = [];
|
||||
}
|
||||
const colAndData: any = await dbInst.runSql(db, sql);
|
||||
state.datas = colAndData.res;
|
||||
} finally {
|
||||
state.loading = false;
|
||||
}
|
||||
@@ -391,6 +422,33 @@ const handleSizeChange = async (size: any) => {
|
||||
await selectData();
|
||||
};
|
||||
|
||||
const handleEndPage = async () => {
|
||||
await handleCount();
|
||||
state.pageNum = Math.ceil(state.total / state.pageSize);
|
||||
await selectData();
|
||||
};
|
||||
|
||||
const handleSetPageNum = async () => {
|
||||
state.pageNum = state.setPageNum;
|
||||
await selectData();
|
||||
};
|
||||
const handleCount = async () => {
|
||||
state.counting = true;
|
||||
|
||||
try {
|
||||
const db = props.dbName;
|
||||
const table = props.tableName;
|
||||
const dbInst = getNowDbInst();
|
||||
const countRes = await dbInst.runSql(db, dbInst.getDefaultCountSql(table, state.condition));
|
||||
state.total = parseInt(countRes.res[0].count || countRes.res[0].COUNT || 0);
|
||||
state.showTotal = true;
|
||||
} catch (e) {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
state.counting = false;
|
||||
};
|
||||
|
||||
// 完整的条件,每次选中后会重置条件框内容,故需要这个变量在获取建议时将文本框内容保存
|
||||
let completeCond = '';
|
||||
// 是否存在列建议
|
||||
@@ -566,7 +624,13 @@ const addRow = async () => {
|
||||
}
|
||||
let columnNames = Object.keys(obj).join(',');
|
||||
let values = Object.values(obj).join(',');
|
||||
let sql = `INSERT INTO ${dbInst.wrapName(props.tableName)} (${columnNames}) VALUES (${values});`;
|
||||
// 获取schema
|
||||
let schema = '';
|
||||
let arr = props.dbName?.split('/');
|
||||
if (arr && arr.length > 1) {
|
||||
schema = dbInst.wrapName(arr[1]) + '.';
|
||||
}
|
||||
let sql = `INSERT INTO ${schema}${dbInst.wrapName(props.tableName)} (${columnNames}) VALUES (${values});`;
|
||||
dbInst.promptExeSql(props.dbName, sql, null, () => {
|
||||
closeAddDataDialog();
|
||||
onRefresh();
|
||||
@@ -579,4 +643,8 @@ const addRow = async () => {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
<style lang="scss">
|
||||
.op-page {
|
||||
margin-left: 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" width="90%" :close-on-press-escape="false" :close-on-click-modal="false">
|
||||
<el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" width="70%" :close-on-press-escape="false" :close-on-click-modal="false">
|
||||
<el-form label-position="left" ref="formRef" :model="tableData" label-width="80px">
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
@@ -26,7 +26,7 @@
|
||||
:width="item.width"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-input v-if="item.prop === 'name'" size="small" v-model="scope.row.name"> </el-input>
|
||||
<el-input v-if="item.prop === 'name'" size="small" v-model="scope.row.name" />
|
||||
|
||||
<el-select v-else-if="item.prop === 'type'" filterable size="small" v-model="scope.row.type">
|
||||
<el-option
|
||||
@@ -42,35 +42,30 @@
|
||||
</el-option>
|
||||
</el-select>
|
||||
|
||||
<el-input v-else-if="item.prop === 'value'" size="small" v-model="scope.row.value"> </el-input>
|
||||
<el-input v-else-if="item.prop === 'value'" size="small" v-model="scope.row.value" />
|
||||
|
||||
<el-input v-else-if="item.prop === 'length'" size="small" v-model="scope.row.length"> </el-input>
|
||||
<el-input v-else-if="item.prop === 'length'" type="number" size="small" v-model.number="scope.row.length" />
|
||||
|
||||
<el-input v-else-if="item.prop === 'numScale'" size="small" v-model="scope.row.numScale"> </el-input>
|
||||
<el-input v-else-if="item.prop === 'numScale'" type="number" size="small" v-model.number="scope.row.numScale" />
|
||||
|
||||
<el-checkbox v-else-if="item.prop === 'notNull'" size="small" v-model="scope.row.notNull"> </el-checkbox>
|
||||
<el-checkbox v-else-if="item.prop === 'notNull'" size="small" v-model="scope.row.notNull" />
|
||||
|
||||
<el-checkbox v-else-if="item.prop === 'pri'" size="small" v-model="scope.row.pri"> </el-checkbox>
|
||||
<el-checkbox v-else-if="item.prop === 'pri'" size="small" v-model="scope.row.pri" />
|
||||
|
||||
<el-checkbox
|
||||
v-else-if="item.prop === 'auto_increment'"
|
||||
size="small"
|
||||
v-model="scope.row.auto_increment"
|
||||
:disabled="dbType === DbType.postgresql"
|
||||
>
|
||||
</el-checkbox>
|
||||
:disabled="disableEditIncr()"
|
||||
/>
|
||||
|
||||
<el-input v-else-if="item.prop === 'remark'" size="small" v-model="scope.row.remark"> </el-input>
|
||||
<el-input v-else-if="item.prop === 'remark'" size="small" v-model="scope.row.remark" />
|
||||
|
||||
<el-link
|
||||
v-else-if="item.prop === 'action'"
|
||||
type="danger"
|
||||
plain
|
||||
size="small"
|
||||
:underline="false"
|
||||
@click.prevent="deleteRow(scope.$index)"
|
||||
>删除</el-link
|
||||
>
|
||||
<el-popconfirm v-else-if="item.prop === 'action'" title="确定删除?" @confirm="deleteRow(scope.$index)">
|
||||
<template #reference>
|
||||
<el-link type="danger" plain size="small" :underline="false">删除</el-link>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -104,21 +99,15 @@
|
||||
<el-checkbox v-if="item.prop === 'unique'" size="small" v-model="scope.row.unique" @change="indexChanges(scope.row)">
|
||||
</el-checkbox>
|
||||
|
||||
<el-select v-if="item.prop === 'indexType'" disabled size="small" v-model="scope.row.indexType">
|
||||
<el-option v-for="typeValue in indexTypeList" :key="typeValue" :value="typeValue">{{ typeValue }}</el-option>
|
||||
</el-select>
|
||||
<el-input v-if="item.prop === 'indexType'" disabled size="small" v-model="scope.row.indexType" />
|
||||
|
||||
<el-input v-if="item.prop === 'indexComment'" size="small" v-model="scope.row.indexComment"> </el-input>
|
||||
|
||||
<el-link
|
||||
v-if="item.prop === 'action'"
|
||||
type="danger"
|
||||
plain
|
||||
size="small"
|
||||
:underline="false"
|
||||
@click.prevent="deleteIndex(scope.$index)"
|
||||
>删除</el-link
|
||||
>
|
||||
<el-popconfirm v-else-if="item.prop === 'action'" title="确定删除?" @confirm="deleteIndex(scope.$index)">
|
||||
<template #reference>
|
||||
<el-link type="danger" plain size="small" :underline="false">删除</el-link>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -130,6 +119,7 @@
|
||||
</el-tabs>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="cancel()">取消</el-button>
|
||||
<el-button :loading="btnloading" @click="submit()" type="primary">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@@ -166,7 +156,7 @@ const props = defineProps({
|
||||
//定义事件
|
||||
const emit = defineEmits(['update:visible', 'cancel', 'val-change', 'submit-sql']);
|
||||
|
||||
const dbDialect = getDbDialect(props.dbType);
|
||||
let dbDialect = getDbDialect(props.dbType);
|
||||
|
||||
type ColName = {
|
||||
prop: string;
|
||||
@@ -180,29 +170,33 @@ const state = reactive({
|
||||
btnloading: false,
|
||||
activeName: '1',
|
||||
columnTypeList: dbDialect.getInfo().columnTypes,
|
||||
indexTypeList: ['BTREE', 'NORMAL'], // mysql索引类型详解 http://c.biancheng.net/view/7897.html
|
||||
tableData: {
|
||||
fields: {
|
||||
colNames: [
|
||||
{
|
||||
prop: 'name',
|
||||
label: '字段名称',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
prop: 'type',
|
||||
label: '字段类型',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
prop: 'length',
|
||||
label: '长度',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
prop: 'numScale',
|
||||
label: '小数点',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
prop: 'value',
|
||||
label: '默认值',
|
||||
width: 120,
|
||||
},
|
||||
|
||||
{
|
||||
@@ -231,6 +225,7 @@ const state = reactive({
|
||||
},
|
||||
] as ColName[],
|
||||
res: [] as RowDefinition[],
|
||||
oldFields: [] as RowDefinition[],
|
||||
},
|
||||
indexs: {
|
||||
colNames: [
|
||||
@@ -261,17 +256,20 @@ const state = reactive({
|
||||
],
|
||||
columns: [{ name: '', remark: '' }],
|
||||
res: [] as IndexDefinition[],
|
||||
oldIndexs: [] as IndexDefinition[],
|
||||
},
|
||||
tableName: '',
|
||||
tableComment: '',
|
||||
height: 450,
|
||||
db: '',
|
||||
},
|
||||
});
|
||||
|
||||
const { dialogVisible, btnloading, activeName, indexTypeList, tableData } = toRefs(state);
|
||||
const { dialogVisible, btnloading, activeName, tableData } = toRefs(state);
|
||||
|
||||
watch(props, async (newValue) => {
|
||||
state.dialogVisible = newValue.visible;
|
||||
dbDialect = getDbDialect(newValue.dbType);
|
||||
});
|
||||
|
||||
const cancel = () => {
|
||||
@@ -359,7 +357,10 @@ const filterChangedData = (oldArr: object[], nowArr: object[], key: string): { d
|
||||
nowArr.forEach((a) => {
|
||||
let k = a[key];
|
||||
newMap[k] = a;
|
||||
if (!oldMap.hasOwnProperty(k)) {
|
||||
// 取oldName,因为修改了name,但是oldName不会变
|
||||
let oldName = a['oldName'];
|
||||
oldName && (newMap[oldName] = a);
|
||||
if (!oldMap.hasOwnProperty(k) && (!oldName || (oldName && !oldMap.hasOwnProperty(oldName)))) {
|
||||
// 新增
|
||||
data.add.push(a);
|
||||
}
|
||||
@@ -376,7 +377,7 @@ const filterChangedData = (oldArr: object[], nowArr: object[], key: string): { d
|
||||
for (let f in a) {
|
||||
let oldV = a[f];
|
||||
let newV = newData[f];
|
||||
if (oldV.toString() !== newV.toString()) {
|
||||
if (oldV?.toString() !== newV?.toString()) {
|
||||
data.upd.push(newData);
|
||||
break;
|
||||
}
|
||||
@@ -399,12 +400,12 @@ const genSql = () => {
|
||||
// 修改
|
||||
if (state.activeName === '1') {
|
||||
// 修改列
|
||||
let changeData = filterChangedData(oldData.fields, state.tableData.fields.res, 'name');
|
||||
return dbDialect.getModifyColumnSql(data.tableName, changeData);
|
||||
let changeData = filterChangedData(state.tableData.fields.oldFields, state.tableData.fields.res, 'name');
|
||||
return dbDialect.getModifyColumnSql(data, data.tableName, changeData);
|
||||
} else if (state.activeName === '2') {
|
||||
// 修改索引
|
||||
let changeData = filterChangedData(oldData.indexs, state.tableData.indexs.res, 'indexName');
|
||||
return dbDialect.getModifyIndexSql(data.tableName, changeData);
|
||||
let changeData = filterChangedData(state.tableData.indexs.oldIndexs, state.tableData.indexs.res, 'indexName');
|
||||
return dbDialect.getModifyIndexSql(data, data.tableName, changeData);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -414,28 +415,8 @@ const reset = () => {
|
||||
formRef.value.resetFields();
|
||||
state.tableData.tableName = '';
|
||||
state.tableData.tableComment = '';
|
||||
state.tableData.fields.res = [
|
||||
{
|
||||
name: '',
|
||||
type: '',
|
||||
value: '',
|
||||
length: '',
|
||||
numScale: '',
|
||||
notNull: false,
|
||||
pri: false,
|
||||
auto_increment: false,
|
||||
remark: '',
|
||||
},
|
||||
];
|
||||
state.tableData.indexs.res = [
|
||||
{
|
||||
indexName: '',
|
||||
columnNames: [],
|
||||
unique: false,
|
||||
indexType: 'BTREE',
|
||||
indexComment: '',
|
||||
},
|
||||
];
|
||||
state.tableData.fields.res = [];
|
||||
state.tableData.indexs.res = [];
|
||||
};
|
||||
|
||||
const indexChanges = (row: any) => {
|
||||
@@ -456,7 +437,21 @@ const indexChanges = (row: any) => {
|
||||
row.indexComment = `${tableData.value.tableName}表(${name.replaceAll('_', ',')})${commentSuffix}`;
|
||||
};
|
||||
|
||||
const oldData = { indexs: [] as any[], fields: [] as RowDefinition[] };
|
||||
const disableEditIncr = () => {
|
||||
if (DbType.postgresql === props.dbType) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 如果是mssql则不能修改自增
|
||||
if (props.data?.edit) {
|
||||
if (DbType.mssql === props.dbType) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
(newValue: any) => {
|
||||
@@ -464,9 +459,10 @@ watch(
|
||||
// 回显表名表注释
|
||||
state.tableData.tableName = row.tableName;
|
||||
state.tableData.tableComment = row.tableComment;
|
||||
state.tableData.db = props.db!;
|
||||
// 回显列
|
||||
if (columns && Array.isArray(columns) && columns.length > 0) {
|
||||
oldData.fields = [];
|
||||
state.tableData.fields.oldFields = [];
|
||||
state.tableData.fields.res = [];
|
||||
// 索引列下拉选
|
||||
state.tableData.indexs.columns = [];
|
||||
@@ -474,26 +470,33 @@ watch(
|
||||
let typeObj = a.columnType.replace(')', '').split('(');
|
||||
let type = typeObj[0];
|
||||
let length = (typeObj.length > 1 && typeObj[1]) || '';
|
||||
let defaultValue = '';
|
||||
if (a.columnDefault) {
|
||||
defaultValue = a.columnDefault.trim().replace(/^'|'$/g, '');
|
||||
// 解决高斯的默认值问题
|
||||
defaultValue = defaultValue.replace("'::character varying", '');
|
||||
}
|
||||
let data = {
|
||||
name: a.columnName,
|
||||
oldName: a.columnName,
|
||||
type,
|
||||
value: a.columnDefault || '',
|
||||
value: defaultValue,
|
||||
length,
|
||||
numScale: a.numScale,
|
||||
notNull: a.nullable !== 'YES',
|
||||
pri: a.columnKey === 'PRI',
|
||||
auto_increment: a.columnKey === 'PRI' /*a.extra?.indexOf('auto_increment') > -1*/,
|
||||
pri: a.isPrimaryKey,
|
||||
auto_increment: a.isIdentity /*a.extra?.indexOf('auto_increment') > -1*/,
|
||||
remark: a.columnComment,
|
||||
};
|
||||
state.tableData.fields.res.push(data);
|
||||
oldData.fields.push(JSON.parse(JSON.stringify(data)));
|
||||
state.tableData.fields.oldFields.push(JSON.parse(JSON.stringify(data)));
|
||||
// 索引字段下拉选项
|
||||
state.tableData.indexs.columns.push({ name: a.columnName, remark: a.columnComment });
|
||||
});
|
||||
}
|
||||
// 回显索引
|
||||
if (indexs && Array.isArray(indexs) && indexs.length > 0) {
|
||||
oldData.indexs = [];
|
||||
state.tableData.indexs.oldIndexs = [];
|
||||
state.tableData.indexs.res = [];
|
||||
// 索引过滤掉主键
|
||||
indexs
|
||||
@@ -502,12 +505,12 @@ watch(
|
||||
let data = {
|
||||
indexName: a.indexName,
|
||||
columnNames: a.columnName?.split(','),
|
||||
unique: a.nonUnique === 0 || false,
|
||||
unique: a.isUnique || false,
|
||||
indexType: a.indexType,
|
||||
indexComment: a.indexComment,
|
||||
};
|
||||
state.tableData.indexs.res.push(data);
|
||||
oldData.indexs.push(JSON.parse(JSON.stringify(data)));
|
||||
state.tableData.indexs.oldIndexs.push(JSON.parse(JSON.stringify(data)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,9 +68,7 @@
|
||||
<template #default="scope">
|
||||
<el-link @click.prevent="showColumns(scope.row)" type="primary">字段</el-link>
|
||||
<el-link class="ml5" @click.prevent="showTableIndex(scope.row)" type="success">索引</el-link>
|
||||
<el-link class="ml5" v-if="tableCreateDialog.enableEditTypes.indexOf(dbType) > -1" @click.prevent="openEditTable(scope.row)" type="warning"
|
||||
>编辑表</el-link
|
||||
>
|
||||
<el-link class="ml5" v-if="editDbTypes.indexOf(dbType) > -1" @click.prevent="openEditTable(scope.row)" type="warning">编辑表</el-link>
|
||||
<el-link class="ml5" @click.prevent="showCreateDdl(scope.row)" type="info">DDL</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -127,7 +125,7 @@ import SqlExecBox from '../sqleditor/SqlExecBox';
|
||||
import config from '@/common/config';
|
||||
import { joinClientParams } from '@/common/request';
|
||||
import { isTrue } from '@/common/assert';
|
||||
import { compatibleMysql, DbType } from '../../dialect/index';
|
||||
import { compatibleMysql, DbType, editDbTypes } from '../../dialect/index';
|
||||
|
||||
const DbTableOp = defineAsyncComponent(() => import('./DbTableOp.vue'));
|
||||
|
||||
@@ -181,7 +179,6 @@ const state = reactive({
|
||||
visible: false,
|
||||
activeName: '1',
|
||||
type: '',
|
||||
enableEditTypes: [DbType.mysql, DbType.mariadb, DbType.postgresql, DbType.dm, DbType.oracle], // 支持"编辑表"的数据库类型
|
||||
data: {
|
||||
// 修改表时,传递修改数据
|
||||
edit: false,
|
||||
|
||||
@@ -7,6 +7,10 @@ import { editor, languages, Position } from 'monaco-editor';
|
||||
|
||||
import { registerCompletionItemProvider } from '@/components/monaco/completionItemProvider';
|
||||
import { DbDialect, EditorCompletionItem, getDbDialect } from './dialect';
|
||||
import { type RemovableRef, useLocalStorage } from '@vueuse/core';
|
||||
|
||||
const hintsStorage: RemovableRef<Map<string, any>> = useLocalStorage('db-table-hints', new Map());
|
||||
const tableStorage: RemovableRef<Map<string, any>> = useLocalStorage('db-tables', new Map());
|
||||
|
||||
const dbInstCache: Map<number, DbInst> = new Map();
|
||||
|
||||
@@ -58,14 +62,15 @@ export class DbInst {
|
||||
if (!dbName) {
|
||||
throw new Error('dbName不能为空');
|
||||
}
|
||||
let db = this.dbs.get(dbName);
|
||||
let key = `${this.id}_${dbName}`;
|
||||
let db = this.dbs.get(key);
|
||||
if (db) {
|
||||
return db;
|
||||
}
|
||||
console.info(`new db -> dbId: ${this.id}, dbName: ${dbName}`);
|
||||
db = new Db();
|
||||
db.name = dbName;
|
||||
this.dbs.set(dbName, db);
|
||||
this.dbs.set(key, db);
|
||||
return db;
|
||||
}
|
||||
|
||||
@@ -77,17 +82,22 @@ export class DbInst {
|
||||
*/
|
||||
async loadTables(dbName: string, reload?: boolean) {
|
||||
const db = this.getDb(dbName);
|
||||
// 优先从 table map中获取
|
||||
let tables = db.tables;
|
||||
let key = this.dbTablesKey(dbName);
|
||||
let tables = tableStorage.value.get(key);
|
||||
// 优先从 table 缓存中获取
|
||||
if (!reload && tables) {
|
||||
db.tables = tables;
|
||||
return tables;
|
||||
}
|
||||
// 重置列信息缓存与表提示信息
|
||||
db.columnsMap?.clear();
|
||||
db.tableHints = null;
|
||||
console.log(`load tables -> dbName: ${dbName}`);
|
||||
tables = await dbApi.tableInfos.request({ id: this.id, db: dbName });
|
||||
tableStorage.value.set(key, tables);
|
||||
db.tables = tables;
|
||||
|
||||
// 异步加载表提示信息
|
||||
this.loadDbHints(dbName, true).then(() => {});
|
||||
return tables;
|
||||
}
|
||||
|
||||
@@ -169,18 +179,30 @@ export class DbInst {
|
||||
return this.getDb(dbName).getColumn(table, columnName);
|
||||
}
|
||||
|
||||
dbTableHintsKey(dbName: string) {
|
||||
return `db-table-hints_${this.id}_${dbName}`;
|
||||
}
|
||||
|
||||
dbTablesKey(dbName: string) {
|
||||
return `db-tables_${this.id}_${dbName}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取库信息提示
|
||||
*/
|
||||
async loadDbHints(dbName: string) {
|
||||
async loadDbHints(dbName: string, reload?: boolean) {
|
||||
const db = this.getDb(dbName);
|
||||
if (db.tableHints) {
|
||||
return db.tableHints;
|
||||
let key = this.dbTableHintsKey(dbName);
|
||||
let hints = hintsStorage.value.get(key);
|
||||
if (!reload && hints) {
|
||||
db.tableHints = hints;
|
||||
return hints;
|
||||
}
|
||||
console.log(`load db-hits -> dbName: ${dbName}`);
|
||||
const hits = await dbApi.hintTables.request({ id: this.id, db: db.name });
|
||||
db.tableHints = hits;
|
||||
return hits;
|
||||
hints = await dbApi.hintTables.request({ id: this.id, db: db.name });
|
||||
db.tableHints = hints;
|
||||
hintsStorage.value.set(key, hints);
|
||||
return hints;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -225,8 +247,8 @@ export class DbInst {
|
||||
};
|
||||
|
||||
// 获取指定表的默认查询sql
|
||||
getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number = DbInst.DefaultLimit) {
|
||||
return getDbDialect(this.type).getDefaultSelectSql(table, condition, orderBy, pageNum, limit);
|
||||
getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number = DbInst.DefaultLimit) {
|
||||
return getDbDialect(this.type).getDefaultSelectSql(db, table, condition, orderBy, pageNum, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -275,6 +297,7 @@ export class DbInst {
|
||||
sql,
|
||||
dbId: this.id,
|
||||
db,
|
||||
dbType: getDbDialect(this.type).getInfo().formatSqlDialect,
|
||||
runSuccessCallback: successFunc,
|
||||
cancelCallback: cancelFunc,
|
||||
});
|
||||
@@ -363,7 +386,7 @@ export class DbInst {
|
||||
return value;
|
||||
}
|
||||
if (!dbDialect) {
|
||||
return `${value}`;
|
||||
return `'${value}'`;
|
||||
}
|
||||
return dbDialect.wrapStrValue(columnType, value);
|
||||
}
|
||||
@@ -441,7 +464,7 @@ class Db {
|
||||
getColumn(table: string, columnName: string = '') {
|
||||
const cols = this.getColumns(table);
|
||||
if (!columnName) {
|
||||
const col = cols.find((c: any) => c.columnKey == 'PRI');
|
||||
const col = cols.find((c: any) => c.isPrimaryKey);
|
||||
return col || cols[0];
|
||||
}
|
||||
return cols.find((c: any) => c.columnName == columnName);
|
||||
|
||||
@@ -54,6 +54,7 @@ const DM_TYPE_LIST: sqlColumnType[] = [
|
||||
{ udtName: 'BFILE', dataType: 'BFILE', desc: '二进制文件', space: '', range: '100G-1' },
|
||||
];
|
||||
|
||||
// 参考官方文档:https://eco.dameng.com/document/dm/zh-cn/pm/function.html
|
||||
const replaceFunctions: EditorCompletionItem[] = [
|
||||
// 数值函数
|
||||
{ label: 'ABS', insertText: 'ABS(n)', description: '求数值 n 的绝对值' },
|
||||
@@ -365,21 +366,22 @@ class DMDialect implements DbDialect {
|
||||
};
|
||||
|
||||
dmDialectInfo = {
|
||||
name: 'DM',
|
||||
icon: 'iconfont icon-db-dm',
|
||||
defaultPort: 5236,
|
||||
formatSqlDialect: 'postgresql',
|
||||
formatSqlDialect: 'plsql',
|
||||
columnTypes: DM_TYPE_LIST.sort((a, b) => a.udtName.localeCompare(b.udtName)),
|
||||
editorCompletions,
|
||||
};
|
||||
return dmDialectInfo;
|
||||
}
|
||||
|
||||
getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
|
||||
getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
|
||||
return `SELECT * FROM "${table}" ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''} ${this.getPageSql(pageNum, limit)};`;
|
||||
}
|
||||
|
||||
getPageSql(pageNum: number, limit: number) {
|
||||
return ` OFFSET ${(pageNum - 1) * limit} LIMIT ${limit};`;
|
||||
return ` OFFSET ${(pageNum - 1) * limit} LIMIT ${limit}`;
|
||||
}
|
||||
|
||||
getDefaultRows(): RowDefinition[] {
|
||||
@@ -500,7 +502,9 @@ class DMDialect implements DbDialect {
|
||||
// 默认值
|
||||
let defVal = this.getDefaultValueSql(cl);
|
||||
let incr = cl.auto_increment ? 'IDENTITY' : '';
|
||||
return ` "${cl.name}" ${cl.type}${length} ${incr} ${cl.notNull ? 'NOT NULL' : ''} ${defVal} `;
|
||||
// 如果有原名以原名为准
|
||||
let name = cl.oldName && cl.name !== cl.oldName ? cl.oldName : cl.name;
|
||||
return ` ${this.quoteIdentifier(name)} ${cl.type}${length} ${incr} ${cl.notNull ? 'NOT NULL' : ''} ${defVal} `;
|
||||
}
|
||||
|
||||
getCreateTableSql(data: any): string {
|
||||
@@ -546,35 +550,78 @@ class DMDialect implements DbDialect {
|
||||
return sql.join(';');
|
||||
}
|
||||
|
||||
getModifyColumnSql(tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
|
||||
let sql: string[] = [];
|
||||
getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
|
||||
let schemaArr = tableData.db.split('/');
|
||||
let schema = schemaArr.length > 1 ? schemaArr[schemaArr.length - 1] : schemaArr[0];
|
||||
|
||||
let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableName)}`;
|
||||
|
||||
let modifySql = '';
|
||||
let dropSql = '';
|
||||
let renameSql = '';
|
||||
let commentSql = '';
|
||||
|
||||
// 主键字段
|
||||
let priArr = new Set();
|
||||
|
||||
if (changeData.add.length > 0) {
|
||||
changeData.add.forEach((a) => {
|
||||
sql.push(`ALTER TABLE "${tableName}" add COLUMN ${this.genColumnBasicSql(a)}`);
|
||||
modifySql += `ALTER TABLE ${dbTable} add COLUMN ${this.genColumnBasicSql(a)};`;
|
||||
if (a.remark) {
|
||||
sql.push(`comment on COLUMN "${tableName}"."${a.name}" is '${a.remark}'`);
|
||||
commentSql += `COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(a.name)} IS '${a.remark}';`;
|
||||
}
|
||||
if (a.pri) {
|
||||
priArr.add(`"${a.name}"`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (changeData.upd.length > 0) {
|
||||
changeData.upd.forEach((a) => {
|
||||
sql.push(`ALTER TABLE "${tableName}" MODIFY ${this.genColumnBasicSql(a)}`);
|
||||
if (a.remark) {
|
||||
sql.push(`comment on COLUMN "${tableName}"."${a.name}" is '${a.remark}'`);
|
||||
let cmtSql = `COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(a.name)} IS '${a.remark}';`;
|
||||
if (a.remark && a.oldName === a.name) {
|
||||
commentSql += cmtSql;
|
||||
}
|
||||
// 修改了字段名
|
||||
if (a.oldName !== a.name) {
|
||||
renameSql += `ALTER TABLE ${dbTable} RENAME COLUMN ${this.quoteIdentifier(a.oldName!)} TO ${this.quoteIdentifier(a.name)};`;
|
||||
if (a.remark) {
|
||||
commentSql += cmtSql;
|
||||
}
|
||||
}
|
||||
modifySql += `ALTER TABLE ${dbTable} MODIFY ${this.genColumnBasicSql(a)};`;
|
||||
if (a.pri) {
|
||||
priArr.add(`${this.quoteIdentifier(a.name)}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (changeData.del.length > 0) {
|
||||
changeData.del.forEach((a) => {
|
||||
sql.push(`ALTER TABLE "${tableName}" DROP COLUMN ${a.name}`);
|
||||
dropSql += `ALTER TABLE ${dbTable} DROP COLUMN ${a.name};`;
|
||||
});
|
||||
}
|
||||
return sql.join(';');
|
||||
|
||||
// 编辑主键
|
||||
let dropPkSql = '';
|
||||
if (priArr.size > 0) {
|
||||
let resPri = tableData.fields.res.filter((a: RowDefinition) => a.pri);
|
||||
if (resPri) {
|
||||
priArr.add(`${this.quoteIdentifier(resPri.name)}`);
|
||||
}
|
||||
// 如果有编辑主键字段,则删除主键,再添加主键
|
||||
// 解析表字段中是否含有主键,有的话就删除主键
|
||||
if (tableData.fields.oldFields.find((a: RowDefinition) => a.pri)) {
|
||||
dropPkSql = `ALTER TABLE ${dbTable} DROP PRIMARY KEY;`;
|
||||
}
|
||||
}
|
||||
|
||||
let addPkSql = priArr.size > 0 ? `ALTER TABLE ${dbTable} ADD PRIMARY KEY (${Array.from(priArr).join(',')});` : '';
|
||||
|
||||
return dropPkSql + modifySql + dropSql + renameSql + addPkSql + commentSql;
|
||||
}
|
||||
|
||||
getModifyIndexSql(tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
|
||||
getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
|
||||
// 不能直接修改索引名或字段、需要先删后加
|
||||
let dropIndexNames: string[] = [];
|
||||
let addIndexs: any[] = [];
|
||||
|
||||
17
mayfly_go_web/src/views/ops/db/dialect/gauss_dialect.ts
Normal file
17
mayfly_go_web/src/views/ops/db/dialect/gauss_dialect.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { PostgresqlDialect } from '@/views/ops/db/dialect/postgres_dialect';
|
||||
import { DialectInfo } from '@/views/ops/db/dialect/index';
|
||||
|
||||
let gsDialectInfo: DialectInfo;
|
||||
export class GaussDialect extends PostgresqlDialect {
|
||||
getInfo(): DialectInfo {
|
||||
if (gsDialectInfo) {
|
||||
return gsDialectInfo;
|
||||
}
|
||||
|
||||
gsDialectInfo = {} as DialectInfo;
|
||||
Object.assign(gsDialectInfo, super.getInfo());
|
||||
gsDialectInfo.icon = 'iconfont icon-gauss';
|
||||
gsDialectInfo.name = 'GaussDB';
|
||||
return gsDialectInfo;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,9 @@ import { PostgresqlDialect } from './postgres_dialect';
|
||||
import { DMDialect } from '@/views/ops/db/dialect/dm_dialect';
|
||||
import { OracleDialect } from '@/views/ops/db/dialect/oracle_dialect';
|
||||
import { MariadbDialect } from '@/views/ops/db/dialect/mariadb_dialect';
|
||||
import { SqliteDialect } from '@/views/ops/db/dialect/sqlite_dialect';
|
||||
import { MssqlDialect } from '@/views/ops/db/dialect/mssql_dialect';
|
||||
import { GaussDialect } from '@/views/ops/db/dialect/gauss_dialect';
|
||||
|
||||
export interface sqlColumnType {
|
||||
udtName: string;
|
||||
@@ -14,6 +17,7 @@ export interface sqlColumnType {
|
||||
|
||||
export interface RowDefinition {
|
||||
name: string;
|
||||
oldName?: string;
|
||||
type: string;
|
||||
value: string;
|
||||
length: string;
|
||||
@@ -78,6 +82,11 @@ export const ColumnTypeSubscript = {
|
||||
|
||||
// 数据库基础信息
|
||||
export interface DialectInfo {
|
||||
/**
|
||||
* 数据库类型label
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* 图标
|
||||
*/
|
||||
@@ -108,10 +117,21 @@ export const DbType = {
|
||||
mysql: 'mysql',
|
||||
mariadb: 'mariadb',
|
||||
postgresql: 'postgres',
|
||||
gauss: 'gauss',
|
||||
dm: 'dm', // 达梦
|
||||
oracle: 'oracle',
|
||||
sqlite: 'sqlite',
|
||||
mssql: 'mssql', // ms sqlserver
|
||||
};
|
||||
|
||||
// mysql兼容的数据库
|
||||
export const noSchemaTypes = [DbType.mysql, DbType.mariadb, DbType.sqlite];
|
||||
|
||||
// 有schema层的数据库
|
||||
export const schemaDbTypes = [DbType.postgresql, DbType.gauss, DbType.dm, DbType.oracle, DbType.mssql];
|
||||
|
||||
export const editDbTypes = [...noSchemaTypes, ...schemaDbTypes];
|
||||
|
||||
export const compatibleMysql = (dbType: string): boolean => {
|
||||
switch (dbType) {
|
||||
case DbType.mysql:
|
||||
@@ -130,13 +150,14 @@ export interface DbDialect {
|
||||
|
||||
/**
|
||||
* 获取默认查询sql
|
||||
* @param db 数据库信息
|
||||
* @param table 表名
|
||||
* @param condition 条件
|
||||
* @param orderBy 排序
|
||||
* @param pageNum 页数
|
||||
* @param limit 条数
|
||||
*/
|
||||
getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number): string;
|
||||
getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number): string;
|
||||
|
||||
getPageSql(pageNum: number, limit: number): string;
|
||||
|
||||
@@ -164,47 +185,51 @@ export interface DbDialect {
|
||||
|
||||
/**
|
||||
* 生成编辑列sql
|
||||
* @param tableData 表数据,包含表名、列数据、索引数据
|
||||
* @param tableName 表名
|
||||
* @param changeData 改变信息
|
||||
*/
|
||||
getModifyColumnSql(tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string;
|
||||
getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string;
|
||||
|
||||
/**
|
||||
* 生成编辑索引sql
|
||||
* @param tableData 表数据,包含表名、列数据、索引数据
|
||||
* @param tableName 表名
|
||||
* @param changeData 改变数据
|
||||
*/
|
||||
getModifyIndexSql(tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string;
|
||||
getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string;
|
||||
|
||||
/** 通过数据库字段类型,返回基本数据类型 */
|
||||
getDataType: (columnType: string) => DataType;
|
||||
getDataType(columnType: string): DataType;
|
||||
|
||||
/** 包装字符串数据, 如:oracle需要把date类型改为 to_date(str, 'yyyy-mm-dd hh24:mi:ss') */
|
||||
wrapStrValue(columnType: string, value: string): string;
|
||||
}
|
||||
|
||||
let mysqlDialect = new MysqlDialect();
|
||||
let mariadbDialect = new MariadbDialect();
|
||||
let postgresDialect = new PostgresqlDialect();
|
||||
let dmDialect = new DMDialect();
|
||||
let oracleDialect = new OracleDialect();
|
||||
|
||||
export const getDbDialect = (dbType: string | undefined): DbDialect => {
|
||||
if (!dbType) {
|
||||
return mysqlDialect;
|
||||
}
|
||||
switch (dbType) {
|
||||
case DbType.mysql:
|
||||
return mysqlDialect;
|
||||
case DbType.mariadb:
|
||||
return mariadbDialect;
|
||||
case DbType.postgresql:
|
||||
return postgresDialect;
|
||||
case DbType.dm:
|
||||
return dmDialect;
|
||||
case DbType.oracle:
|
||||
return oracleDialect;
|
||||
default:
|
||||
throw new Error('不支持的数据库');
|
||||
}
|
||||
let dbType2DialectMap: Map<string, DbDialect> = new Map();
|
||||
|
||||
export const registerDbDialect = (dbType: string, dd: DbDialect) => {
|
||||
dbType2DialectMap.set(dbType, dd);
|
||||
};
|
||||
|
||||
export const getDbDialectMap = () => {
|
||||
return dbType2DialectMap;
|
||||
};
|
||||
|
||||
export const getDbDialect = (dbType: string): DbDialect => {
|
||||
return dbType2DialectMap.get(dbType) || mysqlDialect;
|
||||
};
|
||||
|
||||
(function () {
|
||||
console.log('init register db dialect');
|
||||
registerDbDialect(DbType.mysql, mysqlDialect);
|
||||
registerDbDialect(DbType.mariadb, new MariadbDialect());
|
||||
registerDbDialect(DbType.postgresql, new PostgresqlDialect());
|
||||
registerDbDialect(DbType.gauss, new GaussDialect());
|
||||
registerDbDialect(DbType.dm, new DMDialect());
|
||||
registerDbDialect(DbType.oracle, new OracleDialect());
|
||||
registerDbDialect(DbType.sqlite, new SqliteDialect());
|
||||
registerDbDialect(DbType.mssql, new MssqlDialect());
|
||||
})();
|
||||
|
||||
@@ -12,6 +12,7 @@ class MariadbDialect extends MysqlDialect implements DbDialect {
|
||||
|
||||
mariadbDialectInfo = {} as DialectInfo;
|
||||
Object.assign(mariadbDialectInfo, super.getInfo());
|
||||
mariadbDialectInfo.name = 'MariaDB';
|
||||
mariadbDialectInfo.icon = 'iconfont icon-mariadb';
|
||||
return mariadbDialectInfo;
|
||||
}
|
||||
|
||||
405
mayfly_go_web/src/views/ops/db/dialect/mssql_dialect.ts
Normal file
405
mayfly_go_web/src/views/ops/db/dialect/mssql_dialect.ts
Normal file
@@ -0,0 +1,405 @@
|
||||
import { DbInst } from '../db';
|
||||
import { commonCustomKeywords, DataType, DbDialect, DialectInfo, EditorCompletion, EditorCompletionItem, IndexDefinition, RowDefinition } from './index';
|
||||
import { language as sqlLanguage } from 'monaco-editor/esm/vs/basic-languages/sql/sql.js';
|
||||
|
||||
export { MSSQL_TYPE_LIST, MssqlDialect };
|
||||
|
||||
// 参考官方文档:https://docs.microsoft.com/zh-cn/sql/t-sql/data-types/data-types-transact-sql?view=sql-server-ver15
|
||||
const MSSQL_TYPE_LIST = [
|
||||
//精确数字
|
||||
'bigint',
|
||||
'numeric',
|
||||
'bit',
|
||||
'smallint',
|
||||
'decimal',
|
||||
'smallmoney',
|
||||
'int',
|
||||
'tinyint',
|
||||
'money',
|
||||
// 近似数字
|
||||
'float',
|
||||
'real',
|
||||
// 日期和时间
|
||||
'date',
|
||||
'datetimeoffset',
|
||||
'datetime2',
|
||||
'smalldatetime',
|
||||
'datetime',
|
||||
'time',
|
||||
// 字符串
|
||||
'char',
|
||||
'varchar',
|
||||
'text',
|
||||
'nchar',
|
||||
'nvarchar',
|
||||
'ntext',
|
||||
'binary',
|
||||
'varbinary',
|
||||
|
||||
// 其他
|
||||
'cursor',
|
||||
'rowversion',
|
||||
'hierarchyid',
|
||||
'uniqueidentifier',
|
||||
'sql_variant',
|
||||
'xml',
|
||||
'table',
|
||||
// 空间几何类型 参照 https://learn.microsoft.com/zh-cn/sql/t-sql/spatial-geometry/spatial-types-geometry-transact-sql?view=sql-server-ver15
|
||||
'geometry',
|
||||
// 空间地理类型 参照 https://learn.microsoft.com/zh-cn/sql/t-sql/spatial-geography/spatial-types-geography?view=sql-server-ver15
|
||||
'geography',
|
||||
];
|
||||
// 函数参考官方文档 https://learn.microsoft.com/zh-cn/sql/t-sql/functions/functions?view=sql-server-ver15
|
||||
|
||||
let mssqlDialectInfo: DialectInfo;
|
||||
|
||||
const customKeywords: EditorCompletionItem[] = [
|
||||
{
|
||||
label: 'select top ',
|
||||
description: 'keyword',
|
||||
insertText: 'select top 100 * from',
|
||||
},
|
||||
{
|
||||
label: 'select page ',
|
||||
description: 'keyword',
|
||||
insertText: 'SELECT *, 0 AS _ORDER_F_ FROM table_name \n ORDER BY _ORDER_F_ \n OFFSET 0 ROWS FETCH NEXT 25 ROWS ONLY;',
|
||||
},
|
||||
];
|
||||
|
||||
const fixedLengthTypes = [
|
||||
'int',
|
||||
'bigint',
|
||||
'smallint',
|
||||
'tinyint',
|
||||
'float',
|
||||
'real',
|
||||
'datetime',
|
||||
'smalldatetime',
|
||||
'date',
|
||||
'time',
|
||||
'datetime2',
|
||||
'datetimeoffset',
|
||||
'bit',
|
||||
'uniqueidentifier',
|
||||
'geometry',
|
||||
'geography',
|
||||
];
|
||||
|
||||
class MssqlDialect implements DbDialect {
|
||||
getInfo(): DialectInfo {
|
||||
if (mssqlDialectInfo) {
|
||||
return mssqlDialectInfo;
|
||||
}
|
||||
|
||||
let { keywords, operators, builtinVariables, builtinFunctions } = sqlLanguage;
|
||||
let functions = builtinFunctions.map((a: string): EditorCompletionItem => ({ label: a, insertText: `${a}()`, description: 'func' }));
|
||||
|
||||
let excludeKeywords = new Set(operators);
|
||||
let editorCompletions: EditorCompletion = {
|
||||
keywords: keywords
|
||||
.filter((a: string) => !excludeKeywords.has(a)) // 移除已存在的operator、function
|
||||
.map((a: string): EditorCompletionItem => ({ label: a, description: 'keyword' }))
|
||||
.concat(customKeywords)
|
||||
.concat(commonCustomKeywords.map((a): EditorCompletionItem => ({ label: a, description: 'keyword' }))),
|
||||
operators: operators.map((a: string): EditorCompletionItem => ({ label: a, description: 'operator' })),
|
||||
functions,
|
||||
variables: builtinVariables.map((a: string): EditorCompletionItem => ({ label: a, description: 'var' })),
|
||||
};
|
||||
|
||||
mssqlDialectInfo = {
|
||||
name: 'MSSQL',
|
||||
icon: 'iconfont icon-MSSQLNATIVE',
|
||||
defaultPort: 1433,
|
||||
formatSqlDialect: 'transactsql',
|
||||
columnTypes: MSSQL_TYPE_LIST.map((a) => ({ udtName: a, dataType: a, desc: '', space: '' })),
|
||||
editorCompletions,
|
||||
};
|
||||
return mssqlDialectInfo;
|
||||
}
|
||||
|
||||
getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
|
||||
let schema = db.split('/')[1];
|
||||
return `SELECT *, 0 AS _MAY_ORDER_F_ FROM ${this.quoteIdentifier(schema)}.${this.quoteIdentifier(table)} ${condition ? 'WHERE ' + condition : ''} ${
|
||||
orderBy ? orderBy + ', _MAY_ORDER_F_' : 'order by _MAY_ORDER_F_'
|
||||
} ${this.getPageSql(pageNum, limit)};`.toUpperCase();
|
||||
}
|
||||
|
||||
getPageSql(pageNum: number, limit: number) {
|
||||
return ` offset ${(pageNum - 1) * limit} rows fetch next ${limit} rows only`.toUpperCase();
|
||||
}
|
||||
|
||||
getDefaultRows(): RowDefinition[] {
|
||||
return [
|
||||
{ name: 'id', type: 'bigint', length: '', numScale: '', value: '', notNull: true, pri: true, auto_increment: true, remark: '主键ID' },
|
||||
{ name: 'creator_id', type: 'bigint', length: '20', numScale: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '创建人id' },
|
||||
{
|
||||
name: 'creator',
|
||||
type: 'varchar',
|
||||
length: '100',
|
||||
numScale: '',
|
||||
value: '',
|
||||
notNull: true,
|
||||
pri: false,
|
||||
auto_increment: false,
|
||||
remark: '创建人姓名',
|
||||
},
|
||||
{
|
||||
name: 'create_time',
|
||||
type: 'datetime2',
|
||||
length: '',
|
||||
numScale: '',
|
||||
value: 'CURRENT_TIMESTAMP',
|
||||
notNull: true,
|
||||
pri: false,
|
||||
auto_increment: false,
|
||||
remark: '创建时间',
|
||||
},
|
||||
{ name: 'updator_id', type: 'bigint', length: '20', numScale: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '修改人id' },
|
||||
{
|
||||
name: 'updator',
|
||||
type: 'varchar',
|
||||
length: '100',
|
||||
numScale: '',
|
||||
value: '',
|
||||
notNull: true,
|
||||
pri: false,
|
||||
auto_increment: false,
|
||||
remark: '修改人姓名',
|
||||
},
|
||||
{
|
||||
name: 'update_time',
|
||||
type: 'datetime2',
|
||||
length: '',
|
||||
numScale: '',
|
||||
value: 'CURRENT_TIMESTAMP',
|
||||
notNull: true,
|
||||
pri: false,
|
||||
auto_increment: false,
|
||||
remark: '修改时间',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
getDefaultIndex(): IndexDefinition {
|
||||
return {
|
||||
indexName: '',
|
||||
columnNames: [],
|
||||
unique: false,
|
||||
indexType: 'NONCLUSTERED',
|
||||
indexComment: '',
|
||||
};
|
||||
}
|
||||
|
||||
quoteIdentifier = (name: string) => {
|
||||
return `[${name}]`;
|
||||
};
|
||||
|
||||
genColumnBasicSql(cl: any): string {
|
||||
let val = cl.value ? (cl.value === 'CURRENT_TIMESTAMP' ? cl.value : `'${cl.value}'`) : '';
|
||||
let defVal = val ? `DEFAULT ${val}` : '';
|
||||
// mssql哪些字段允许有长度
|
||||
let length = '';
|
||||
if (!fixedLengthTypes.includes(cl.type)) {
|
||||
length = cl.length ? `(${cl.length})` : '';
|
||||
}
|
||||
return ` ${this.quoteIdentifier(cl.name)} ${cl.type}${length} ${cl.auto_increment ? 'IDENTITY(1,1)' : ''} ${defVal} ${cl.notNull ? 'NOT NULL' : 'NULL'} `;
|
||||
}
|
||||
getCreateTableSql(data: any): string {
|
||||
let schema = data.db.split('/')[1];
|
||||
|
||||
// 创建表结构
|
||||
let pks = [] as string[];
|
||||
let fields: string[] = [];
|
||||
let fieldComments: string[] = [];
|
||||
data.fields.res.forEach((item: any) => {
|
||||
item.name && fields.push(this.genColumnBasicSql(item));
|
||||
item.remark &&
|
||||
fieldComments.push(
|
||||
`EXECUTE sp_addextendedproperty N'MS_Description', N'${item.remark}', N'SCHEMA', N'${schema}', N'TABLE', N'${data.tableName}', N'COLUMN', N'${item.name}'`
|
||||
);
|
||||
if (item.pri) {
|
||||
pks.push(`${this.quoteIdentifier(item.name)}`);
|
||||
}
|
||||
});
|
||||
|
||||
let baseTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(data.tableName)}`;
|
||||
|
||||
// 建表语句
|
||||
let createTable = `CREATE TABLE ${baseTable}
|
||||
( ${fields.join(',')}
|
||||
${pks.length > 0 ? `, PRIMARY KEY CLUSTERED (${pks.join(',')})` : ''}
|
||||
);`;
|
||||
|
||||
let createIndexSql = this.getCreateIndexSql(data);
|
||||
|
||||
// 表注释
|
||||
if (data.tableComment) {
|
||||
createTable += ` EXECUTE sp_addextendedproperty N'MS_Description', N'${data.tableComment}', N'SCHEMA', N'${schema}', N'TABLE', N'${data.tableName}';`;
|
||||
}
|
||||
|
||||
return createTable + createIndexSql + fieldComments.join(';');
|
||||
}
|
||||
|
||||
getCreateIndexSql(data: any): string {
|
||||
// CREATE UNIQUE NONCLUSTERED INDEX [aaa]
|
||||
// ON [dbo].[无标题] (
|
||||
// [id],
|
||||
// [name]
|
||||
// )
|
||||
let schema = data.db.split('/')[1];
|
||||
let baseTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(data.tableName)}`;
|
||||
|
||||
let indexComment = [] as string[];
|
||||
|
||||
// 创建索引
|
||||
let sql: string[] = [];
|
||||
data.indexs.res.forEach((a: any) => {
|
||||
let columnNames = a.columnNames.map((b: string) => `${this.quoteIdentifier(b)}`);
|
||||
sql.push(` CREATE ${a.unique ? 'UNIQUE' : ''} NONCLUSTERED INDEX ${this.quoteIdentifier(a.indexName)} on ${baseTable} (${columnNames.join(',')})`);
|
||||
if (a.indexComment) {
|
||||
indexComment.push(
|
||||
`EXECUTE sp_addextendedproperty N'MS_Description', N'${a.indexComment}', N'SCHEMA', N'${schema}', N'TABLE', N'${data.tableName}', N'INDEX', N'${a.indexName}'`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return sql.join(';') + ';' + indexComment.join(';');
|
||||
}
|
||||
|
||||
getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
|
||||
// sql执行顺序
|
||||
// 1. 删除字段
|
||||
// 2. 添加字段
|
||||
// 3. 修改字段名字
|
||||
// 4. 修改字段类型
|
||||
// 5. 修改字段注释
|
||||
// 6. 添加字段注释
|
||||
|
||||
let schema = tableData.db.split('/')[1];
|
||||
let baseTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableName)}`;
|
||||
|
||||
let delSql = '';
|
||||
let addArr = [] as string[];
|
||||
let renameArr = [] as string[];
|
||||
let updArr = [] as string[];
|
||||
let changeCommentArr = [] as string[];
|
||||
let addCommentArr = [] as string[];
|
||||
|
||||
if (changeData.del.length > 0) {
|
||||
delSql = `ALTER TABLE ${baseTable} DROP ${changeData.del.map((a) => 'COLUMN ' + this.quoteIdentifier(a.name)).join(',')};`;
|
||||
}
|
||||
if (changeData.add.length > 0) {
|
||||
changeData.add.forEach((a) => {
|
||||
addArr.push(` ALTER TABLE ${baseTable} ADD COLUMN ${this.genColumnBasicSql(a)}`);
|
||||
if (a.remark) {
|
||||
addCommentArr.push(
|
||||
`EXECUTE sp_addextendedproperty N'MS_Description', N'${a.remark}', N'SCHEMA', N'${schema}', N'TABLE', N'${tableName}', N'COLUMN', N'${a.name}'`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (changeData.upd.length > 0) {
|
||||
changeData.upd.forEach((a) => {
|
||||
if (a.oldName && a.name !== a.oldName) {
|
||||
renameArr.push(` EXEC sp_rename '${baseTable}.${this.quoteIdentifier(a.oldName)}', '${a.name}', 'COLUMN' `);
|
||||
} else {
|
||||
updArr.push(` ALTER TABLE ${baseTable} ALTER COLUMN ${this.genColumnBasicSql(a)} `);
|
||||
}
|
||||
if (a.remark) {
|
||||
changeCommentArr.push(`IF ((SELECT COUNT(*) FROM fn_listextendedproperty('MS_Description',
|
||||
'SCHEMA', N'${schema}',
|
||||
'TABLE', N'${tableName}',
|
||||
'COLUMN', N'${a.name}')) > 0)
|
||||
EXEC sp_updateextendedproperty
|
||||
'MS_Description', N'${a.remark}',
|
||||
'SCHEMA', N'${schema}',
|
||||
'TABLE', N'${tableName}',
|
||||
'COLUMN', N'${a.name}'
|
||||
ELSE
|
||||
EXEC sp_addextendedproperty
|
||||
'MS_Description', N'${a.remark}',
|
||||
'SCHEMA', N'${schema}',
|
||||
'TABLE', N'${tableName}',
|
||||
'COLUMN',N'${a.name}'`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
delSql +
|
||||
(addArr.length > 0 ? addArr.join(';') + ';' : '') +
|
||||
(renameArr.length > 0 ? renameArr.join(';') + ';' : '') +
|
||||
(updArr.length > 0 ? updArr.join(';') + ';' : '') +
|
||||
(changeCommentArr.length > 0 ? changeCommentArr.join(';') + ';' : '') +
|
||||
(addCommentArr.length > 0 ? addCommentArr.join(';') + ';' : '')
|
||||
);
|
||||
}
|
||||
|
||||
getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
|
||||
let schema = tableData.db.split('/')[1];
|
||||
let baseTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableName)}`;
|
||||
|
||||
let dropArr = [] as string[];
|
||||
let addArr = [] as string[];
|
||||
let commentArr = [] as string[];
|
||||
|
||||
const pushDrop = (a: any) => {
|
||||
dropArr.push(` DROP INDEX ${this.quoteIdentifier(a.indexName)} ON ${baseTable} `);
|
||||
};
|
||||
const pushAdd = (a: any) => {
|
||||
addArr.push(
|
||||
` CREATE ${a.unique ? 'UNIQUE' : ''} NONCLUSTERED INDEX ${this.quoteIdentifier(a.indexName)} ON ${baseTable} (${a.columnNames.map((b: string) => this.quoteIdentifier(b)).join(',')}) `
|
||||
);
|
||||
if (a.indexComment) {
|
||||
commentArr.push(
|
||||
` EXEC sp_addextendedproperty N'MS_Description', N'${a.indexComment}', N'SCHEMA', N'${schema}', N'TABLE', N'${tableName}', N'INDEX', N'${a.indexName}' `
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (changeData.upd.length > 0) {
|
||||
changeData.upd.forEach((a) => {
|
||||
pushDrop(a);
|
||||
pushAdd(a);
|
||||
});
|
||||
}
|
||||
|
||||
if (changeData.del.length > 0) {
|
||||
changeData.del.forEach((a) => {
|
||||
pushDrop(a);
|
||||
});
|
||||
}
|
||||
|
||||
if (changeData.add.length > 0) {
|
||||
changeData.add.forEach((a) => pushAdd(a));
|
||||
}
|
||||
let dropSql = dropArr.join(';');
|
||||
let addSql = addArr.join(';');
|
||||
let commentSql = commentArr.join(';');
|
||||
return dropSql + ';' + addSql + ';' + commentSql + ';';
|
||||
}
|
||||
|
||||
getDataType(columnType: string): DataType {
|
||||
if (DbInst.isNumber(columnType)) {
|
||||
return DataType.Number;
|
||||
}
|
||||
// 日期时间类型
|
||||
if (/datetime|timestamp/gi.test(columnType)) {
|
||||
return DataType.DateTime;
|
||||
}
|
||||
// 日期类型
|
||||
if (/date/gi.test(columnType)) {
|
||||
return DataType.Date;
|
||||
}
|
||||
// 时间类型
|
||||
if (/time/gi.test(columnType)) {
|
||||
return DataType.Time;
|
||||
}
|
||||
return DataType.String;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
|
||||
wrapStrValue(columnType: string, value: string): string {
|
||||
return `'${value}'`;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { language as mysqlLanguage } from 'monaco-editor/esm/vs/basic-languages/
|
||||
|
||||
export { MYSQL_TYPE_LIST, MysqlDialect };
|
||||
|
||||
// 参考官方文档:https://dev.mysql.com/doc/refman/8.0/en/data-types.html
|
||||
const MYSQL_TYPE_LIST = [
|
||||
'bigint',
|
||||
'binary',
|
||||
@@ -31,6 +32,7 @@ const MYSQL_TYPE_LIST = [
|
||||
'varchar',
|
||||
];
|
||||
|
||||
// 参考官方文档:https://dev.mysql.com/doc/refman/8.3/en/functions.html
|
||||
const replaceFunctions: EditorCompletionItem[] = [
|
||||
/** 字符串相关函数 */
|
||||
{ label: 'CONCAT', insertText: 'CONCAT(str1,str2,...)', description: '多字符串合并' },
|
||||
@@ -102,6 +104,7 @@ class MysqlDialect implements DbDialect {
|
||||
};
|
||||
|
||||
mysqlDialectInfo = {
|
||||
name: 'MySQL',
|
||||
icon: 'iconfont icon-op-mysql',
|
||||
defaultPort: 3306,
|
||||
formatSqlDialect: 'mysql',
|
||||
@@ -111,7 +114,7 @@ class MysqlDialect implements DbDialect {
|
||||
return mysqlDialectInfo;
|
||||
}
|
||||
|
||||
getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
|
||||
getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
|
||||
return `SELECT * FROM ${this.quoteIdentifier(table)} ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''} ${this.getPageSql(
|
||||
pageNum,
|
||||
limit
|
||||
@@ -193,7 +196,7 @@ class MysqlDialect implements DbDialect {
|
||||
let defVal = val ? `DEFAULT ${val}` : '';
|
||||
let length = cl.length ? `(${cl.length})` : '';
|
||||
let onUpdate = 'update_time' === cl.name ? ' ON UPDATE CURRENT_TIMESTAMP ' : '';
|
||||
return ` ${cl.name} ${cl.type}${length} ${cl.notNull ? 'NOT NULL' : 'NULL'} ${
|
||||
return ` ${this.quoteIdentifier(cl.name)} ${cl.type}${length} ${cl.notNull ? 'NOT NULL' : 'NULL'} ${
|
||||
cl.auto_increment ? 'AUTO_INCREMENT' : ''
|
||||
} ${defVal} ${onUpdate} comment '${cl.remark || ''}' `;
|
||||
}
|
||||
@@ -223,38 +226,34 @@ class MysqlDialect implements DbDialect {
|
||||
return sql.substring(0, sql.length - 1) + ';';
|
||||
}
|
||||
|
||||
getModifyColumnSql(tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
|
||||
let addSql = '',
|
||||
updSql = '',
|
||||
delSql = '';
|
||||
if (changeData.add.length > 0) {
|
||||
addSql = `ALTER TABLE ${tableName}`;
|
||||
changeData.add.forEach((a) => {
|
||||
addSql += ` ADD ${this.genColumnBasicSql(a)},`;
|
||||
getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
|
||||
let sql = `ALTER TABLE ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(tableName)}`;
|
||||
let arr = [] as string[];
|
||||
if (changeData.del.length > 0) {
|
||||
changeData.del.forEach((a) => {
|
||||
arr.push(` DROP COLUMN ${this.quoteIdentifier(a.name)} `);
|
||||
});
|
||||
}
|
||||
if (changeData.add.length > 0) {
|
||||
changeData.add.forEach((a) => {
|
||||
arr.push(` ADD COLUMN ${this.genColumnBasicSql(a)} `);
|
||||
});
|
||||
addSql = addSql.substring(0, addSql.length - 1);
|
||||
addSql += ';';
|
||||
}
|
||||
|
||||
if (changeData.upd.length > 0) {
|
||||
updSql = `ALTER TABLE ${tableName}`;
|
||||
let arr = [] as string[];
|
||||
changeData.upd.forEach((a) => {
|
||||
arr.push(` MODIFY ${this.genColumnBasicSql(a)}`);
|
||||
if (a.name === a.oldName) {
|
||||
arr.push(` MODIFY COLUMN ${this.genColumnBasicSql(a)} `);
|
||||
} else {
|
||||
arr.push(` CHANGE COLUMN ${this.quoteIdentifier(a.oldName!)} ${this.genColumnBasicSql(a)} `);
|
||||
}
|
||||
});
|
||||
updSql += arr.join(',');
|
||||
updSql += ';';
|
||||
}
|
||||
|
||||
if (changeData.del.length > 0) {
|
||||
changeData.del.forEach((a) => {
|
||||
delSql += ` ALTER TABLE ${tableName} DROP COLUMN ${a.name}; `;
|
||||
});
|
||||
}
|
||||
return addSql + updSql + delSql;
|
||||
return sql + arr.join(',') + ';';
|
||||
}
|
||||
|
||||
getModifyIndexSql(tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
|
||||
getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
|
||||
// 搜集修改和删除的索引,添加到drop index xx
|
||||
// 收集新增和修改的索引,添加到ADD xx
|
||||
// ALTER TABLE `test1`
|
||||
|
||||
@@ -14,7 +14,7 @@ import { language as sqlLanguage } from 'monaco-editor/esm/vs/basic-languages/sq
|
||||
|
||||
export { OracleDialect, ORACLE_TYPE_LIST };
|
||||
|
||||
// 参考文档:https://eco.dameng.com/document/dm/zh-cn/sql-dev/dmpl-sql-datatype.html#%E5%AD%97%E7%AC%A6%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B
|
||||
// 参考官方文档:https://docs.oracle.com/cd/B19306_01/server.102/b14200/sql_elements001.htm
|
||||
const ORACLE_TYPE_LIST: sqlColumnType[] = [
|
||||
// 字符数据类型
|
||||
{ udtName: 'CHAR', dataType: 'CHAR', desc: '定长字符串,自动在末尾用空格补全,非unicode', space: '', range: '1 - 2000' },
|
||||
@@ -50,6 +50,7 @@ const ORACLE_TYPE_LIST: sqlColumnType[] = [
|
||||
{ udtName: 'BFILE', dataType: 'BFILE', desc: '二进制文件', space: '', range: '' },
|
||||
];
|
||||
|
||||
// 参考官方文档:https://docs.oracle.com/cd/B19306_01/server.102/b14200/functions001.htm
|
||||
const replaceFunctions: EditorCompletionItem[] = [
|
||||
// 字符函数
|
||||
{ label: 'ASCII', insertText: 'ASCII(x)', description: '返回字符X的ASCII码' },
|
||||
@@ -91,7 +92,35 @@ const replaceFunctions: EditorCompletionItem[] = [
|
||||
{ label: 'NVL2', insertText: 'NVL2(x,value1,value2)', description: '如果x非空,返回value1,否则返回value2' },
|
||||
];
|
||||
|
||||
const addCustomKeywords = ['ROWNUM', 'DUAL'];
|
||||
const addCustomKeywords: EditorCompletionItem[] = [
|
||||
{
|
||||
label: 'ROWNUM',
|
||||
description: 'keyword',
|
||||
insertText: 'ROWNUM',
|
||||
},
|
||||
{
|
||||
label: 'DUAL',
|
||||
description: 'keyword',
|
||||
insertText: 'DUAL',
|
||||
},
|
||||
// 分页代码块
|
||||
{
|
||||
label: 'SELECT ROWNUM',
|
||||
description: 'code block',
|
||||
insertText: 'SELECT * from table_name where rownum <= 10',
|
||||
},
|
||||
{
|
||||
label: 'SELECT PAGE',
|
||||
description: 'code block',
|
||||
insertText: ` SELECT * FROM
|
||||
(
|
||||
SELECT t.*, ROWNUM AS rn
|
||||
FROM table_name t
|
||||
WHERE ROWNUM <= 25
|
||||
)
|
||||
WHERE rn > 0 \n`,
|
||||
},
|
||||
];
|
||||
|
||||
let oracleDialectInfo: DialectInfo;
|
||||
class OracleDialect implements DbDialect {
|
||||
@@ -103,6 +132,7 @@ class OracleDialect implements DbDialect {
|
||||
let { keywords, operators, builtinVariables } = sqlLanguage;
|
||||
let functionNames = replaceFunctions.map((a) => a.label);
|
||||
let excludeKeywords = new Set(functionNames.concat(operators));
|
||||
excludeKeywords.add('SELECT');
|
||||
|
||||
let editorCompletions: EditorCompletion = {
|
||||
keywords: keywords
|
||||
@@ -117,21 +147,14 @@ class OracleDialect implements DbDialect {
|
||||
})
|
||||
)
|
||||
)
|
||||
.concat(
|
||||
// 加上自定义的关键字
|
||||
addCustomKeywords.map(
|
||||
(a): EditorCompletionItem => ({
|
||||
label: a,
|
||||
description: 'keyword',
|
||||
})
|
||||
)
|
||||
),
|
||||
.concat(addCustomKeywords),
|
||||
operators: operators.map((a: string): EditorCompletionItem => ({ label: a, description: 'operator' })),
|
||||
functions: replaceFunctions,
|
||||
variables: builtinVariables.map((a: string): EditorCompletionItem => ({ label: a, description: 'var' })),
|
||||
};
|
||||
|
||||
oracleDialectInfo = {
|
||||
name: 'Oracle',
|
||||
icon: 'iconfont icon-oracle',
|
||||
defaultPort: 1521,
|
||||
formatSqlDialect: 'plsql',
|
||||
@@ -141,7 +164,7 @@ class OracleDialect implements DbDialect {
|
||||
return oracleDialectInfo;
|
||||
}
|
||||
|
||||
getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
|
||||
getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
|
||||
return `
|
||||
SELECT *
|
||||
FROM (
|
||||
@@ -268,16 +291,22 @@ class OracleDialect implements DbDialect {
|
||||
return '';
|
||||
}
|
||||
|
||||
genColumnBasicSql(cl: RowDefinition): string {
|
||||
genColumnBasicSql(cl: RowDefinition, create: boolean): string {
|
||||
let length = this.getTypeLengthSql(cl);
|
||||
// 默认值
|
||||
let defVal = this.getDefaultValueSql(cl);
|
||||
let incr = cl.auto_increment ? 'generated by default as IDENTITY' : '';
|
||||
let pri = cl.pri ? 'PRIMARY KEY' : '';
|
||||
return ` ${cl.name.toUpperCase()} ${cl.type}${length} ${incr} ${pri} ${defVal} ${cl.notNull ? 'NOT NULL' : ''} `;
|
||||
let incr = cl.auto_increment && create ? '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}`;
|
||||
return incr ? baseSql : ` ${baseSql} ${defVal} ${cl.notNull ? 'NOT NULL' : ''} `;
|
||||
}
|
||||
|
||||
getCreateTableSql(data: any): string {
|
||||
let schemaArr = data.db.split('/');
|
||||
let schema = schemaArr.length > 1 ? schemaArr[schemaArr.length - 1] : schemaArr[0];
|
||||
let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(data.tableName)}`;
|
||||
|
||||
let createSql = '';
|
||||
let tableCommentSql = '';
|
||||
let columCommentSql = '';
|
||||
@@ -285,17 +314,17 @@ class OracleDialect implements DbDialect {
|
||||
// 创建表结构
|
||||
let fields: string[] = [];
|
||||
data.fields.res.forEach((item: any) => {
|
||||
item.name && fields.push(this.genColumnBasicSql(item));
|
||||
item.name && fields.push(this.genColumnBasicSql(item, true));
|
||||
// 列注释
|
||||
if (item.remark) {
|
||||
columCommentSql += ` comment on column ${data.tableName?.toUpperCase()}.${item.name?.toUpperCase()} is '${item.remark}'; `;
|
||||
columCommentSql += ` COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(item.name)} is '${item.remark}'; `;
|
||||
}
|
||||
});
|
||||
// 建表
|
||||
createSql = `CREATE TABLE ${data.tableName?.toUpperCase()} ( ${fields.join(',')} );`;
|
||||
createSql = `CREATE TABLE ${dbTable} ( ${fields.join(',')} );`;
|
||||
// 表注释
|
||||
if (data.tableComment) {
|
||||
tableCommentSql = ` comment on table ${data.tableName?.toUpperCase()} is '${data.tableComment}'; `;
|
||||
tableCommentSql = ` COMMENT ON TABLE ${dbTable} is '${data.tableComment}'; `;
|
||||
}
|
||||
|
||||
return createSql + tableCommentSql + columCommentSql;
|
||||
@@ -304,43 +333,95 @@ class OracleDialect implements DbDialect {
|
||||
getCreateIndexSql(tableData: any): string {
|
||||
// CREATE UNIQUE INDEX idx_column_name ON your_table (column1, column2);
|
||||
// COMMENT ON INDEX idx_column_name IS 'Your index comment here';
|
||||
// 创建索引
|
||||
|
||||
let schemaArr = tableData.db.split('/');
|
||||
let schema = schemaArr.length > 1 ? schemaArr[schemaArr.length - 1] : schemaArr[0];
|
||||
let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableData.tableName)}`;
|
||||
|
||||
let sql: string[] = [];
|
||||
tableData.indexs.res.forEach((a: any) => {
|
||||
sql.push(` CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${a.indexName} ON "${tableData.tableName}" ("${a.columnNames.join('","')})"`);
|
||||
sql.push(` CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${a.indexName} ON ${dbTable} ("${a.columnNames.join('","')})"`);
|
||||
});
|
||||
return sql.join(';');
|
||||
}
|
||||
|
||||
getModifyColumnSql(tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
|
||||
let sql: string[] = [];
|
||||
if (changeData.add.length > 0) {
|
||||
changeData.add.forEach((a) => {
|
||||
sql.push(`ALTER TABLE "${tableName}" add COLUMN ${this.genColumnBasicSql(a)}`);
|
||||
if (a.remark) {
|
||||
sql.push(`comment on COLUMN "${tableName}"."${a.name}" is '${a.remark}'`);
|
||||
getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
|
||||
let schemaArr = tableData.db.split('/');
|
||||
let schema = schemaArr.length > 1 ? schemaArr[schemaArr.length - 1] : schemaArr[0];
|
||||
let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableName)}`;
|
||||
|
||||
let baseSql = `ALTER TABLE ${dbTable} `;
|
||||
|
||||
let modifyArr: string[] = [];
|
||||
let dropArr: string[] = [];
|
||||
// 重命名的sql要一条条执行
|
||||
let renameArr: string[] = [];
|
||||
let commentArr: string[] = [];
|
||||
|
||||
// 主键字段
|
||||
let priArr = new Set();
|
||||
|
||||
if (changeData.upd.length > 0) {
|
||||
changeData.upd.forEach((a) => {
|
||||
let commentSql = `COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(a.name)} IS '${a.remark}'`;
|
||||
if (a.remark && a.oldName === a.name) {
|
||||
commentArr.push(commentSql);
|
||||
}
|
||||
// 修改了字段名
|
||||
if (a.oldName !== a.name) {
|
||||
renameArr.push(baseSql + ` RENAME COLUMN ${this.quoteIdentifier(a.oldName!)} TO ${this.quoteIdentifier(a.name)} ;`);
|
||||
if (a.remark) {
|
||||
commentArr.push(commentSql);
|
||||
}
|
||||
}
|
||||
modifyArr.push(` MODIFY (${this.genColumnBasicSql(a, false)})`);
|
||||
if (a.pri) {
|
||||
priArr.add(`${this.quoteIdentifier(a.name)}"`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (changeData.upd.length > 0) {
|
||||
changeData.upd.forEach((a) => {
|
||||
sql.push(`ALTER TABLE "${tableName}" MODIFY ${this.genColumnBasicSql(a)}`);
|
||||
if (changeData.add.length > 0) {
|
||||
changeData.add.forEach((a) => {
|
||||
modifyArr.push(` ADD (${this.genColumnBasicSql(a, false)})`);
|
||||
if (a.remark) {
|
||||
sql.push(`comment on COLUMN "${tableName}"."${a.name}" is '${a.remark}'`);
|
||||
commentArr.push(`COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(a.name)} is '${a.remark}'`);
|
||||
}
|
||||
if (a.pri) {
|
||||
priArr.add(`"${a.name}"`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (changeData.del.length > 0) {
|
||||
changeData.del.forEach((a) => {
|
||||
sql.push(`ALTER TABLE "${tableName}" DROP COLUMN ${a.name}`);
|
||||
dropArr.push(`${this.quoteIdentifier(a.name)}`);
|
||||
});
|
||||
}
|
||||
return sql.join(';');
|
||||
|
||||
let dropPkSql = '';
|
||||
if (priArr.size > 0) {
|
||||
let resPri = tableData.fields.res.find((a: RowDefinition) => a.pri);
|
||||
if (resPri) {
|
||||
priArr.add(`"${resPri.name}"`);
|
||||
}
|
||||
// 如果有编辑主键字段,则删除主键,再添加主键
|
||||
// 解析表字段中是否含有主键,有的话就删除主键
|
||||
if (tableData.fields.oldFields.find((a: RowDefinition) => a.pri)) {
|
||||
dropPkSql = `ALTER TABLE ${dbTable} DROP PRIMARY KEY;`;
|
||||
}
|
||||
}
|
||||
|
||||
let modifySql = baseSql + modifyArr.join(' ') + ';';
|
||||
let dropSql = baseSql + ` DROP (${dropArr.join(',')}) ;`;
|
||||
let renameSql = renameArr.join('');
|
||||
let addPkSql = priArr.size > 0 ? `ALTER TABLE ${dbTable} ADD CONSTRAINT "PK_${tableName}" PRIMARY KEY (${Array.from(priArr).join(',')});` : '';
|
||||
let commentSql = commentArr.join(';');
|
||||
|
||||
return dropPkSql + modifySql + dropSql + renameSql + addPkSql + commentSql;
|
||||
}
|
||||
|
||||
getModifyIndexSql(tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
|
||||
getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
|
||||
// 不能直接修改索引名或字段、需要先删后加
|
||||
let dropIndexNames: string[] = [];
|
||||
let addIndexs: any[] = [];
|
||||
|
||||
@@ -123,6 +123,7 @@ class PostgresqlDialect implements DbDialect {
|
||||
};
|
||||
|
||||
pgDialectInfo = {
|
||||
name: 'PostgreSQL',
|
||||
icon: 'iconfont icon-op-postgres',
|
||||
defaultPort: 5432,
|
||||
formatSqlDialect: 'postgresql',
|
||||
@@ -132,7 +133,7 @@ class PostgresqlDialect implements DbDialect {
|
||||
return pgDialectInfo;
|
||||
}
|
||||
|
||||
getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
|
||||
getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
|
||||
return `SELECT * FROM ${this.quoteIdentifier(table)} ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''} ${this.getPageSql(
|
||||
pageNum,
|
||||
limit
|
||||
@@ -228,7 +229,7 @@ class PostgresqlDialect implements DbDialect {
|
||||
let marks = false;
|
||||
if (this.matchType(cl.type, ['char', 'time', 'date', 'text'])) {
|
||||
// 默认值是now()的time或date不需要加引号
|
||||
if (cl.value.toLowerCase().replace(' ', '') === 'current_timestamp' && this.matchType(cl.type, ['time', 'date'])) {
|
||||
if (['pg_systimestamp()', 'current_timestamp'].includes(cl.value.toLowerCase()) && this.matchType(cl.type, ['time', 'date'])) {
|
||||
marks = false;
|
||||
} else {
|
||||
marks = true;
|
||||
@@ -260,7 +261,10 @@ class PostgresqlDialect implements DbDialect {
|
||||
let length = this.getTypeLengthSql(cl);
|
||||
// 默认值
|
||||
let defVal = this.getDefaultValueSql(cl);
|
||||
return ` ${cl.name} ${cl.type}${length} ${cl.notNull ? 'NOT NULL' : ''} ${defVal} `;
|
||||
// 如果有原名以原名为准
|
||||
let name = cl.oldName && cl.name !== cl.oldName ? cl.oldName : cl.name;
|
||||
|
||||
return ` ${this.quoteIdentifier(name)} ${cl.type}${length} ${cl.notNull ? 'NOT NULL' : ''} ${defVal} `;
|
||||
}
|
||||
|
||||
getCreateTableSql(data: any): string {
|
||||
@@ -301,7 +305,7 @@ class PostgresqlDialect implements DbDialect {
|
||||
// 创建索引
|
||||
let sql: string[] = [];
|
||||
tableData.indexs.res.forEach((a: any) => {
|
||||
sql.push(` CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${a.indexName} USING btree ("${a.columnNames.join('","')})"`);
|
||||
sql.push(` create ${a.unique ? 'UNIQUE' : ''} index ${a.indexName} ("${a.columnNames.join('","')})"`);
|
||||
if (a.indexComment) {
|
||||
sql.push(`COMMENT ON INDEX ${a.indexName} IS '${a.indexComment}'`);
|
||||
}
|
||||
@@ -309,42 +313,60 @@ class PostgresqlDialect implements DbDialect {
|
||||
return sql.join(';');
|
||||
}
|
||||
|
||||
getModifyColumnSql(tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
|
||||
let sql: string[] = [];
|
||||
getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
|
||||
let schemaArr = tableData.db.split('/');
|
||||
let schema = schemaArr.length > 1 ? schemaArr[schemaArr.length - 1] : schemaArr[0];
|
||||
let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableName)}`;
|
||||
|
||||
let dropPkSql = '';
|
||||
let modifySql = '';
|
||||
let dropSql = '';
|
||||
let renameSql = '';
|
||||
let addPkSql = '';
|
||||
let commentSql = '';
|
||||
|
||||
if (changeData.add.length > 0) {
|
||||
changeData.add.forEach((a) => {
|
||||
let typeLength = this.getTypeLengthSql(a);
|
||||
let defaultSql = this.getDefaultValueSql(a);
|
||||
sql.push(`ALTER TABLE ${tableName} add ${a.name} ${a.type}${typeLength} ${defaultSql}`);
|
||||
modifySql += `alter table ${dbTable} add ${this.genColumnBasicSql(a)};`;
|
||||
if (a.remark) {
|
||||
sql.push(`comment on column "${tableName}"."${a.name}" is '${a.remark}'`);
|
||||
commentSql += `comment on column ${dbTable}.${this.quoteIdentifier(a.name)} is '${a.remark}';`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (changeData.upd.length > 0) {
|
||||
changeData.upd.forEach((a) => {
|
||||
let cmtSql = `comment on column ${dbTable}.${this.quoteIdentifier(a.name)} is '${a.remark}';`;
|
||||
if (a.remark && a.oldName === a.name) {
|
||||
commentSql += cmtSql;
|
||||
}
|
||||
// 修改了字段名
|
||||
if (a.oldName !== a.name) {
|
||||
renameSql += `alter table ${dbTable} rename column ${this.quoteIdentifier(a.oldName!)} to ${this.quoteIdentifier(a.name)};`;
|
||||
if (a.remark) {
|
||||
commentSql += cmtSql;
|
||||
}
|
||||
}
|
||||
let typeLength = this.getTypeLengthSql(a);
|
||||
sql.push(`ALTER TABLE ${tableName} alter column ${a.name} type ${a.type}${typeLength}`);
|
||||
// 如果有原名以原名为准
|
||||
let name = a.oldName && a.name !== a.oldName ? a.oldName : a.name;
|
||||
modifySql += `alter table ${dbTable} alter column ${this.quoteIdentifier(name)} type ${a.type}${typeLength} ;`;
|
||||
let defaultSql = this.getDefaultValueSql(a);
|
||||
if (defaultSql) {
|
||||
sql.push(`alter table ${tableName} alter column ${a.name} set ${defaultSql}`);
|
||||
}
|
||||
if (a.remark) {
|
||||
sql.push(`comment on column "${tableName}"."${a.name}" is '${a.remark}'`);
|
||||
modifySql += `alter table ${dbTable} alter column ${this.quoteIdentifier(name)} set ${defaultSql} ;`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (changeData.del.length > 0) {
|
||||
changeData.del.forEach((a) => {
|
||||
sql.push(`ALTER TABLE ${tableName} DROP COLUMN ${a.name}`);
|
||||
dropSql += `alter table ${dbTable} drop column ${a.name};`;
|
||||
});
|
||||
}
|
||||
return sql.join(';');
|
||||
return dropPkSql + modifySql + dropSql + renameSql + addPkSql + commentSql;
|
||||
}
|
||||
|
||||
getModifyIndexSql(tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
|
||||
getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
|
||||
// 不能直接修改索引名或字段、需要先删后加
|
||||
let dropIndexNames: string[] = [];
|
||||
let addIndexs: any[] = [];
|
||||
|
||||
338
mayfly_go_web/src/views/ops/db/dialect/sqlite_dialect.ts
Normal file
338
mayfly_go_web/src/views/ops/db/dialect/sqlite_dialect.ts
Normal file
@@ -0,0 +1,338 @@
|
||||
import {
|
||||
commonCustomKeywords,
|
||||
DataType,
|
||||
DbDialect,
|
||||
DialectInfo,
|
||||
EditorCompletion,
|
||||
EditorCompletionItem,
|
||||
IndexDefinition,
|
||||
RowDefinition,
|
||||
sqlColumnType,
|
||||
} from './index';
|
||||
import { DbInst } from '@/views/ops/db/db';
|
||||
import { language as sqlLanguage } from 'monaco-editor/esm/vs/basic-languages/sql/sql.js';
|
||||
|
||||
export { SqliteDialect };
|
||||
|
||||
// 参考官方文档:https://www.sqlite.org/datatype3.html
|
||||
const SQLITE_TYPE_LIST: sqlColumnType[] = [
|
||||
// INTEGER
|
||||
{ udtName: 'int', dataType: 'int', desc: '', space: '', range: '' },
|
||||
{ udtName: 'integer', dataType: 'integer', desc: '', space: '', range: '' },
|
||||
{ udtName: 'tinyint', dataType: 'tinyint', desc: '', space: '', range: '' },
|
||||
{ udtName: 'smallint', dataType: 'smallint', desc: '', space: '', range: '' },
|
||||
{ udtName: 'mediumint', dataType: 'mediumint', desc: '', space: '', range: '' },
|
||||
{ udtName: 'bigint', dataType: 'bigint', desc: '', space: '', range: '' },
|
||||
{ udtName: 'unsigned big int', dataType: 'unsigned big int', desc: '', space: '', range: '' },
|
||||
{ udtName: 'int2', dataType: 'int2', desc: '', space: '', range: '' },
|
||||
{ udtName: 'int8', dataType: 'int8', desc: '', space: '', range: '' },
|
||||
// TEXT
|
||||
{ udtName: 'character', dataType: 'character', desc: '', space: '', range: '' },
|
||||
{ udtName: 'varchar', dataType: 'varchar', desc: '', space: '', range: '' },
|
||||
{ udtName: 'varying character', dataType: 'varying character', desc: '', space: '', range: '' },
|
||||
{ udtName: 'nchar', dataType: 'nchar', desc: '', space: '', range: '' },
|
||||
{ udtName: 'native character', dataType: 'native character', desc: '', space: '', range: '' },
|
||||
{ udtName: 'nvarchar', dataType: 'nvarchar', desc: '', space: '', range: '' },
|
||||
{ udtName: 'text', dataType: 'text', desc: '', space: '', range: '' },
|
||||
{ udtName: 'clob', dataType: 'clob', desc: '', space: '', range: '' },
|
||||
// blob
|
||||
{ udtName: 'blob', dataType: 'blob', desc: '', space: '', range: '' },
|
||||
{ udtName: 'no datatype specified', dataType: 'no datatype specified', desc: '', space: '', range: '' },
|
||||
// REAL
|
||||
{ udtName: 'real', dataType: 'real', desc: '', space: '', range: '' },
|
||||
{ udtName: 'double', dataType: 'double', desc: '', space: '', range: '' },
|
||||
{ udtName: 'double precision', dataType: 'double precision', desc: '', space: '', range: '' },
|
||||
{ udtName: 'float', dataType: 'float', desc: '', space: '', range: '' },
|
||||
// NUMERIC
|
||||
{ udtName: 'numeric', dataType: 'numeric', desc: '', space: '', range: '' },
|
||||
{ udtName: 'decimal', dataType: 'decimal', desc: '', space: '', range: '' },
|
||||
{ udtName: 'boolean', dataType: 'boolean', desc: '', space: '', range: '' },
|
||||
{ udtName: 'date', dataType: 'date', desc: '', space: '', range: '' },
|
||||
{ udtName: 'datetime', dataType: 'datetime', desc: '', space: '', range: '' },
|
||||
];
|
||||
|
||||
const addCustomKeywords = ['PRAGMA', 'database_list', 'sqlite_master'];
|
||||
|
||||
// 参考官方文档:https://www.sqlite.org/lang_corefunc.html
|
||||
const functions: EditorCompletionItem[] = [
|
||||
// 字符函数
|
||||
{ label: 'abs', insertText: 'abs(X)', description: '返回给定数值的绝对值' },
|
||||
{ label: 'changes', insertText: 'changes()', description: '返回最近增删改影响的行数' },
|
||||
{ label: 'coalesce', insertText: 'coalesce(X,Y,...)', description: '返回第一个不为空的值' },
|
||||
{ label: 'hex', insertText: 'hex(X)', description: '返回给定字符的hex值' },
|
||||
{ label: 'ifnull', insertText: 'ifnull(X,Y)', description: '返回第一个不为空的值' },
|
||||
{ label: 'iif', insertText: 'iif(X,Y,Z)', description: '如果x为真则返回y,否则返回z' },
|
||||
{ label: 'instr', insertText: 'instr(X,Y)', description: '返回字符y在x的第n个位置' },
|
||||
{ label: 'length', insertText: 'length(X)', description: '返回给定字符的长度' },
|
||||
{ label: 'load_extension', insertText: 'load_extension(X[,Y])', description: '加载扩展块' },
|
||||
{ label: 'lower', insertText: 'lower(X)', description: '返回小写字符' },
|
||||
{ label: 'ltrim', insertText: 'ltrim(X[,Y])', description: '左trim' },
|
||||
{ label: 'nullif', insertText: 'nullif(X,Y)', description: '比较两值相等则返回null,否则返回第一个值' },
|
||||
{ label: 'printf', insertText: "printf('%s',...)", description: '字符串格式化拼接,如%s %d' },
|
||||
{ label: 'quote', insertText: 'quote(X)', description: '把字符串用引号包起来' },
|
||||
{ label: 'random', insertText: 'random()', description: '生成随机数' },
|
||||
{ label: 'randomblob', insertText: 'randomblob(N)', description: '生成一个包含N个随机字节的BLOB' },
|
||||
{ label: 'replace', insertText: 'replace(X,Y,Z)', description: '替换字符串' },
|
||||
{ label: 'round', insertText: 'round(X[,Y])', description: '将数值四舍五入到指定的小数位数' },
|
||||
{ label: 'rtrim', insertText: 'rtrim(X[,Y])', description: '右trim' },
|
||||
{ label: 'sign', insertText: 'sign(X)', description: '返回数字符号 1正 -1负 0零 null' },
|
||||
{ label: 'soundex', insertText: 'soundex(X)', description: '返回字符串X的soundex编码字符串' },
|
||||
{ label: 'sqlite_compileoption_get', insertText: 'sqlite_compileoption_get(N)', description: '获取指定编译选项的值' },
|
||||
{ label: 'sqlite_compileoption_used', insertText: 'sqlite_compileoption_used(X)', description: '检查SQLite编译时是否使用了指定的编译选项' },
|
||||
{ label: 'sqlite_source_id', insertText: 'sqlite_source_id()', description: '获取sqlite源代码标识符' },
|
||||
{ label: 'sqlite_version', insertText: 'sqlite_version()', description: '获取sqlite版本' },
|
||||
{ label: 'substr', insertText: 'substr(X,Y[,Z])', description: '截取字符串' },
|
||||
{ label: 'substring', insertText: 'substring(X,Y[,Z])', description: '截取字符串' },
|
||||
{ label: 'trim', insertText: 'trim(X[,Y])', description: '去除给定字符串前后的字符,默认空格' },
|
||||
{ label: 'typeof', insertText: 'typeof(X)', description: '返回X的基本类型:null,integer,real,text,blob' },
|
||||
{ label: 'unicode', insertText: 'unicode(X)', description: '返回与字符串X的第一个字符相对应的数字unicode代码点' },
|
||||
{ label: 'unlikely', insertText: 'unlikely(X)', description: '返回大写字符' },
|
||||
{ label: 'upper', insertText: 'upper(X)', description: '返回由0x00的N个字节组成的BLOB' },
|
||||
{ label: 'zeroblob', insertText: 'zeroblob(N)', description: '返回分组中的平均值' },
|
||||
{ label: 'avg', insertText: 'avg(X)', description: '返回总条数' },
|
||||
{ label: 'count', insertText: 'count(*)', description: '返回分组中用给定非空字符串连接的值' },
|
||||
{ label: 'group_concat', insertText: 'group_concat(X[,Y])', description: '返回分组中最大值' },
|
||||
{ label: 'max', insertText: 'max(X)', description: '返回分组中最小值' },
|
||||
{ label: 'min', insertText: 'min(X)', description: '返回分组中非空值的总和。' },
|
||||
{ label: 'sum', insertText: 'sum(X)', description: '返回分组中非空值的总和。' },
|
||||
{ label: 'total', insertText: 'total(X)', description: '返回YYYY-MM-DD格式的字符串' },
|
||||
{ label: 'date', insertText: 'date(time-value[, modifier, ...])', description: '返回HH:MM:SS格式的字符串' },
|
||||
{ label: 'time', insertText: 'time(time-value[, modifier, ...])', description: '将日期和时间字符串转换为特定的日期和时间格式' },
|
||||
{ label: 'datetime', insertText: 'datetime(time-value[, modifier, ...])', description: '计算日期和时间的儒略日数' },
|
||||
{ label: 'julianday', insertText: 'julianday(time-value[, modifier, ...])', description: '将日期和时间格式化为指定的字符串' },
|
||||
];
|
||||
|
||||
let sqliteDialectInfo: DialectInfo;
|
||||
class SqliteDialect implements DbDialect {
|
||||
getInfo(): DialectInfo {
|
||||
if (sqliteDialectInfo) {
|
||||
return sqliteDialectInfo;
|
||||
}
|
||||
|
||||
let { keywords, operators, builtinVariables } = sqlLanguage;
|
||||
|
||||
let editorCompletions: EditorCompletion = {
|
||||
keywords: keywords
|
||||
.filter((a: string) => addCustomKeywords.indexOf(a) === -1)
|
||||
.map((a: string): EditorCompletionItem => ({ label: a, description: 'keyword' }))
|
||||
.concat(commonCustomKeywords.map((a): EditorCompletionItem => ({ label: a, description: 'keyword' })))
|
||||
.concat(addCustomKeywords.map((a): EditorCompletionItem => ({ label: a, description: 'keyword' }))),
|
||||
operators: operators.map((a: string): EditorCompletionItem => ({ label: a, description: 'operator' })),
|
||||
functions,
|
||||
variables: builtinVariables.map((a: string): EditorCompletionItem => ({ label: a, description: 'var' })),
|
||||
};
|
||||
|
||||
sqliteDialectInfo = {
|
||||
name: 'Sqlite',
|
||||
icon: 'iconfont icon-sqlite',
|
||||
defaultPort: 0,
|
||||
formatSqlDialect: 'sql',
|
||||
columnTypes: SQLITE_TYPE_LIST.sort((a, b) => a.udtName.localeCompare(b.udtName)),
|
||||
editorCompletions,
|
||||
};
|
||||
return sqliteDialectInfo;
|
||||
}
|
||||
|
||||
getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
|
||||
return `SELECT * FROM ${this.quoteIdentifier(table)} ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''} ${this.getPageSql(
|
||||
pageNum,
|
||||
limit
|
||||
)};`;
|
||||
}
|
||||
|
||||
getPageSql(pageNum: number, limit: number) {
|
||||
return ` LIMIT ${(pageNum - 1) * limit}, ${limit}`;
|
||||
}
|
||||
|
||||
getDefaultRows(): RowDefinition[] {
|
||||
return [
|
||||
{ name: 'id', type: 'integer', length: '', numScale: '', value: '', notNull: true, pri: true, auto_increment: true, remark: '主键ID' },
|
||||
{ name: 'creator_id', type: 'bigint', length: '20', numScale: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '创建人id' },
|
||||
{
|
||||
name: 'creator',
|
||||
type: 'varchar',
|
||||
length: '100',
|
||||
numScale: '',
|
||||
value: '',
|
||||
notNull: true,
|
||||
pri: false,
|
||||
auto_increment: false,
|
||||
remark: '创建人姓名',
|
||||
},
|
||||
{
|
||||
name: 'create_time',
|
||||
type: 'datetime',
|
||||
length: '',
|
||||
numScale: '',
|
||||
value: 'CURRENT_TIMESTAMP',
|
||||
notNull: true,
|
||||
pri: false,
|
||||
auto_increment: false,
|
||||
remark: '创建时间',
|
||||
},
|
||||
{ name: 'updator_id', type: 'bigint', length: '20', numScale: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '修改人id' },
|
||||
{ name: 'updator', type: 'varchar', length: '100', numScale: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '修改姓名' },
|
||||
{
|
||||
name: 'update_time',
|
||||
type: 'datetime',
|
||||
length: '',
|
||||
numScale: '',
|
||||
value: 'CURRENT_TIMESTAMP',
|
||||
notNull: true,
|
||||
pri: false,
|
||||
auto_increment: false,
|
||||
remark: '修改时间',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
getDefaultIndex(): IndexDefinition {
|
||||
return {
|
||||
indexName: '',
|
||||
columnNames: [],
|
||||
unique: false,
|
||||
indexType: 'BTREE',
|
||||
indexComment: '',
|
||||
};
|
||||
}
|
||||
|
||||
quoteIdentifier = (name: string) => {
|
||||
return `\"${name}\"`;
|
||||
};
|
||||
|
||||
genColumnBasicSql(cl: any): string {
|
||||
let val = cl.value ? (cl.value === 'CURRENT_TIMESTAMP' ? cl.value : `'${cl.value}'`) : '';
|
||||
let defVal = val ? `DEFAULT ${val}` : '';
|
||||
let length = cl.length ? `(${cl.length})` : '';
|
||||
let nullAble = cl.notNull ? 'NOT NULL' : 'NULL';
|
||||
if (cl.pri) {
|
||||
return ` ${this.quoteIdentifier(cl.name)} ${cl.type}${length} PRIMARY KEY ${cl.auto_increment ? 'AUTOINCREMENT' : ''} ${nullAble} `;
|
||||
}
|
||||
return ` ${this.quoteIdentifier(cl.name)} ${cl.type}${length} ${nullAble} ${defVal} `;
|
||||
}
|
||||
getCreateTableSql(data: any): string {
|
||||
// 创建表结构
|
||||
let fields: string[] = [];
|
||||
data.fields.res.forEach((item: any) => {
|
||||
item.name && fields.push(this.genColumnBasicSql(item));
|
||||
});
|
||||
|
||||
return `CREATE TABLE ${this.quoteIdentifier(data.db)}.${this.quoteIdentifier(data.tableName)}
|
||||
( ${fields.join(',')} )`;
|
||||
}
|
||||
|
||||
getCreateIndexSql(data: any): string {
|
||||
// 创建索引
|
||||
let sql = [] as string[];
|
||||
data.indexs.res.forEach((a: any) => {
|
||||
sql.push(
|
||||
`CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${this.quoteIdentifier(data.db)}.${this.quoteIdentifier(a.indexName)} ON "${data.tableName}" (${a.columnNames.join(',')})`
|
||||
);
|
||||
});
|
||||
return sql.join(';');
|
||||
}
|
||||
|
||||
getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
|
||||
// sqlite修改表结构需要先删除再创建
|
||||
|
||||
// 1.删除旧表索引 DROP INDEX "main"."aa";
|
||||
let sql = [] as string[];
|
||||
tableData.indexs.res.forEach((a: any) => {
|
||||
a.indexName && sql.push(`DROP INDEX ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(a.indexName)}`);
|
||||
});
|
||||
|
||||
// 2.重命名表,备份旧表 ALTER TABLE "main"."t_sys_resource" RENAME TO "_t_sys_resource_old_20240118162712"; new Date().getTime()
|
||||
let oldTableName = `_${tableName}_old_${new Date().getTime()}`;
|
||||
sql.push(`ALTER TABLE ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(tableName)} RENAME TO ${this.quoteIdentifier(oldTableName)}`);
|
||||
|
||||
// 3.创建新表
|
||||
sql.push(this.getCreateTableSql(tableData));
|
||||
|
||||
// 4.复制数据 INSERT INTO "库名"."新表名" (${insertFields}) SELECT ${queryFields} FROM "库名"."旧表名";
|
||||
// 查询的字段数据类型和数量应与插入的字段一致
|
||||
// 判断哪些字段需要查询旧表,哪些字段需要插入新表
|
||||
// 解析changeData,统计需要查询旧表的字段,统计需要插入新表的字段
|
||||
let delFields = changeData.del.map((a) => a.name);
|
||||
let addFields = changeData.add.map((a) => a.name);
|
||||
|
||||
let queryFields = [] as string[];
|
||||
let insertFields = [] as string[];
|
||||
tableData.fields.res.forEach((a: any) => {
|
||||
// 新增、删除的字段不需要查询旧表,不需要插入新表
|
||||
if (addFields.includes(a.name) || delFields.includes(a.name)) {
|
||||
return;
|
||||
}
|
||||
// 修改的字段需要查询和插入,判断是否修改了字段名,如果修改了字段名,需要查询旧表原名,插入新表新名
|
||||
// 其余未删除、未修改的字段,需要查询旧表,插入新表
|
||||
queryFields.push(this.quoteIdentifier(a.name === a.oldName ? a.name : a.oldName));
|
||||
insertFields.push(this.quoteIdentifier(a.name));
|
||||
});
|
||||
// 生成sql
|
||||
sql.push(
|
||||
`INSERT INTO ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(tableName)} (${insertFields.join(',')}) SELECT ${queryFields.join(
|
||||
','
|
||||
)} FROM ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(oldTableName)}`
|
||||
);
|
||||
|
||||
// 5.创建索引
|
||||
tableData.indexs.res.forEach((a: any) => {
|
||||
a.indexName &&
|
||||
sql.push(
|
||||
`CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(a.indexName)} ON "${tableName}" (${a.columnNames.join(',')})`
|
||||
);
|
||||
});
|
||||
|
||||
return sql.join(';') + ';';
|
||||
}
|
||||
|
||||
getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
|
||||
// sqlite创建索引需要先删除再创建
|
||||
// CREATE INDEX "main"."aa1" ON "t_sys_resource" ( "ui_path" );
|
||||
|
||||
let sql = [] as string[];
|
||||
|
||||
if (changeData.del.length > 0) {
|
||||
changeData.del.forEach((a) => {
|
||||
sql.push(`DROP INDEX ${this.quoteIdentifier(a.indexName)}`);
|
||||
});
|
||||
}
|
||||
|
||||
let indexData = [] as any[];
|
||||
if (changeData.add.length > 0) {
|
||||
indexData = indexData.concat(changeData.add);
|
||||
}
|
||||
if (changeData.upd.length > 0) {
|
||||
indexData = indexData.concat(changeData.upd);
|
||||
}
|
||||
|
||||
if (indexData.length > 0) {
|
||||
indexData.forEach((a) => {
|
||||
sql.push(`CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${this.quoteIdentifier(a.indexName)} ON ${tableName} (${a.columnNames.join(',')})`);
|
||||
});
|
||||
}
|
||||
return sql.join(';');
|
||||
}
|
||||
|
||||
getDataType(columnType: string): DataType {
|
||||
if (DbInst.isNumber(columnType)) {
|
||||
return DataType.Number;
|
||||
}
|
||||
// 日期时间类型
|
||||
if (/datetime|timestamp/gi.test(columnType)) {
|
||||
return DataType.DateTime;
|
||||
}
|
||||
// 日期类型
|
||||
if (/date/gi.test(columnType)) {
|
||||
return DataType.Date;
|
||||
}
|
||||
// 时间类型
|
||||
if (/time/gi.test(columnType)) {
|
||||
return DataType.Time;
|
||||
}
|
||||
return DataType.String;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
|
||||
wrapStrValue(columnType: string, value: string): string {
|
||||
return `'${value}'`;
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,15 @@
|
||||
<el-input v-model="state.keySeparator" placeholder="分割符" size="small" class="ml5" />
|
||||
</el-col>
|
||||
<el-col :span="18">
|
||||
<el-input @clear="clear" v-model="scanParam.match" placeholder="match 支持*模糊key" clearable size="small" class="ml10" />
|
||||
<el-input
|
||||
@clear="clear"
|
||||
v-model="scanParam.match"
|
||||
@keyup.enter.native="searchKey()"
|
||||
placeholder="match 支持*模糊key, 回车搜索"
|
||||
clearable
|
||||
size="small"
|
||||
class="ml10"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-button
|
||||
|
||||
@@ -107,10 +107,10 @@ defineExpose({ getContent });
|
||||
|
||||
.format-viewer-container .el-textarea textarea {
|
||||
font-size: 14px;
|
||||
height: calc(100vh - 546px + v-bind(height));
|
||||
height: calc(100vh - 550px + v-bind(height));
|
||||
}
|
||||
|
||||
.format-viewer-container .monaco-editor-content {
|
||||
height: calc(100vh - 560px + v-bind(height)) !important;
|
||||
height: calc(100vh - 565px + v-bind(height)) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-button @click="showEditDialog(null)" icon="plus" size="small" plain type="primary" class="mb10">添加新行</el-button>
|
||||
<el-table size="small" border :data="hashValues" height="450" min-height="300" stripe>
|
||||
<el-table size="small" border :data="hashValues" height="500" min-height="300" stripe>
|
||||
<el-table-column type="index" :label="'ID (Total: ' + total + ')'" sortable width="100"> </el-table-column>
|
||||
<el-table-column resizable sortable prop="field" label="field" show-overflow-tooltip min-width="100"> </el-table-column>
|
||||
<el-table-column resizable sortable prop="value" label="value" show-overflow-tooltip min-width="200"> </el-table-column>
|
||||
@@ -11,7 +11,7 @@
|
||||
class="key-detail-filter-value"
|
||||
v-model="state.filterValue"
|
||||
@keyup.enter="hscan(true, true)"
|
||||
placeholder="输入关键词回车搜索"
|
||||
placeholder="关键词回车搜索"
|
||||
clearable
|
||||
size="small"
|
||||
/>
|
||||
@@ -51,7 +51,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, reactive, watch, toRefs } from 'vue';
|
||||
import { ref, onMounted, reactive, toRefs } from 'vue';
|
||||
import { redisApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { notBlank } from '@/common/assert';
|
||||
|
||||
@@ -142,7 +142,7 @@ const search = async () => {
|
||||
|
||||
const changeStatus = async (row: any) => {
|
||||
let id = row.id;
|
||||
let status = row.status == -1 ? 1 : -1;
|
||||
let status = row.status == AccountStatusEnum.Disable.value ? AccountStatusEnum.Enable.value : AccountStatusEnum.Disable.value;
|
||||
await accountApi.changeStatus.request({
|
||||
id,
|
||||
status,
|
||||
|
||||
@@ -2,6 +2,7 @@ import vue from '@vitejs/plugin-vue';
|
||||
import { resolve } from 'path';
|
||||
import type { UserConfig } from 'vite';
|
||||
import { loadEnv } from './src/common/utils/viteBuild';
|
||||
import { CodeInspectorPlugin } from 'code-inspector-plugin';
|
||||
|
||||
const pathResolve = (dir: string): any => {
|
||||
return resolve(__dirname, '.', dir);
|
||||
@@ -14,7 +15,12 @@ const alias: Record<string, string> = {
|
||||
};
|
||||
|
||||
const viteConfig: UserConfig = {
|
||||
plugins: [vue()],
|
||||
plugins: [
|
||||
vue(),
|
||||
CodeInspectorPlugin({
|
||||
bundler: 'vite',
|
||||
}),
|
||||
],
|
||||
root: process.cwd(),
|
||||
resolve: {
|
||||
alias,
|
||||
|
||||
@@ -37,22 +37,19 @@ sqlite:
|
||||
# password: 111049
|
||||
# db: 0
|
||||
log:
|
||||
# 日志等级, debug, info, warn, error
|
||||
# 日志等级, debug, info, warn, error
|
||||
level: info
|
||||
# 日志格式类型, text/json
|
||||
type: text
|
||||
# 是否记录方法调用栈信息
|
||||
add-source: false
|
||||
# 日志文件配置
|
||||
# file:
|
||||
# path: ./
|
||||
# path: ./log
|
||||
# name: mayfly-go.log
|
||||
db:
|
||||
backup-path: ./backup
|
||||
mysqlutil-path:
|
||||
mysql: ./mysqlutil/bin/mysql
|
||||
mysqldump: ./mysqlutil/bin/mysqldump
|
||||
mysqlbinlog: ./mysqlutil/bin/mysqlbinlog
|
||||
mariadbutil-path:
|
||||
mysql: ./mariadbutil/bin/mariadb
|
||||
mysqldump: ./mariadbutil/bin/mariadb-dump
|
||||
mysqlbinlog: ./mariadbutil/bin/mariadb-binlog
|
||||
# # 日志文件的最大大小(以兆字节为单位)。当日志文件大小达到该值时,将触发切割操作
|
||||
# max-size: 500
|
||||
# # 根据文件名中的时间戳,设置保留旧日志文件的最大天数
|
||||
# max-age: 60
|
||||
# # 是否使用 gzip 压缩方式压缩轮转后的日志文件
|
||||
# compress: true
|
||||
|
||||
@@ -10,31 +10,35 @@ require (
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/glebarez/sqlite v1.10.0
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.0
|
||||
github.com/go-ldap/ldap/v3 v3.4.5
|
||||
github.com/go-ldap/ldap/v3 v3.4.6
|
||||
github.com/go-playground/locales v0.14.1
|
||||
github.com/go-playground/universal-translator v0.18.1
|
||||
github.com/go-playground/validator/v10 v10.14.0
|
||||
github.com/go-sql-driver/mysql v1.7.1
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/google/uuid v1.3.1
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/kanzihuang/vitess/go/vt/sqlparser v0.0.0-20231018071450-ac8d9f0167e9
|
||||
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230712084735-068dc2aee82d
|
||||
github.com/microsoft/go-mssqldb v1.6.0
|
||||
github.com/mojocn/base64Captcha v1.3.6 // 验证码
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pkg/sftp v1.13.6
|
||||
github.com/pquerna/otp v1.4.0
|
||||
github.com/redis/go-redis/v9 v9.4.0
|
||||
github.com/robfig/cron/v3 v3.0.1 // 定时任务
|
||||
github.com/sijms/go-ora/v2 v2.8.5
|
||||
github.com/sijms/go-ora/v2 v2.8.7
|
||||
github.com/stretchr/testify v1.8.4
|
||||
go.mongodb.org/mongo-driver v1.13.1 // mongo
|
||||
golang.org/x/crypto v0.18.0 // ssh
|
||||
golang.org/x/oauth2 v0.15.0
|
||||
golang.org/x/sync v0.6.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
// gorm
|
||||
gorm.io/driver/mysql v1.5.2
|
||||
gorm.io/gorm v1.25.5
|
||||
gorm.io/gorm v1.25.6
|
||||
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -49,8 +53,10 @@ require (
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/golang/glog v1.0.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
@@ -79,10 +85,9 @@ require (
|
||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230519143937-03e91628a987
|
||||
golang.org/x/exp v0.0.0-20230519143937-03e91628a987 // indirect
|
||||
golang.org/x/image v0.13.0 // indirect
|
||||
golang.org/x/net v0.19.0 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
|
||||
@@ -1,11 +1,45 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
dbInit "mayfly-go/internal/db/init"
|
||||
machineInit "mayfly-go/internal/machine/init"
|
||||
"mayfly-go/pkg/biz"
|
||||
"mayfly-go/pkg/ioc"
|
||||
)
|
||||
|
||||
func InitOther() {
|
||||
machineInit.Init()
|
||||
dbInit.Init()
|
||||
// 初始化ioc函数
|
||||
type InitIocFunc func()
|
||||
|
||||
// 初始化函数
|
||||
type InitFunc func()
|
||||
|
||||
var (
|
||||
initIocFuncs = make([]InitIocFunc, 0)
|
||||
initFuncs = make([]InitFunc, 0)
|
||||
)
|
||||
|
||||
// 添加初始化ioc函数,由各个默认自行添加
|
||||
func AddInitIocFunc(initIocFunc InitIocFunc) {
|
||||
initIocFuncs = append(initIocFuncs, initIocFunc)
|
||||
}
|
||||
|
||||
// 添加初始化函数,由各个默认自行添加
|
||||
func AddInitFunc(initFunc InitFunc) {
|
||||
initFuncs = append(initFuncs, initFunc)
|
||||
}
|
||||
|
||||
// 系统启动时,调用各个模块的初始化函数
|
||||
func InitOther() {
|
||||
// 调用各个默认ioc组件注册初始化,优先调用ioc初始化注册函数和注入函数(可能在后续的InitFunc中需要用到依赖实例)
|
||||
for _, initIocFunc := range initIocFuncs {
|
||||
initIocFunc()
|
||||
}
|
||||
initIocFuncs = nil
|
||||
|
||||
// 为所有注册的实例注入其依赖的其他组件实例
|
||||
biz.ErrIsNil(ioc.InjectComponents())
|
||||
|
||||
// 调用各个默认的初始化函数
|
||||
for _, initFunc := range initFuncs {
|
||||
go initFunc()
|
||||
}
|
||||
initFuncs = nil
|
||||
}
|
||||
|
||||
@@ -3,15 +3,6 @@ package initialize
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
auth_router "mayfly-go/internal/auth/router"
|
||||
common_router "mayfly-go/internal/common/router"
|
||||
db_router "mayfly-go/internal/db/router"
|
||||
machine_router "mayfly-go/internal/machine/router"
|
||||
mongo_router "mayfly-go/internal/mongo/router"
|
||||
msg_router "mayfly-go/internal/msg/router"
|
||||
redis_router "mayfly-go/internal/redis/router"
|
||||
sys_router "mayfly-go/internal/sys/router"
|
||||
tag_router "mayfly-go/internal/tag/router"
|
||||
"mayfly-go/pkg/config"
|
||||
"mayfly-go/pkg/middleware"
|
||||
"mayfly-go/static"
|
||||
@@ -20,6 +11,18 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 初始化路由函数
|
||||
type InitRouterFunc func(router *gin.RouterGroup)
|
||||
|
||||
var (
|
||||
initRouterFuncs = make([]InitRouterFunc, 0)
|
||||
)
|
||||
|
||||
// 添加初始化路由函数,由各个默认自行添加
|
||||
func AddInitRouterFunc(initRouterFunc InitRouterFunc) {
|
||||
initRouterFuncs = append(initRouterFuncs, initRouterFunc)
|
||||
}
|
||||
|
||||
func InitRouter() *gin.Engine {
|
||||
// server配置
|
||||
serverConfig := config.Conf.Server
|
||||
@@ -43,20 +46,11 @@ func InitRouter() *gin.Engine {
|
||||
|
||||
// 设置路由组
|
||||
api := router.Group(serverConfig.ContextPath + "/api")
|
||||
{
|
||||
common_router.Init(api)
|
||||
|
||||
auth_router.Init(api)
|
||||
|
||||
sys_router.Init(api)
|
||||
msg_router.Init(api)
|
||||
|
||||
tag_router.Init(api)
|
||||
machine_router.Init(api)
|
||||
db_router.Init(api)
|
||||
redis_router.Init(api)
|
||||
mongo_router.Init(api)
|
||||
// 调用所有模块注册的初始化路由函数
|
||||
for _, initRouterFunc := range initRouterFuncs {
|
||||
initRouterFunc(api)
|
||||
}
|
||||
initRouterFuncs = nil
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
sysapp "mayfly-go/internal/sys/application"
|
||||
"mayfly-go/pkg/req"
|
||||
)
|
||||
|
||||
func InitSaveLogFunc() req.SaveLogFunc {
|
||||
return sysapp.GetSyslogApp().SaveFromReq
|
||||
}
|
||||
@@ -1,25 +1,20 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
dbApp "mayfly-go/internal/db/application"
|
||||
// 系统进程退出终止函数
|
||||
type TerminateFunc func()
|
||||
|
||||
var (
|
||||
terminateFuncs = make([]TerminateFunc, 0)
|
||||
)
|
||||
|
||||
// 终止服务后的一些操作
|
||||
func Terminate() {
|
||||
closeDbTasks()
|
||||
// 添加系统退出终止时执行的函数,由各个默认自行添加
|
||||
func AddTerminateFunc(terminateFunc TerminateFunc) {
|
||||
terminateFuncs = append(terminateFuncs, terminateFunc)
|
||||
}
|
||||
|
||||
func closeDbTasks() {
|
||||
restoreApp := dbApp.GetDbRestoreApp()
|
||||
if restoreApp != nil {
|
||||
restoreApp.Close()
|
||||
}
|
||||
binlogApp := dbApp.GetDbBinlogApp()
|
||||
if binlogApp != nil {
|
||||
binlogApp.Close()
|
||||
}
|
||||
backupApp := dbApp.GetDbBackupApp()
|
||||
if backupApp != nil {
|
||||
backupApp.Close()
|
||||
// 终止进程服务后的一些操作
|
||||
func Terminate() {
|
||||
for _, terminateFunc := range terminateFuncs {
|
||||
terminateFunc()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@ import (
|
||||
)
|
||||
|
||||
type AccountLogin struct {
|
||||
AccountApp sysapp.Account
|
||||
MsgApp msgapp.Msg
|
||||
AccountApp sysapp.Account `inject:""`
|
||||
MsgApp msgapp.Msg `inject:""`
|
||||
}
|
||||
|
||||
/** 用户账号密码登录 **/
|
||||
|
||||
@@ -28,8 +28,8 @@ import (
|
||||
)
|
||||
|
||||
type LdapLogin struct {
|
||||
AccountApp sysapp.Account
|
||||
MsgApp msgapp.Msg
|
||||
AccountApp sysapp.Account `inject:""`
|
||||
MsgApp msgapp.Msg `inject:""`
|
||||
}
|
||||
|
||||
// @router /auth/ldap/enabled [get]
|
||||
|
||||
@@ -28,9 +28,9 @@ import (
|
||||
)
|
||||
|
||||
type Oauth2Login struct {
|
||||
Oauth2App application.Oauth2
|
||||
AccountApp sysapp.Account
|
||||
MsgApp msgapp.Msg
|
||||
Oauth2App application.Oauth2 `inject:""`
|
||||
AccountApp sysapp.Account `inject:""`
|
||||
MsgApp msgapp.Msg `inject:""`
|
||||
}
|
||||
|
||||
func (a *Oauth2Login) OAuth2Login(rc *req.Ctx) {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package application
|
||||
|
||||
import "mayfly-go/internal/auth/infrastructure/persistence"
|
||||
|
||||
var (
|
||||
authApp = newAuthApp(persistence.GetOauthAccountRepo())
|
||||
import (
|
||||
"mayfly-go/internal/auth/infrastructure/persistence"
|
||||
"mayfly-go/pkg/ioc"
|
||||
)
|
||||
|
||||
func GetAuthApp() Oauth2 {
|
||||
return authApp
|
||||
func InitIoc() {
|
||||
persistence.Init()
|
||||
|
||||
ioc.Register(new(oauth2AppImpl), ioc.WithComponentName("Oauth2App"))
|
||||
}
|
||||
|
||||
@@ -14,27 +14,21 @@ type Oauth2 interface {
|
||||
Unbind(accountId uint64)
|
||||
}
|
||||
|
||||
func newAuthApp(oauthAccountRepo repository.Oauth2Account) Oauth2 {
|
||||
return &oauth2AppImpl{
|
||||
oauthAccountRepo: oauthAccountRepo,
|
||||
}
|
||||
}
|
||||
|
||||
type oauth2AppImpl struct {
|
||||
oauthAccountRepo repository.Oauth2Account
|
||||
Oauth2AccountRepo repository.Oauth2Account `inject:""`
|
||||
}
|
||||
|
||||
func (a *oauth2AppImpl) GetOAuthAccount(condition *entity.Oauth2Account, cols ...string) error {
|
||||
return a.oauthAccountRepo.GetBy(condition, cols...)
|
||||
return a.Oauth2AccountRepo.GetBy(condition, cols...)
|
||||
}
|
||||
|
||||
func (a *oauth2AppImpl) BindOAuthAccount(e *entity.Oauth2Account) error {
|
||||
if e.Id == 0 {
|
||||
return a.oauthAccountRepo.Insert(context.Background(), e)
|
||||
return a.Oauth2AccountRepo.Insert(context.Background(), e)
|
||||
}
|
||||
return a.oauthAccountRepo.UpdateById(context.Background(), e)
|
||||
return a.Oauth2AccountRepo.UpdateById(context.Background(), e)
|
||||
}
|
||||
|
||||
func (a *oauth2AppImpl) Unbind(accountId uint64) {
|
||||
a.oauthAccountRepo.DeleteByCond(context.Background(), &entity.Oauth2Account{AccountId: accountId})
|
||||
a.Oauth2AccountRepo.DeleteByCond(context.Background(), &entity.Oauth2Account{AccountId: accountId})
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
sysapp "mayfly-go/internal/sys/application"
|
||||
"mayfly-go/pkg/utils/conv"
|
||||
"mayfly-go/pkg/utils/stringx"
|
||||
)
|
||||
|
||||
@@ -26,8 +27,8 @@ func GetAccountLoginSecurity() *AccountLoginSecurity {
|
||||
als := new(AccountLoginSecurity)
|
||||
als.UseCaptcha = c.ConvBool(jm["useCaptcha"], true)
|
||||
als.UseOtp = c.ConvBool(jm["useOtp"], false)
|
||||
als.LoginFailCount = stringx.ConvInt(jm["loginFailCount"], 5)
|
||||
als.LoginFailMin = stringx.ConvInt(jm["loginFailMin"], 10)
|
||||
als.LoginFailCount = conv.Str2Int(jm["loginFailCount"], 5)
|
||||
als.LoginFailMin = conv.Str2Int(jm["loginFailMin"], 10)
|
||||
otpIssuer := jm["otpIssuer"]
|
||||
if otpIssuer == "" {
|
||||
otpIssuer = "mayfly-go"
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package persistence
|
||||
|
||||
import "mayfly-go/internal/auth/domain/repository"
|
||||
|
||||
var (
|
||||
authAccountRepo = newAuthAccountRepo()
|
||||
import (
|
||||
"mayfly-go/pkg/ioc"
|
||||
)
|
||||
|
||||
func GetOauthAccountRepo() repository.Oauth2Account {
|
||||
return authAccountRepo
|
||||
func Init() {
|
||||
ioc.Register(newAuthAccountRepo(), ioc.WithComponentName("Oauth2AccountRepo"))
|
||||
}
|
||||
|
||||
12
server/internal/auth/init/init.go
Normal file
12
server/internal/auth/init/init.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package init
|
||||
|
||||
import (
|
||||
"mayfly-go/initialize"
|
||||
"mayfly-go/internal/auth/application"
|
||||
"mayfly-go/internal/auth/router"
|
||||
)
|
||||
|
||||
func init() {
|
||||
initialize.AddInitIocFunc(application.InitIoc)
|
||||
initialize.AddInitRouterFunc(router.Init)
|
||||
}
|
||||
@@ -2,30 +2,22 @@ package router
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/auth/api"
|
||||
"mayfly-go/internal/auth/application"
|
||||
msgapp "mayfly-go/internal/msg/application"
|
||||
sysapp "mayfly-go/internal/sys/application"
|
||||
"mayfly-go/pkg/biz"
|
||||
"mayfly-go/pkg/ioc"
|
||||
"mayfly-go/pkg/req"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func Init(router *gin.RouterGroup) {
|
||||
accountLogin := &api.AccountLogin{
|
||||
AccountApp: sysapp.GetAccountApp(),
|
||||
MsgApp: msgapp.GetMsgApp(),
|
||||
}
|
||||
accountLogin := new(api.AccountLogin)
|
||||
biz.ErrIsNil(ioc.Inject(accountLogin))
|
||||
|
||||
ldapLogin := &api.LdapLogin{
|
||||
AccountApp: sysapp.GetAccountApp(),
|
||||
MsgApp: msgapp.GetMsgApp(),
|
||||
}
|
||||
ldapLogin := new(api.LdapLogin)
|
||||
biz.ErrIsNil(ioc.Inject(ldapLogin))
|
||||
|
||||
oauth2Login := &api.Oauth2Login{
|
||||
Oauth2App: application.GetAuthApp(),
|
||||
AccountApp: sysapp.GetAccountApp(),
|
||||
MsgApp: msgapp.GetMsgApp(),
|
||||
}
|
||||
oauth2Login := new(api.Oauth2Login)
|
||||
biz.ErrIsNil(ioc.Inject(oauth2Login))
|
||||
|
||||
rg := router.Group("/auth")
|
||||
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/common/consts"
|
||||
dbapp "mayfly-go/internal/db/application"
|
||||
machineapp "mayfly-go/internal/machine/application"
|
||||
mongoapp "mayfly-go/internal/mongo/application"
|
||||
redisapp "mayfly-go/internal/redis/application"
|
||||
tagapp "mayfly-go/internal/tag/application"
|
||||
"mayfly-go/pkg/req"
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
)
|
||||
|
||||
type Index struct {
|
||||
TagApp tagapp.TagTree
|
||||
MachineApp machineapp.Machine
|
||||
DbApp dbapp.Db
|
||||
RedisApp redisapp.Redis
|
||||
MongoApp mongoapp.Mongo
|
||||
}
|
||||
|
||||
func (i *Index) Count(rc *req.Ctx) {
|
||||
accountId := rc.GetLoginAccount().Id
|
||||
|
||||
mongoNum := len(i.TagApp.GetAccountResourceCodes(accountId, consts.TagResourceTypeMongo, ""))
|
||||
machienNum := len(i.TagApp.GetAccountResourceCodes(accountId, consts.TagResourceTypeMachine, ""))
|
||||
dbNum := len(i.TagApp.GetAccountResourceCodes(accountId, consts.TagResourceTypeDb, ""))
|
||||
redisNum := len(i.TagApp.GetAccountResourceCodes(accountId, consts.TagResourceTypeRedis, ""))
|
||||
|
||||
rc.ResData = collx.M{
|
||||
"mongoNum": mongoNum,
|
||||
"machineNum": machienNum,
|
||||
"dbNum": dbNum,
|
||||
"redisNum": redisNum,
|
||||
}
|
||||
}
|
||||
10
server/internal/common/init/init.go
Normal file
10
server/internal/common/init/init.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package init
|
||||
|
||||
import (
|
||||
"mayfly-go/initialize"
|
||||
"mayfly-go/internal/common/router"
|
||||
)
|
||||
|
||||
func init() {
|
||||
initialize.AddInitRouterFunc(router.Init)
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/common/api"
|
||||
dbapp "mayfly-go/internal/db/application"
|
||||
machineapp "mayfly-go/internal/machine/application"
|
||||
mongoapp "mayfly-go/internal/mongo/application"
|
||||
redisapp "mayfly-go/internal/redis/application"
|
||||
tagapp "mayfly-go/internal/tag/application"
|
||||
"mayfly-go/pkg/req"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func InitIndexRouter(router *gin.RouterGroup) {
|
||||
index := router.Group("common/index")
|
||||
i := &api.Index{
|
||||
TagApp: tagapp.GetTagTreeApp(),
|
||||
MachineApp: machineapp.GetMachineApp(),
|
||||
DbApp: dbapp.GetDbApp(),
|
||||
RedisApp: redisapp.GetRedisApp(),
|
||||
MongoApp: mongoapp.GetMongoApp(),
|
||||
}
|
||||
{
|
||||
// 首页基本信息统计
|
||||
req.NewGet("count", i.Count).Group(index)
|
||||
}
|
||||
}
|
||||
@@ -4,5 +4,4 @@ import "github.com/gin-gonic/gin"
|
||||
|
||||
func Init(router *gin.RouterGroup) {
|
||||
InitCommonRouter(router)
|
||||
InitIndexRouter(router)
|
||||
}
|
||||
|
||||
23
server/internal/db/api/dashbord.go
Normal file
23
server/internal/db/api/dashbord.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/common/consts"
|
||||
"mayfly-go/internal/db/application"
|
||||
tagapp "mayfly-go/internal/tag/application"
|
||||
"mayfly-go/pkg/req"
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
)
|
||||
|
||||
type Dashbord struct {
|
||||
TagTreeApp tagapp.TagTree `inject:""`
|
||||
DbApp application.Db `inject:""`
|
||||
}
|
||||
|
||||
func (m *Dashbord) Dashbord(rc *req.Ctx) {
|
||||
accountId := rc.GetLoginAccount().Id
|
||||
dbNum := len(m.TagTreeApp.GetAccountResourceCodes(accountId, consts.TagResourceTypeDb, ""))
|
||||
|
||||
rc.ResData = collx.M{
|
||||
"dbNum": dbNum,
|
||||
}
|
||||
}
|
||||
@@ -32,11 +32,11 @@ import (
|
||||
)
|
||||
|
||||
type Db struct {
|
||||
InstanceApp application.Instance
|
||||
DbApp application.Db
|
||||
DbSqlExecApp application.DbSqlExec
|
||||
MsgApp msgapp.Msg
|
||||
TagApp tagapp.TagTree
|
||||
InstanceApp application.Instance `inject:"DbInstanceApp"`
|
||||
DbApp application.Db `inject:""`
|
||||
DbSqlExecApp application.DbSqlExec `inject:""`
|
||||
MsgApp msgapp.Msg `inject:""`
|
||||
TagApp tagapp.TagTree `inject:"TagTreeApp"`
|
||||
}
|
||||
|
||||
// @router /api/dbs [get]
|
||||
@@ -355,7 +355,7 @@ func (d *Db) dumpDb(writer *gzipWriter, dbId uint64, dbName string, tables []str
|
||||
writer.WriteString("BEGIN;\n")
|
||||
}
|
||||
insertSql := "INSERT INTO %s VALUES (%s);\n"
|
||||
dbMeta.WalkTableRecord(table, func(record map[string]any, columns []*dbi.QueryColumn) error {
|
||||
dbConn.WalkTableRows(context.TODO(), table, func(record map[string]any, columns []*dbi.QueryColumn) error {
|
||||
var values []string
|
||||
writer.TryFlush()
|
||||
for _, column := range columns {
|
||||
@@ -462,6 +462,20 @@ func (d *Db) GetSchemas(rc *req.Ctx) {
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
func (d *Db) CopyTable(rc *req.Ctx) {
|
||||
form := &form.DbCopyTableForm{}
|
||||
copy := ginx.BindJsonAndCopyTo[*dbi.DbCopyTable](rc.GinCtx, form, new(dbi.DbCopyTable))
|
||||
|
||||
conn, err := d.DbApp.GetDbConn(form.Id, form.Db)
|
||||
biz.ErrIsNilAppendErr(err, "拷贝表失败: %s")
|
||||
|
||||
err = conn.GetDialect().CopyTable(copy)
|
||||
if err != nil {
|
||||
logx.Errorf("拷贝表失败: %s", err.Error())
|
||||
}
|
||||
biz.ErrIsNilAppendErr(err, "拷贝表失败: %s")
|
||||
}
|
||||
|
||||
func getDbId(g *gin.Context) uint64 {
|
||||
dbId, _ := strconv.Atoi(g.Param("dbId"))
|
||||
biz.IsTrue(dbId > 0, "dbId错误")
|
||||
|
||||
@@ -9,13 +9,16 @@ import (
|
||||
"mayfly-go/pkg/biz"
|
||||
"mayfly-go/pkg/ginx"
|
||||
"mayfly-go/pkg/req"
|
||||
"mayfly-go/pkg/utils/timex"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DbBackup struct {
|
||||
DbBackupApp *application.DbBackupApp
|
||||
DbApp application.Db
|
||||
backupApp *application.DbBackupApp `inject:"DbBackupApp"`
|
||||
dbApp application.Db `inject:"DbApp"`
|
||||
restoreApp *application.DbRestoreApp `inject:"DbRestoreApp"`
|
||||
}
|
||||
|
||||
// todo: 鉴权,避免未经授权进行数据库备份和恢复
|
||||
@@ -25,13 +28,13 @@ type DbBackup struct {
|
||||
func (d *DbBackup) GetPageList(rc *req.Ctx) {
|
||||
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
|
||||
biz.IsTrue(dbId > 0, "无效的 dbId: %v", dbId)
|
||||
db, err := d.DbApp.GetById(new(entity.Db), dbId, "db_instance_id", "database")
|
||||
db, err := d.dbApp.GetById(new(entity.Db), dbId, "db_instance_id", "database")
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||
|
||||
queryCond, page := ginx.BindQueryAndPage[*entity.DbJobQuery](rc.GinCtx, new(entity.DbJobQuery))
|
||||
queryCond, page := ginx.BindQueryAndPage[*entity.DbBackupQuery](rc.GinCtx, new(entity.DbBackupQuery))
|
||||
queryCond.DbInstanceId = db.InstanceId
|
||||
queryCond.InDbNames = strings.Fields(db.Database)
|
||||
res, err := d.DbBackupApp.GetPageList(queryCond, page, new([]vo.DbBackup))
|
||||
res, err := d.backupApp.GetPageList(queryCond, page, new([]vo.DbBackup))
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库备份任务失败: %v")
|
||||
rc.ResData = res
|
||||
}
|
||||
@@ -48,23 +51,22 @@ func (d *DbBackup) Create(rc *req.Ctx) {
|
||||
|
||||
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
|
||||
biz.IsTrue(dbId > 0, "无效的 dbId: %v", dbId)
|
||||
db, err := d.DbApp.GetById(new(entity.Db), dbId, "instanceId")
|
||||
db, err := d.dbApp.GetById(new(entity.Db), dbId, "instanceId")
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||
|
||||
jobs := make([]*entity.DbBackup, 0, len(dbNames))
|
||||
for _, dbName := range dbNames {
|
||||
job := &entity.DbBackup{
|
||||
DbJobBaseImpl: entity.NewDbBJobBase(db.InstanceId, entity.DbJobTypeBackup),
|
||||
Enabled: true,
|
||||
Repeated: backupForm.Repeated,
|
||||
StartTime: backupForm.StartTime,
|
||||
Interval: backupForm.Interval,
|
||||
Name: backupForm.Name,
|
||||
DbInstanceId: db.InstanceId,
|
||||
DbName: dbName,
|
||||
Enabled: true,
|
||||
Repeated: backupForm.Repeated,
|
||||
StartTime: backupForm.StartTime,
|
||||
Interval: backupForm.Interval,
|
||||
Name: backupForm.Name,
|
||||
}
|
||||
job.DbName = dbName
|
||||
jobs = append(jobs, job)
|
||||
}
|
||||
biz.ErrIsNilAppendErr(d.DbBackupApp.Create(rc.MetaCtx, jobs), "添加数据库备份任务失败: %v")
|
||||
biz.ErrIsNilAppendErr(d.backupApp.Create(rc.MetaCtx, jobs), "添加数据库备份任务失败: %v")
|
||||
}
|
||||
|
||||
// Update 保存数据库备份任务
|
||||
@@ -74,17 +76,17 @@ func (d *DbBackup) Update(rc *req.Ctx) {
|
||||
ginx.BindJsonAndValid(rc.GinCtx, backupForm)
|
||||
rc.ReqParam = backupForm
|
||||
|
||||
job := entity.NewDbJob(entity.DbJobTypeBackup).(*entity.DbBackup)
|
||||
job := &entity.DbBackup{}
|
||||
job.Id = backupForm.Id
|
||||
job.Name = backupForm.Name
|
||||
job.StartTime = backupForm.StartTime
|
||||
job.Interval = backupForm.Interval
|
||||
biz.ErrIsNilAppendErr(d.DbBackupApp.Update(rc.MetaCtx, job), "保存数据库备份任务失败: %v")
|
||||
biz.ErrIsNilAppendErr(d.backupApp.Update(rc.MetaCtx, job), "保存数据库备份任务失败: %v")
|
||||
}
|
||||
|
||||
func (d *DbBackup) walk(rc *req.Ctx, fn func(ctx context.Context, backupId uint64) error) error {
|
||||
idsStr := ginx.PathParam(rc.GinCtx, "backupId")
|
||||
biz.NotEmpty(idsStr, "backupId 为空")
|
||||
func (d *DbBackup) walk(rc *req.Ctx, paramName string, fn func(ctx context.Context, id uint64) error) error {
|
||||
idsStr := ginx.PathParam(rc.GinCtx, paramName)
|
||||
biz.NotEmpty(idsStr, paramName+" 为空")
|
||||
rc.ReqParam = idsStr
|
||||
ids := strings.Fields(idsStr)
|
||||
for _, v := range ids {
|
||||
@@ -104,28 +106,28 @@ func (d *DbBackup) walk(rc *req.Ctx, fn func(ctx context.Context, backupId uint6
|
||||
// Delete 删除数据库备份任务
|
||||
// @router /api/dbs/:dbId/backups/:backupId [DELETE]
|
||||
func (d *DbBackup) Delete(rc *req.Ctx) {
|
||||
err := d.walk(rc, d.DbBackupApp.Delete)
|
||||
err := d.walk(rc, "backupId", d.backupApp.Delete)
|
||||
biz.ErrIsNilAppendErr(err, "删除数据库备份任务失败: %v")
|
||||
}
|
||||
|
||||
// Enable 启用数据库备份任务
|
||||
// @router /api/dbs/:dbId/backups/:backupId/enable [PUT]
|
||||
func (d *DbBackup) Enable(rc *req.Ctx) {
|
||||
err := d.walk(rc, d.DbBackupApp.Enable)
|
||||
err := d.walk(rc, "backupId", d.backupApp.Enable)
|
||||
biz.ErrIsNilAppendErr(err, "启用数据库备份任务失败: %v")
|
||||
}
|
||||
|
||||
// Disable 禁用数据库备份任务
|
||||
// @router /api/dbs/:dbId/backups/:backupId/disable [PUT]
|
||||
func (d *DbBackup) Disable(rc *req.Ctx) {
|
||||
err := d.walk(rc, d.DbBackupApp.Disable)
|
||||
err := d.walk(rc, "backupId", d.backupApp.Disable)
|
||||
biz.ErrIsNilAppendErr(err, "禁用数据库备份任务失败: %v")
|
||||
}
|
||||
|
||||
// Start 禁用数据库备份任务
|
||||
// @router /api/dbs/:dbId/backups/:backupId/start [PUT]
|
||||
func (d *DbBackup) Start(rc *req.Ctx) {
|
||||
err := d.walk(rc, d.DbBackupApp.Start)
|
||||
err := d.walk(rc, "backupId", d.backupApp.StartNow)
|
||||
biz.ErrIsNilAppendErr(err, "运行数据库备份任务失败: %v")
|
||||
}
|
||||
|
||||
@@ -133,10 +135,10 @@ func (d *DbBackup) Start(rc *req.Ctx) {
|
||||
// @router /api/dbs/:dbId/db-names-without-backup [GET]
|
||||
func (d *DbBackup) GetDbNamesWithoutBackup(rc *req.Ctx) {
|
||||
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
|
||||
db, err := d.DbApp.GetById(new(entity.Db), dbId, "instance_id", "database")
|
||||
db, err := d.dbApp.GetById(new(entity.Db), dbId, "instance_id", "database")
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||
dbNames := strings.Fields(db.Database)
|
||||
dbNamesWithoutBackup, err := d.DbBackupApp.GetDbNamesWithoutBackup(db.InstanceId, dbNames)
|
||||
dbNamesWithoutBackup, err := d.backupApp.GetDbNamesWithoutBackup(db.InstanceId, dbNames)
|
||||
biz.ErrIsNilAppendErr(err, "获取未配置定时备份的数据库名称失败: %v")
|
||||
rc.ResData = dbNamesWithoutBackup
|
||||
}
|
||||
@@ -146,13 +148,74 @@ func (d *DbBackup) GetDbNamesWithoutBackup(rc *req.Ctx) {
|
||||
func (d *DbBackup) GetHistoryPageList(rc *req.Ctx) {
|
||||
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
|
||||
biz.IsTrue(dbId > 0, "无效的 dbId: %v", dbId)
|
||||
db, err := d.DbApp.GetById(new(entity.Db), dbId, "db_instance_id", "database")
|
||||
db, err := d.dbApp.GetById(new(entity.Db), dbId, "db_instance_id", "database")
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||
|
||||
queryCond, page := ginx.BindQueryAndPage[*entity.DbBackupHistoryQuery](rc.GinCtx, new(entity.DbBackupHistoryQuery))
|
||||
queryCond.DbInstanceId = db.InstanceId
|
||||
queryCond.InDbNames = strings.Fields(db.Database)
|
||||
res, err := d.DbBackupApp.GetHistoryPageList(queryCond, page, new([]vo.DbBackupHistory))
|
||||
backupHistoryCond, page := ginx.BindQueryAndPage[*entity.DbBackupHistoryQuery](rc.GinCtx, new(entity.DbBackupHistoryQuery))
|
||||
backupHistoryCond.DbInstanceId = db.InstanceId
|
||||
backupHistoryCond.InDbNames = strings.Fields(db.Database)
|
||||
backupHistories := make([]*vo.DbBackupHistory, 0, page.PageSize)
|
||||
res, err := d.backupApp.GetHistoryPageList(backupHistoryCond, page, &backupHistories)
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库备份历史失败: %v")
|
||||
historyIds := make([]uint64, 0, len(backupHistories))
|
||||
for _, history := range backupHistories {
|
||||
historyIds = append(historyIds, history.Id)
|
||||
}
|
||||
restores := make([]*entity.DbRestore, 0, page.PageSize)
|
||||
if err := d.restoreApp.GetRestoresEnabled(&restores, historyIds...); err != nil {
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库备份恢复记录失败")
|
||||
}
|
||||
for _, history := range backupHistories {
|
||||
for _, restore := range restores {
|
||||
if restore.DbBackupHistoryId == history.Id {
|
||||
history.LastStatus = restore.LastStatus
|
||||
history.LastResult = restore.LastResult
|
||||
history.LastTime = restore.LastTime
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
// RestoreHistories 删除数据库备份历史
|
||||
// @router /api/dbs/:dbId/backup-histories/:backupHistoryId/restore [POST]
|
||||
func (d *DbBackup) RestoreHistories(rc *req.Ctx) {
|
||||
pm := ginx.PathParam(rc.GinCtx, "backupHistoryId")
|
||||
biz.NotEmpty(pm, "backupHistoryId 为空")
|
||||
idsStr := strings.Fields(pm)
|
||||
ids := make([]uint64, 0, len(idsStr))
|
||||
for _, s := range idsStr {
|
||||
id, err := strconv.ParseUint(s, 10, 64)
|
||||
biz.ErrIsNilAppendErr(err, "从数据库备份历史恢复数据库失败: %v")
|
||||
ids = append(ids, id)
|
||||
}
|
||||
histories := make([]*entity.DbBackupHistory, 0, len(ids))
|
||||
err := d.backupApp.GetHistories(ids, &histories)
|
||||
biz.ErrIsNilAppendErr(err, "添加数据库恢复任务失败: %v")
|
||||
restores := make([]*entity.DbRestore, 0, len(histories))
|
||||
now := time.Now()
|
||||
for _, history := range histories {
|
||||
job := &entity.DbRestore{
|
||||
DbInstanceId: history.DbInstanceId,
|
||||
DbName: history.DbName,
|
||||
Enabled: true,
|
||||
Repeated: false,
|
||||
StartTime: now,
|
||||
Interval: 0,
|
||||
PointInTime: timex.NewNullTime(time.Time{}),
|
||||
DbBackupId: history.DbBackupId,
|
||||
DbBackupHistoryId: history.Id,
|
||||
DbBackupHistoryName: history.Name,
|
||||
}
|
||||
restores = append(restores, job)
|
||||
}
|
||||
biz.ErrIsNilAppendErr(d.restoreApp.Create(rc.MetaCtx, restores), "添加数据库恢复任务失败: %v")
|
||||
}
|
||||
|
||||
// DeleteHistories 删除数据库备份历史
|
||||
// @router /api/dbs/:dbId/backup-histories/:backupHistoryId [DELETE]
|
||||
func (d *DbBackup) DeleteHistories(rc *req.Ctx) {
|
||||
err := d.walk(rc, "backupHistoryId", d.backupApp.DeleteHistory)
|
||||
biz.ErrIsNilAppendErr(err, "删除数据库备份历史失败: %v")
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"mayfly-go/internal/db/api/form"
|
||||
"mayfly-go/internal/db/api/vo"
|
||||
@@ -15,11 +14,10 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type DataSyncTask struct {
|
||||
DataSyncTaskApp application.DataSyncTask
|
||||
DataSyncTaskApp application.DataSyncTask `inject:"DbDataSyncTaskApp"`
|
||||
}
|
||||
|
||||
func (d *DataSyncTask) Tasks(rc *req.Ctx) {
|
||||
@@ -47,13 +45,6 @@ func (d *DataSyncTask) SaveTask(rc *req.Ctx) {
|
||||
task.DataSql = sql
|
||||
form.DataSql = sql
|
||||
|
||||
key := task.TaskKey
|
||||
// 判断key为空就生成随机key
|
||||
if key == "" {
|
||||
key = uuid.New().String()
|
||||
task.TaskKey = key
|
||||
}
|
||||
|
||||
rc.ReqParam = form
|
||||
biz.ErrIsNil(d.DataSyncTaskApp.Save(rc.MetaCtx, task))
|
||||
}
|
||||
@@ -73,7 +64,7 @@ func (d *DataSyncTask) DeleteTask(rc *req.Ctx) {
|
||||
func (d *DataSyncTask) ChangeStatus(rc *req.Ctx) {
|
||||
form := &form.DataSyncTaskStatusForm{}
|
||||
task := ginx.BindJsonAndCopyTo[*entity.DataSyncTask](rc.GinCtx, form, new(entity.DataSyncTask))
|
||||
_ = d.DataSyncTaskApp.UpdateById(context.Background(), task)
|
||||
_ = d.DataSyncTaskApp.UpdateById(rc.MetaCtx, task)
|
||||
|
||||
if task.Status == entity.DataSyncTaskStatusEnable {
|
||||
task, err := d.DataSyncTaskApp.GetById(new(entity.DataSyncTask), task.Id)
|
||||
@@ -89,7 +80,7 @@ func (d *DataSyncTask) ChangeStatus(rc *req.Ctx) {
|
||||
func (d *DataSyncTask) Run(rc *req.Ctx) {
|
||||
taskId := getTaskId(rc.GinCtx)
|
||||
rc.ReqParam = taskId
|
||||
d.DataSyncTaskApp.RunCronJob(taskId)
|
||||
_ = d.DataSyncTaskApp.RunCronJob(taskId)
|
||||
}
|
||||
|
||||
func (d *DataSyncTask) Stop(rc *req.Ctx) {
|
||||
@@ -99,7 +90,7 @@ func (d *DataSyncTask) Stop(rc *req.Ctx) {
|
||||
task := new(entity.DataSyncTask)
|
||||
task.Id = taskId
|
||||
task.RunningState = entity.DataSyncTaskRunStateStop
|
||||
_ = d.DataSyncTaskApp.UpdateById(context.Background(), task)
|
||||
_ = d.DataSyncTaskApp.UpdateById(rc.MetaCtx, task)
|
||||
}
|
||||
|
||||
func (d *DataSyncTask) GetTask(rc *req.Ctx) {
|
||||
|
||||
@@ -14,8 +14,8 @@ import (
|
||||
)
|
||||
|
||||
type DbRestore struct {
|
||||
DbRestoreApp *application.DbRestoreApp
|
||||
DbApp application.Db
|
||||
restoreApp *application.DbRestoreApp `inject:"DbRestoreApp"`
|
||||
dbApp application.Db `inject:"DbApp"`
|
||||
}
|
||||
|
||||
// GetPageList 获取数据库恢复任务
|
||||
@@ -23,14 +23,14 @@ type DbRestore struct {
|
||||
func (d *DbRestore) GetPageList(rc *req.Ctx) {
|
||||
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
|
||||
biz.IsTrue(dbId > 0, "无效的 dbId: %v", dbId)
|
||||
db, err := d.DbApp.GetById(new(entity.Db), dbId, "db_instance_id", "database")
|
||||
db, err := d.dbApp.GetById(new(entity.Db), dbId, "db_instance_id", "database")
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||
|
||||
var restores []vo.DbRestore
|
||||
queryCond, page := ginx.BindQueryAndPage[*entity.DbJobQuery](rc.GinCtx, new(entity.DbJobQuery))
|
||||
queryCond, page := ginx.BindQueryAndPage[*entity.DbRestoreQuery](rc.GinCtx, new(entity.DbRestoreQuery))
|
||||
queryCond.DbInstanceId = db.InstanceId
|
||||
queryCond.InDbNames = strings.Fields(db.Database)
|
||||
res, err := d.DbRestoreApp.GetPageList(queryCond, page, &restores)
|
||||
res, err := d.restoreApp.GetPageList(queryCond, page, &restores)
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库恢复任务失败: %v")
|
||||
rc.ResData = res
|
||||
}
|
||||
@@ -44,11 +44,12 @@ func (d *DbRestore) Create(rc *req.Ctx) {
|
||||
|
||||
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
|
||||
biz.IsTrue(dbId > 0, "无效的 dbId: %v", dbId)
|
||||
db, err := d.DbApp.GetById(new(entity.Db), dbId, "instanceId")
|
||||
db, err := d.dbApp.GetById(new(entity.Db), dbId, "instanceId")
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||
|
||||
job := &entity.DbRestore{
|
||||
DbJobBaseImpl: entity.NewDbBJobBase(db.InstanceId, entity.DbJobTypeRestore),
|
||||
DbInstanceId: db.InstanceId,
|
||||
DbName: restoreForm.DbName,
|
||||
Enabled: true,
|
||||
Repeated: restoreForm.Repeated,
|
||||
StartTime: restoreForm.StartTime,
|
||||
@@ -58,8 +59,11 @@ func (d *DbRestore) Create(rc *req.Ctx) {
|
||||
DbBackupHistoryId: restoreForm.DbBackupHistoryId,
|
||||
DbBackupHistoryName: restoreForm.DbBackupHistoryName,
|
||||
}
|
||||
job.DbName = restoreForm.DbName
|
||||
biz.ErrIsNilAppendErr(d.DbRestoreApp.Create(rc.MetaCtx, job), "添加数据库恢复任务失败: %v")
|
||||
biz.ErrIsNilAppendErr(d.restoreApp.Create(rc.MetaCtx, job), "添加数据库恢复任务失败: %v")
|
||||
}
|
||||
|
||||
func (d *DbRestore) createWithBackupHistory(backupHistoryIds string) {
|
||||
|
||||
}
|
||||
|
||||
// Update 保存数据库恢复任务
|
||||
@@ -73,7 +77,7 @@ func (d *DbRestore) Update(rc *req.Ctx) {
|
||||
job.Id = restoreForm.Id
|
||||
job.StartTime = restoreForm.StartTime
|
||||
job.Interval = restoreForm.Interval
|
||||
biz.ErrIsNilAppendErr(d.DbRestoreApp.Update(rc.MetaCtx, job), "保存数据库恢复任务失败: %v")
|
||||
biz.ErrIsNilAppendErr(d.restoreApp.Update(rc.MetaCtx, job), "保存数据库恢复任务失败: %v")
|
||||
}
|
||||
|
||||
func (d *DbRestore) walk(rc *req.Ctx, fn func(ctx context.Context, restoreId uint64) error) error {
|
||||
@@ -98,21 +102,21 @@ func (d *DbRestore) walk(rc *req.Ctx, fn func(ctx context.Context, restoreId uin
|
||||
// Delete 删除数据库恢复任务
|
||||
// @router /api/dbs/:dbId/restores/:restoreId [DELETE]
|
||||
func (d *DbRestore) Delete(rc *req.Ctx) {
|
||||
err := d.walk(rc, d.DbRestoreApp.Delete)
|
||||
err := d.walk(rc, d.restoreApp.Delete)
|
||||
biz.ErrIsNilAppendErr(err, "删除数据库恢复任务失败: %v")
|
||||
}
|
||||
|
||||
// Enable 启用数据库恢复任务
|
||||
// @router /api/dbs/:dbId/restores/:restoreId/enable [PUT]
|
||||
func (d *DbRestore) Enable(rc *req.Ctx) {
|
||||
err := d.walk(rc, d.DbRestoreApp.Enable)
|
||||
err := d.walk(rc, d.restoreApp.Enable)
|
||||
biz.ErrIsNilAppendErr(err, "启用数据库恢复任务失败: %v")
|
||||
}
|
||||
|
||||
// Disable 禁用数据库恢复任务
|
||||
// @router /api/dbs/:dbId/restores/:restoreId/disable [PUT]
|
||||
func (d *DbRestore) Disable(rc *req.Ctx) {
|
||||
err := d.walk(rc, d.DbRestoreApp.Disable)
|
||||
err := d.walk(rc, d.restoreApp.Disable)
|
||||
biz.ErrIsNilAppendErr(err, "禁用数据库恢复任务失败: %v")
|
||||
}
|
||||
|
||||
@@ -120,21 +124,21 @@ func (d *DbRestore) Disable(rc *req.Ctx) {
|
||||
// @router /api/dbs/:dbId/db-names-without-backup [GET]
|
||||
func (d *DbRestore) GetDbNamesWithoutRestore(rc *req.Ctx) {
|
||||
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
|
||||
db, err := d.DbApp.GetById(new(entity.Db), dbId, "instance_id", "database")
|
||||
db, err := d.dbApp.GetById(new(entity.Db), dbId, "instance_id", "database")
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||
dbNames := strings.Fields(db.Database)
|
||||
dbNamesWithoutRestore, err := d.DbRestoreApp.GetDbNamesWithoutRestore(db.InstanceId, dbNames)
|
||||
dbNamesWithoutRestore, err := d.restoreApp.GetDbNamesWithoutRestore(db.InstanceId, dbNames)
|
||||
biz.ErrIsNilAppendErr(err, "获取未配置定时备份的数据库名称失败: %v")
|
||||
rc.ResData = dbNamesWithoutRestore
|
||||
}
|
||||
|
||||
// 获取数据库备份历史
|
||||
// GetHistoryPageList 获取数据库备份历史
|
||||
// @router /api/dbs/:dbId/restores/:restoreId/histories [GET]
|
||||
func (d *DbRestore) GetHistoryPageList(rc *req.Ctx) {
|
||||
queryCond := &entity.DbRestoreHistoryQuery{
|
||||
DbRestoreId: uint64(ginx.PathParamInt(rc.GinCtx, "restoreId")),
|
||||
}
|
||||
res, err := d.DbRestoreApp.GetHistoryPageList(queryCond, ginx.GetPageParam(rc.GinCtx), new([]vo.DbRestoreHistory))
|
||||
res, err := d.restoreApp.GetHistoryPageList(queryCond, ginx.GetPageParam(rc.GinCtx), new([]vo.DbRestoreHistory))
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库备份历史失败: %v")
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
type DbSql struct {
|
||||
DbSqlApp application.DbSql
|
||||
DbSqlApp application.DbSql `inject:""`
|
||||
}
|
||||
|
||||
// @router /api/db/:dbId/sql [post]
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
type DbSqlExec struct {
|
||||
DbSqlExecApp application.DbSqlExec
|
||||
DbSqlExecApp application.DbSqlExec `inject:""`
|
||||
}
|
||||
|
||||
func (d *DbSqlExec) DbSqlExecs(rc *req.Ctx) {
|
||||
|
||||
@@ -23,3 +23,11 @@ type DbSqlExecForm struct {
|
||||
Sql string `binding:"required" json:"sql"` // 执行sql
|
||||
Remark string `json:"remark"` // 执行备注
|
||||
}
|
||||
|
||||
// 数据库复制表
|
||||
type DbCopyTableForm struct {
|
||||
Id uint64 `binding:"required" json:"id"`
|
||||
Db string `binding:"required" json:"db" `
|
||||
TableName string `binding:"required" json:"tableName"`
|
||||
CopyData bool `json:"copyData"` // 是否复制数据
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ type InstanceForm struct {
|
||||
Name string `binding:"required" json:"name"`
|
||||
Type string `binding:"required" json:"type"` // 类型,mysql oracle等
|
||||
Host string `binding:"required" json:"host"`
|
||||
Port int `binding:"required" json:"port"`
|
||||
Port int `json:"port"`
|
||||
Sid string `json:"sid"`
|
||||
Username string `binding:"required" json:"username"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Params string `json:"params"`
|
||||
Remark string `json:"remark"`
|
||||
|
||||
@@ -16,8 +16,8 @@ import (
|
||||
)
|
||||
|
||||
type Instance struct {
|
||||
InstanceApp application.Instance
|
||||
DbApp application.Db
|
||||
InstanceApp application.Instance `inject:"DbInstanceApp"`
|
||||
DbApp application.Db `inject:""`
|
||||
}
|
||||
|
||||
// Instances 获取数据库实例信息
|
||||
|
||||
@@ -2,37 +2,50 @@ package vo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
"mayfly-go/pkg/utils/timex"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DbBackup 数据库备份任务
|
||||
type DbBackup struct {
|
||||
Id uint64 `json:"id"`
|
||||
DbName string `json:"dbName"` // 数据库名
|
||||
CreateTime time.Time `json:"createTime"` // 创建时间
|
||||
StartTime time.Time `json:"startTime"` // 开始时间
|
||||
Interval time.Duration `json:"-"` // 间隔时间
|
||||
IntervalDay uint64 `json:"intervalDay" gorm:"-"` // 间隔天数
|
||||
Enabled bool `json:"enabled"` // 是否启用
|
||||
LastTime timex.NullTime `json:"lastTime"` // 最近一次执行时间
|
||||
LastStatus string `json:"lastStatus"` // 最近一次执行状态
|
||||
LastResult string `json:"lastResult"` // 最近一次执行结果
|
||||
DbInstanceId uint64 `json:"dbInstanceId"` // 数据库实例ID
|
||||
Name string `json:"name"` // 备份任务名称
|
||||
Id uint64 `json:"id"`
|
||||
DbName string `json:"dbName"` // 数据库名
|
||||
CreateTime time.Time `json:"createTime"` // 创建时间
|
||||
StartTime time.Time `json:"startTime"` // 开始时间
|
||||
Interval time.Duration `json:"-"` // 间隔时间
|
||||
IntervalDay uint64 `json:"intervalDay" gorm:"-"` // 间隔天数
|
||||
Enabled bool `json:"enabled"` // 是否启用
|
||||
EnabledDesc string `json:"enabledDesc"` // 启用状态描述
|
||||
LastTime timex.NullTime `json:"lastTime"` // 最近一次执行时间
|
||||
LastStatus entity.DbJobStatus `json:"lastStatus"` // 最近一次执行状态
|
||||
LastResult string `json:"lastResult"` // 最近一次执行结果
|
||||
DbInstanceId uint64 `json:"dbInstanceId"` // 数据库实例ID
|
||||
Name string `json:"name"` // 备份任务名称
|
||||
}
|
||||
|
||||
func (backup *DbBackup) MarshalJSON() ([]byte, error) {
|
||||
type dbBackup DbBackup
|
||||
backup.IntervalDay = uint64(backup.Interval / time.Hour / 24)
|
||||
if len(backup.EnabledDesc) == 0 {
|
||||
if backup.Enabled {
|
||||
backup.EnabledDesc = "任务已启用"
|
||||
} else {
|
||||
backup.EnabledDesc = "任务已禁用"
|
||||
}
|
||||
}
|
||||
return json.Marshal((*dbBackup)(backup))
|
||||
}
|
||||
|
||||
// DbBackupHistory 数据库备份历史
|
||||
type DbBackupHistory struct {
|
||||
Id uint64 `json:"id"`
|
||||
DbBackupId uint64 `json:"dbBackupId"`
|
||||
CreateTime time.Time `json:"createTime"`
|
||||
DbName string `json:"dbName"` // 数据库名称
|
||||
Name string `json:"name"` // 备份历史名称
|
||||
Id uint64 `json:"id"`
|
||||
DbBackupId uint64 `json:"dbBackupId"`
|
||||
CreateTime time.Time `json:"createTime"`
|
||||
DbName string `json:"dbName"` // 数据库名称
|
||||
Name string `json:"name"` // 备份历史名称
|
||||
BinlogFileName string `json:"binlogFileName"`
|
||||
LastTime timex.NullTime `json:"lastTime" gorm:"-"` // 最近一次恢复时间
|
||||
LastStatus entity.DbJobStatus `json:"lastStatus" gorm:"-"` // 最近一次恢复状态
|
||||
LastResult string `json:"lastResult" gorm:"-"` // 最近一次恢复结果
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ type DbRestore struct {
|
||||
Interval time.Duration `json:"-"` // 间隔时间
|
||||
IntervalDay uint64 `json:"intervalDay" gorm:"-"` // 间隔天数
|
||||
Enabled bool `json:"enabled"` // 是否启用
|
||||
EnabledDesc string `json:"enabledDesc"` // 启用状态描述
|
||||
LastTime timex.NullTime `json:"lastTime"` // 最近一次执行时间
|
||||
LastStatus string `json:"lastStatus"` // 最近一次执行状态
|
||||
LastResult string `json:"lastResult"` // 最近一次执行结果
|
||||
@@ -27,6 +28,13 @@ type DbRestore struct {
|
||||
func (restore *DbRestore) MarshalJSON() ([]byte, error) {
|
||||
type dbBackup DbRestore
|
||||
restore.IntervalDay = uint64(restore.Interval / time.Hour / 24)
|
||||
if len(restore.EnabledDesc) == 0 {
|
||||
if restore.Enabled {
|
||||
restore.EnabledDesc = "任务已启用"
|
||||
} else {
|
||||
restore.EnabledDesc = "任务已禁用"
|
||||
}
|
||||
}
|
||||
return json.Marshal((*dbBackup)(restore))
|
||||
}
|
||||
|
||||
|
||||
@@ -2,91 +2,50 @@ package application
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mayfly-go/internal/db/domain/repository"
|
||||
"mayfly-go/internal/db/infrastructure/persistence"
|
||||
tagapp "mayfly-go/internal/tag/application"
|
||||
"mayfly-go/pkg/ioc"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
instanceApp Instance
|
||||
dbApp Db
|
||||
dbSqlExecApp DbSqlExec
|
||||
dbSqlApp DbSql
|
||||
dbBackupApp *DbBackupApp
|
||||
dbRestoreApp *DbRestoreApp
|
||||
dbBinlogApp *DbBinlogApp
|
||||
dataSyncApp DataSyncTask
|
||||
)
|
||||
func InitIoc() {
|
||||
persistence.Init()
|
||||
|
||||
ioc.Register(new(instanceAppImpl), ioc.WithComponentName("DbInstanceApp"))
|
||||
ioc.Register(new(dbAppImpl), ioc.WithComponentName("DbApp"))
|
||||
ioc.Register(new(dbSqlExecAppImpl), ioc.WithComponentName("DbSqlExecApp"))
|
||||
ioc.Register(new(dbSqlAppImpl), ioc.WithComponentName("DbSqlApp"))
|
||||
ioc.Register(new(dataSyncAppImpl), ioc.WithComponentName("DbDataSyncTaskApp"))
|
||||
|
||||
ioc.Register(newDbScheduler(), ioc.WithComponentName("DbScheduler"))
|
||||
ioc.Register(new(DbBackupApp), ioc.WithComponentName("DbBackupApp"))
|
||||
ioc.Register(new(DbRestoreApp), ioc.WithComponentName("DbRestoreApp"))
|
||||
ioc.Register(newDbBinlogApp(), ioc.WithComponentName("DbBinlogApp"))
|
||||
}
|
||||
|
||||
func Init() {
|
||||
sync.OnceFunc(func() {
|
||||
repositories := &repository.Repositories{
|
||||
Instance: persistence.GetInstanceRepo(),
|
||||
Backup: persistence.NewDbBackupRepo(),
|
||||
BackupHistory: persistence.NewDbBackupHistoryRepo(),
|
||||
Restore: persistence.NewDbRestoreRepo(),
|
||||
RestoreHistory: persistence.NewDbRestoreHistoryRepo(),
|
||||
Binlog: persistence.NewDbBinlogRepo(),
|
||||
BinlogHistory: persistence.NewDbBinlogHistoryRepo(),
|
||||
}
|
||||
var err error
|
||||
instanceRepo := persistence.GetInstanceRepo()
|
||||
instanceApp = newInstanceApp(instanceRepo)
|
||||
dbApp = newDbApp(persistence.GetDbRepo(), persistence.GetDbSqlRepo(), instanceApp, tagapp.GetTagTreeApp())
|
||||
dbSqlExecApp = newDbSqlExecApp(persistence.GetDbSqlExecRepo())
|
||||
dbSqlApp = newDbSqlApp(persistence.GetDbSqlRepo())
|
||||
dataSyncApp = newDataSyncApp(persistence.GetDataSyncTaskRepo(), persistence.GetDataSyncLogRepo())
|
||||
|
||||
scheduler, err := newDbScheduler(repositories)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("初始化 dbScheduler 失败: %v", err))
|
||||
}
|
||||
dbBackupApp, err = newDbBackupApp(repositories, dbApp, scheduler)
|
||||
if err != nil {
|
||||
if err := GetDbBackupApp().Init(); err != nil {
|
||||
panic(fmt.Sprintf("初始化 dbBackupApp 失败: %v", err))
|
||||
}
|
||||
dbRestoreApp, err = newDbRestoreApp(repositories, dbApp, scheduler)
|
||||
if err != nil {
|
||||
if err := GetDbRestoreApp().Init(); err != nil {
|
||||
panic(fmt.Sprintf("初始化 dbRestoreApp 失败: %v", err))
|
||||
}
|
||||
dbBinlogApp, err = newDbBinlogApp(repositories, dbApp, scheduler)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("初始化 dbBinlogApp 失败: %v", err))
|
||||
}
|
||||
|
||||
dataSyncApp.InitCronJob()
|
||||
GetDataSyncTaskApp().InitCronJob()
|
||||
})()
|
||||
}
|
||||
|
||||
func GetInstanceApp() Instance {
|
||||
return instanceApp
|
||||
}
|
||||
|
||||
func GetDbApp() Db {
|
||||
return dbApp
|
||||
}
|
||||
|
||||
func GetDbSqlApp() DbSql {
|
||||
return dbSqlApp
|
||||
}
|
||||
|
||||
func GetDbSqlExecApp() DbSqlExec {
|
||||
return dbSqlExecApp
|
||||
}
|
||||
|
||||
func GetDbBackupApp() *DbBackupApp {
|
||||
return dbBackupApp
|
||||
return ioc.Get[*DbBackupApp]("DbBackupApp")
|
||||
}
|
||||
|
||||
func GetDbRestoreApp() *DbRestoreApp {
|
||||
return dbRestoreApp
|
||||
return ioc.Get[*DbRestoreApp]("DbRestoreApp")
|
||||
}
|
||||
|
||||
func GetDbBinlogApp() *DbBinlogApp {
|
||||
return dbBinlogApp
|
||||
return ioc.Get[*DbBinlogApp]("DbBinlogApp")
|
||||
}
|
||||
|
||||
func GetDataSyncTaskApp() DataSyncTask {
|
||||
return dataSyncApp
|
||||
return ioc.Get[DataSyncTask]("DbDataSyncTaskApp")
|
||||
}
|
||||
|
||||
@@ -40,22 +40,17 @@ type Db interface {
|
||||
GetDbConnByInstanceId(instanceId uint64) (*dbi.DbConn, error)
|
||||
}
|
||||
|
||||
func newDbApp(dbRepo repository.Db, dbSqlRepo repository.DbSql, dbInstanceApp Instance, tagApp tagapp.TagTree) Db {
|
||||
app := &dbAppImpl{
|
||||
dbSqlRepo: dbSqlRepo,
|
||||
dbInstanceApp: dbInstanceApp,
|
||||
tagApp: tagApp,
|
||||
}
|
||||
app.Repo = dbRepo
|
||||
return app
|
||||
}
|
||||
|
||||
type dbAppImpl struct {
|
||||
base.AppImpl[*entity.Db, repository.Db]
|
||||
|
||||
dbSqlRepo repository.DbSql
|
||||
dbInstanceApp Instance
|
||||
tagApp tagapp.TagTree
|
||||
dbSqlRepo repository.DbSql `inject:"DbSqlRepo"`
|
||||
dbInstanceApp Instance `inject:"DbInstanceApp"`
|
||||
tagApp tagapp.TagTree `inject:"TagTreeApp"`
|
||||
}
|
||||
|
||||
// 注入DbRepo
|
||||
func (d *dbAppImpl) InjectDbRepo(repo repository.Db) {
|
||||
d.Repo = repo
|
||||
}
|
||||
|
||||
// 分页获取数据库信息列表
|
||||
@@ -103,9 +98,13 @@ func (d *dbAppImpl) SaveDb(ctx context.Context, dbEntity *entity.Db, tagIds ...u
|
||||
// 比较新旧数据库列表,需要将移除的数据库相关联的信息删除
|
||||
_, delDb, _ := collx.ArrayCompare(newDbs, oldDbs)
|
||||
|
||||
for _, v := range delDb {
|
||||
// 先简单关闭可能存在的旧库连接(可能改了关联标签导致DbConn.Info.TagPath与修改后的标签不一致、导致操作权限校验出错)
|
||||
for _, v := range oldDbs {
|
||||
// 关闭数据库连接
|
||||
dbm.CloseDb(dbEntity.Id, v)
|
||||
}
|
||||
|
||||
for _, v := range delDb {
|
||||
// 删除该库关联的所有sql记录
|
||||
d.dbSqlRepo.DeleteByCond(ctx, &entity.DbSql{DbId: dbId, Db: v})
|
||||
}
|
||||
@@ -155,7 +154,7 @@ func (d *dbAppImpl) GetDbConn(dbId uint64, dbName string) (*dbi.DbConn, error) {
|
||||
|
||||
checkDb := dbName
|
||||
// 兼容pgsql/dm db/schema模式
|
||||
if dbi.DbTypePostgres.Equal(instance.Type) || dbi.DbTypeDM.Equal(instance.Type) || dbi.DbTypeOracle.Equal(instance.Type) {
|
||||
if dbi.DbTypePostgres.Equal(instance.Type) || dbi.DbTypeGauss.Equal(instance.Type) || dbi.DbTypeDM.Equal(instance.Type) || dbi.DbTypeOracle.Equal(instance.Type) || dbi.DbTypeMssql.Equal(instance.Type) {
|
||||
ss := strings.Split(dbName, "/")
|
||||
if len(ss) > 1 {
|
||||
checkDb = ss[0]
|
||||
|
||||
@@ -3,36 +3,36 @@ package application
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"github.com/google/uuid"
|
||||
"errors"
|
||||
"fmt"
|
||||
"gorm.io/gorm"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
"mayfly-go/internal/db/domain/repository"
|
||||
"mayfly-go/pkg/logx"
|
||||
"mayfly-go/pkg/model"
|
||||
"sync"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func newDbBackupApp(repositories *repository.Repositories, dbApp Db, scheduler *dbScheduler) (*DbBackupApp, error) {
|
||||
var jobs []*entity.DbBackup
|
||||
if err := repositories.Backup.ListToDo(&jobs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := scheduler.AddJob(context.Background(), false, entity.DbJobTypeBackup, jobs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
app := &DbBackupApp{
|
||||
backupRepo: repositories.Backup,
|
||||
instanceRepo: repositories.Instance,
|
||||
backupHistoryRepo: repositories.BackupHistory,
|
||||
dbApp: dbApp,
|
||||
scheduler: scheduler,
|
||||
}
|
||||
return app, nil
|
||||
type DbBackupApp struct {
|
||||
scheduler *dbScheduler `inject:"DbScheduler"`
|
||||
backupRepo repository.DbBackup `inject:"DbBackupRepo"`
|
||||
backupHistoryRepo repository.DbBackupHistory `inject:"DbBackupHistoryRepo"`
|
||||
restoreRepo repository.DbRestore `inject:"DbRestoreRepo"`
|
||||
dbApp Db `inject:"DbApp"`
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
type DbBackupApp struct {
|
||||
backupRepo repository.DbBackup
|
||||
instanceRepo repository.Instance
|
||||
backupHistoryRepo repository.DbBackupHistory
|
||||
dbApp Db
|
||||
scheduler *dbScheduler
|
||||
func (app *DbBackupApp) Init() error {
|
||||
var jobs []*entity.DbBackup
|
||||
if err := app.backupRepo.ListToDo(&jobs); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := app.scheduler.AddJob(context.Background(), jobs); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) Close() {
|
||||
@@ -40,32 +40,111 @@ func (app *DbBackupApp) Close() {
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) Create(ctx context.Context, jobs []*entity.DbBackup) error {
|
||||
return app.scheduler.AddJob(ctx, true /* 保存到数据库 */, entity.DbJobTypeBackup, jobs)
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
if err := app.backupRepo.AddJob(ctx, jobs); err != nil {
|
||||
return err
|
||||
}
|
||||
return app.scheduler.AddJob(ctx, jobs)
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) Update(ctx context.Context, job *entity.DbBackup) error {
|
||||
return app.scheduler.UpdateJob(ctx, job)
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
if err := app.backupRepo.UpdateById(ctx, job); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = app.scheduler.UpdateJob(ctx, job)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) Delete(ctx context.Context, jobId uint64) error {
|
||||
// todo: 删除数据库备份历史文件
|
||||
return app.scheduler.RemoveJob(ctx, entity.DbJobTypeBackup, jobId)
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
if err := app.scheduler.RemoveJob(ctx, entity.DbJobTypeBackup, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
history := &entity.DbBackupHistory{
|
||||
DbBackupId: jobId,
|
||||
}
|
||||
err := app.backupHistoryRepo.GetBy(history, "name")
|
||||
switch {
|
||||
default:
|
||||
return err
|
||||
case err == nil:
|
||||
return fmt.Errorf("数据库备份存在历史记录【%s】,无法删除该任务", history.Name)
|
||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||
}
|
||||
if err := app.backupRepo.DeleteById(ctx, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) Enable(ctx context.Context, jobId uint64) error {
|
||||
return app.scheduler.EnableJob(ctx, entity.DbJobTypeBackup, jobId)
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
repo := app.backupRepo
|
||||
job := &entity.DbBackup{}
|
||||
if err := repo.GetById(job, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
if job.IsEnabled() {
|
||||
return nil
|
||||
}
|
||||
if job.IsExpired() {
|
||||
return errors.New("任务已过期")
|
||||
}
|
||||
_ = app.scheduler.EnableJob(ctx, job)
|
||||
if err := repo.UpdateEnabled(ctx, jobId, true); err != nil {
|
||||
logx.Errorf("数据库备份任务已启用( jobId: %d ),任务状态保存失败: %v", jobId, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) Disable(ctx context.Context, jobId uint64) error {
|
||||
return app.scheduler.DisableJob(ctx, entity.DbJobTypeBackup, jobId)
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
repo := app.backupRepo
|
||||
job := &entity.DbBackup{}
|
||||
if err := repo.GetById(job, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
if !job.IsEnabled() {
|
||||
return nil
|
||||
}
|
||||
_ = app.scheduler.DisableJob(ctx, entity.DbJobTypeBackup, jobId)
|
||||
if err := repo.UpdateEnabled(ctx, jobId, false); err != nil {
|
||||
logx.Errorf("数据库恢复任务已禁用( jobId: %d ),任务状态保存失败: %v", jobId, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) Start(ctx context.Context, jobId uint64) error {
|
||||
return app.scheduler.StartJobNow(ctx, entity.DbJobTypeBackup, jobId)
|
||||
func (app *DbBackupApp) StartNow(ctx context.Context, jobId uint64) error {
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
job := &entity.DbBackup{}
|
||||
if err := app.backupRepo.GetById(job, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
if !job.IsEnabled() {
|
||||
return errors.New("任务未启用")
|
||||
}
|
||||
_ = app.scheduler.StartJobNow(ctx, job)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPageList 分页获取数据库备份任务
|
||||
func (app *DbBackupApp) GetPageList(condition *entity.DbJobQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
func (app *DbBackupApp) GetPageList(condition *entity.DbBackupQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
return app.backupRepo.GetPageList(condition, pageParam, toEntity, orderBy...)
|
||||
}
|
||||
|
||||
@@ -76,7 +155,11 @@ func (app *DbBackupApp) GetDbNamesWithoutBackup(instanceId uint64, dbNames []str
|
||||
|
||||
// GetHistoryPageList 分页获取数据库备份历史
|
||||
func (app *DbBackupApp) GetHistoryPageList(condition *entity.DbBackupHistoryQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
return app.backupHistoryRepo.GetHistories(condition, pageParam, toEntity, orderBy...)
|
||||
return app.backupHistoryRepo.GetPageList(condition, pageParam, toEntity, orderBy...)
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) GetHistories(backupHistoryIds []uint64, toEntity any) error {
|
||||
return app.backupHistoryRepo.GetHistories(backupHistoryIds, toEntity)
|
||||
}
|
||||
|
||||
func NewIncUUID() (uuid.UUID, error) {
|
||||
@@ -99,3 +182,41 @@ func NewIncUUID() (uuid.UUID, error) {
|
||||
|
||||
return uid, nil
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) DeleteHistory(ctx context.Context, historyId uint64) (retErr error) {
|
||||
// todo: 删除数据库备份历史文件
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
ok, err := app.backupHistoryRepo.UpdateDeleting(true, historyId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_, err = app.backupHistoryRepo.UpdateDeleting(false, historyId)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if retErr == nil {
|
||||
retErr = err
|
||||
return
|
||||
}
|
||||
retErr = fmt.Errorf("%w, %w", retErr, err)
|
||||
}()
|
||||
if !ok {
|
||||
return errors.New("正在从备份历史中恢复数据库")
|
||||
}
|
||||
job := &entity.DbBackupHistory{}
|
||||
if err := app.backupHistoryRepo.GetById(job, historyId); err != nil {
|
||||
return err
|
||||
}
|
||||
conn, err := app.dbApp.GetDbConnByInstanceId(job.DbInstanceId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dbProgram := conn.GetDialect().GetDbProgram()
|
||||
if err := dbProgram.RemoveBackupHistory(ctx, job.DbBackupId, job.Uuid); err != nil {
|
||||
return err
|
||||
}
|
||||
return app.backupHistoryRepo.DeleteById(ctx, historyId)
|
||||
}
|
||||
|
||||
@@ -11,32 +11,24 @@ import (
|
||||
)
|
||||
|
||||
type DbBinlogApp struct {
|
||||
binlogRepo repository.DbBinlog
|
||||
binlogHistoryRepo repository.DbBinlogHistory
|
||||
backupRepo repository.DbBackup
|
||||
backupHistoryRepo repository.DbBackupHistory
|
||||
dbApp Db
|
||||
context context.Context
|
||||
cancel context.CancelFunc
|
||||
waitGroup sync.WaitGroup
|
||||
scheduler *dbScheduler
|
||||
scheduler *dbScheduler `inject:"DbScheduler"`
|
||||
binlogRepo repository.DbBinlog `inject:"DbBinlogRepo"`
|
||||
backupRepo repository.DbBackup `inject:"DbBackupRepo"`
|
||||
|
||||
context context.Context
|
||||
cancel context.CancelFunc
|
||||
waitGroup sync.WaitGroup
|
||||
}
|
||||
|
||||
func newDbBinlogApp(repositories *repository.Repositories, dbApp Db, scheduler *dbScheduler) (*DbBinlogApp, error) {
|
||||
func newDbBinlogApp() *DbBinlogApp {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
svc := &DbBinlogApp{
|
||||
binlogRepo: repositories.Binlog,
|
||||
binlogHistoryRepo: repositories.BinlogHistory,
|
||||
backupRepo: repositories.Backup,
|
||||
backupHistoryRepo: repositories.BackupHistory,
|
||||
dbApp: dbApp,
|
||||
scheduler: scheduler,
|
||||
context: ctx,
|
||||
cancel: cancel,
|
||||
context: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
svc.waitGroup.Add(1)
|
||||
go svc.run()
|
||||
return svc, nil
|
||||
return svc
|
||||
}
|
||||
|
||||
func (app *DbBinlogApp) run() {
|
||||
@@ -54,7 +46,7 @@ func (app *DbBinlogApp) run() {
|
||||
if app.closed() {
|
||||
break
|
||||
}
|
||||
if err := app.scheduler.AddJob(app.context, false, entity.DbJobTypeBinlog, jobs); err != nil {
|
||||
if err := app.scheduler.AddJob(app.context, jobs); err != nil {
|
||||
logx.Error("DbBinlogApp: 添加 BINLOG 同步任务失败: ", err.Error())
|
||||
}
|
||||
timex.SleepWithContext(app.context, entity.BinlogDownloadInterval)
|
||||
|
||||
@@ -14,7 +14,12 @@ import (
|
||||
"mayfly-go/pkg/logx"
|
||||
"mayfly-go/pkg/model"
|
||||
"mayfly-go/pkg/scheduler"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type DataSyncTask interface {
|
||||
@@ -38,17 +43,20 @@ type DataSyncTask interface {
|
||||
GetTaskLogList(condition *entity.DataSyncLogQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
||||
}
|
||||
|
||||
func newDataSyncApp(dataSyncRepo repository.DataSyncTask, dataSyncLogRepo repository.DataSyncLog) DataSyncTask {
|
||||
app := new(dataSyncAppImpl)
|
||||
app.Repo = dataSyncRepo
|
||||
app.dataSyncLogRepo = dataSyncLogRepo
|
||||
return app
|
||||
}
|
||||
|
||||
type dataSyncAppImpl struct {
|
||||
base.AppImpl[*entity.DataSyncTask, repository.DataSyncTask]
|
||||
|
||||
dataSyncLogRepo repository.DataSyncLog
|
||||
dbDataSyncLogRepo repository.DataSyncLog `inject:"DbDataSyncLogRepo"`
|
||||
|
||||
dbApp Db `inject:"DbApp"`
|
||||
}
|
||||
|
||||
var (
|
||||
dateTimeReg = regexp.MustCompile(`^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$`)
|
||||
)
|
||||
|
||||
func (app *dataSyncAppImpl) InjectDbDataSyncTaskRepo(repo repository.DataSyncTask) {
|
||||
app.Repo = repo
|
||||
}
|
||||
|
||||
func (app *dataSyncAppImpl) GetPageList(condition *entity.DataSyncTaskQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
@@ -58,15 +66,22 @@ func (app *dataSyncAppImpl) GetPageList(condition *entity.DataSyncTaskQuery, pag
|
||||
func (app *dataSyncAppImpl) Save(ctx context.Context, taskEntity *entity.DataSyncTask) error {
|
||||
var err error
|
||||
if taskEntity.Id == 0 {
|
||||
// 新建时生成key
|
||||
taskEntity.TaskKey = uuid.New().String()
|
||||
err = app.Insert(ctx, taskEntity)
|
||||
} else {
|
||||
err = app.UpdateById(ctx, taskEntity)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
app.AddCronJob(taskEntity)
|
||||
task, err := app.GetById(new(entity.DataSyncTask), taskEntity.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
app.AddCronJob(task)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -85,8 +100,10 @@ func (app *dataSyncAppImpl) AddCronJob(taskEntity *entity.DataSyncTask) {
|
||||
|
||||
// 根据状态添加新的任务
|
||||
if taskEntity.Status == entity.DataSyncTaskStatusEnable {
|
||||
taskId := taskEntity.Id
|
||||
scheduler.AddFunByKey(key, taskEntity.TaskCron, func() {
|
||||
if err := app.RunCronJob(taskEntity.Id); err != nil {
|
||||
logx.Infof("开始执行同步任务: %d", taskId)
|
||||
if err := app.RunCronJob(taskId); err != nil {
|
||||
logx.Errorf("定时执行数据同步任务失败: %s", err.Error())
|
||||
}
|
||||
})
|
||||
@@ -126,7 +143,23 @@ func (app *dataSyncAppImpl) RunCronJob(id uint64) error {
|
||||
updSql := ""
|
||||
orderSql := ""
|
||||
if task.UpdFieldVal != "0" && task.UpdFieldVal != "" && task.UpdField != "" {
|
||||
updSql = fmt.Sprintf("and %s > '%s'", task.UpdField, task.UpdFieldVal)
|
||||
srcConn, _ := app.dbApp.GetDbConn(uint64(task.SrcDbId), task.SrcDbName)
|
||||
|
||||
task.UpdFieldVal = strings.Trim(task.UpdFieldVal, " ")
|
||||
// 把UpdFieldVal尝试转为int,如果可以转为int,则不添加引号,否则添加引号
|
||||
if _, err := strconv.Atoi(task.UpdFieldVal); err != nil {
|
||||
updSql = fmt.Sprintf("and %s > '%s'", task.UpdField, task.UpdFieldVal)
|
||||
} else {
|
||||
updSql = fmt.Sprintf("and %s > %s", task.UpdField, task.UpdFieldVal)
|
||||
}
|
||||
|
||||
// 如果是oracle且数据类型是时间类型,则需要加上to_date函数
|
||||
if srcConn.Info.Type == dbi.DbTypeOracle {
|
||||
// 用正则判断数据类型是时间
|
||||
if dateTimeReg.MatchString(task.UpdFieldVal) {
|
||||
updSql = fmt.Sprintf("and %s > to_date('%s','yyyy-mm-dd hh24:mi:ss')", task.UpdField, task.UpdFieldVal)
|
||||
}
|
||||
}
|
||||
orderSql = "order by " + task.UpdField + " asc "
|
||||
}
|
||||
// 组装查询sql
|
||||
@@ -153,13 +186,13 @@ func (app *dataSyncAppImpl) doDataSync(sql string, task *entity.DataSyncTask) (*
|
||||
}
|
||||
|
||||
// 获取源数据库连接
|
||||
srcConn, err := GetDbApp().GetDbConn(uint64(task.SrcDbId), task.SrcDbName)
|
||||
srcConn, err := app.dbApp.GetDbConn(uint64(task.SrcDbId), task.SrcDbName)
|
||||
if err != nil {
|
||||
return syncLog, errorx.NewBiz("连接源数据库失败: %s", err.Error())
|
||||
}
|
||||
|
||||
// 获取目标数据库连接
|
||||
targetConn, err := GetDbApp().GetDbConn(uint64(task.TargetDbId), task.TargetDbName)
|
||||
targetConn, err := app.dbApp.GetDbConn(uint64(task.TargetDbId), task.TargetDbName)
|
||||
if err != nil {
|
||||
return syncLog, errorx.NewBiz("连接目标数据库失败: %s", err.Error())
|
||||
}
|
||||
@@ -197,8 +230,8 @@ func (app *dataSyncAppImpl) doDataSync(sql string, task *entity.DataSyncTask) (*
|
||||
// 遍历columns 取task.UpdField的字段类型
|
||||
updFieldType = dbi.DataTypeString
|
||||
for _, column := range columns {
|
||||
if column.Name == task.UpdField {
|
||||
updFieldType = srcDialect.GetDataType(column.Type)
|
||||
if strings.EqualFold(strings.ToLower(column.Name), strings.ToLower(task.UpdField)) {
|
||||
updFieldType = srcDialect.GetDataConverter().GetDataType(column.Type)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -207,7 +240,7 @@ func (app *dataSyncAppImpl) doDataSync(sql string, task *entity.DataSyncTask) (*
|
||||
total++
|
||||
result = append(result, row)
|
||||
if total%batchSize == 0 {
|
||||
if err := app.srcData2TargetDb(result, fieldMap, updFieldType, task, srcDialect, targetConn, targetDbTx); err != nil {
|
||||
if err := app.srcData2TargetDb(result, fieldMap, columns, updFieldType, task, srcDialect, targetConn, targetDbTx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -229,7 +262,7 @@ func (app *dataSyncAppImpl) doDataSync(sql string, task *entity.DataSyncTask) (*
|
||||
|
||||
// 处理剩余的数据
|
||||
if len(result) > 0 {
|
||||
if err := app.srcData2TargetDb(result, fieldMap, updFieldType, task, srcDialect, targetConn, targetDbTx); err != nil {
|
||||
if err := app.srcData2TargetDb(result, fieldMap, queryColumns, updFieldType, task, srcDialect, targetConn, targetDbTx); err != nil {
|
||||
targetDbTx.Rollback()
|
||||
return syncLog, err
|
||||
}
|
||||
@@ -249,10 +282,16 @@ func (app *dataSyncAppImpl) doDataSync(sql string, task *entity.DataSyncTask) (*
|
||||
return syncLog, nil
|
||||
}
|
||||
|
||||
func (app *dataSyncAppImpl) srcData2TargetDb(srcRes []map[string]any, fieldMap []map[string]string, updFieldType dbi.DataType, task *entity.DataSyncTask, srcDialect dbi.Dialect, targetDbConn *dbi.DbConn, targetDbTx *sql.Tx) error {
|
||||
var data = make([]map[string]any, 0)
|
||||
func (app *dataSyncAppImpl) srcData2TargetDb(srcRes []map[string]any, fieldMap []map[string]string, columns []*dbi.QueryColumn, updFieldType dbi.DataType, task *entity.DataSyncTask, srcDialect dbi.Dialect, targetDbConn *dbi.DbConn, targetDbTx *sql.Tx) error {
|
||||
|
||||
// 遍历res,组装插入sql
|
||||
// 遍历src字段列表,取出字段对应的类型
|
||||
var srcColumnTypes = make(map[string]string)
|
||||
for _, column := range columns {
|
||||
srcColumnTypes[column.Name] = column.Type
|
||||
}
|
||||
|
||||
// 遍历res,组装数据
|
||||
var data = make([]map[string]any, 0)
|
||||
for _, record := range srcRes {
|
||||
var rowData = make(map[string]any)
|
||||
// 遍历字段映射, target字段的值为src字段取值
|
||||
@@ -265,18 +304,23 @@ func (app *dataSyncAppImpl) srcData2TargetDb(srcRes []map[string]any, fieldMap [
|
||||
|
||||
data = append(data, rowData)
|
||||
}
|
||||
// 解决字段大小写问题
|
||||
updFieldVal := srcRes[len(srcRes)-1][strings.ToUpper(task.UpdField)]
|
||||
if updFieldVal == "" || updFieldVal == nil {
|
||||
updFieldVal = srcRes[len(srcRes)-1][strings.ToLower(task.UpdField)]
|
||||
}
|
||||
|
||||
updFieldVal := fmt.Sprintf("%v", srcRes[len(srcRes)-1][task.UpdField])
|
||||
updFieldVal = srcDialect.FormatStrData(updFieldVal, updFieldType)
|
||||
task.UpdFieldVal = updFieldVal
|
||||
task.UpdFieldVal = srcDialect.GetDataConverter().FormatData(updFieldVal, updFieldType)
|
||||
|
||||
// 获取目标库字段数组
|
||||
targetWrapColumns := make([]string, 0)
|
||||
// 获取源库字段数组
|
||||
srcColumns := make([]string, 0)
|
||||
srcFieldTypes := make(map[string]dbi.DataType)
|
||||
for _, item := range fieldMap {
|
||||
targetField := item["target"]
|
||||
srcField := item["target"]
|
||||
srcFieldTypes[srcField] = srcDialect.GetDataConverter().GetDataType(srcColumnTypes[item["src"]])
|
||||
targetWrapColumns = append(targetWrapColumns, targetDbConn.Info.Type.QuoteIdentifier(targetField))
|
||||
srcColumns = append(srcColumns, srcField)
|
||||
}
|
||||
@@ -286,7 +330,9 @@ func (app *dataSyncAppImpl) srcData2TargetDb(srcRes []map[string]any, fieldMap [
|
||||
for _, record := range data {
|
||||
rawValue := make([]any, 0)
|
||||
for _, column := range srcColumns {
|
||||
rawValue = append(rawValue, record[column])
|
||||
// 某些情况,如oracle,需要转换时间类型的字符串为time类型
|
||||
res := srcDialect.GetDataConverter().ParseData(record[column], srcFieldTypes[column])
|
||||
rawValue = append(rawValue, res)
|
||||
}
|
||||
values = append(values, rawValue)
|
||||
}
|
||||
@@ -328,7 +374,7 @@ func (app *dataSyncAppImpl) endRunning(taskEntity *entity.DataSyncTask, log *ent
|
||||
}
|
||||
|
||||
func (app *dataSyncAppImpl) saveLog(log *entity.DataSyncLog) {
|
||||
app.dataSyncLogRepo.Save(context.Background(), log)
|
||||
app.dbDataSyncLogRepo.Save(context.Background(), log)
|
||||
}
|
||||
|
||||
func (app *dataSyncAppImpl) InitCronJob() {
|
||||
@@ -374,5 +420,5 @@ func (app *dataSyncAppImpl) InitCronJob() {
|
||||
}
|
||||
|
||||
func (app *dataSyncAppImpl) GetTaskLogList(condition *entity.DataSyncLogQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
return app.dataSyncLogRepo.GetTaskLogList(condition, pageParam, toEntity, orderBy...)
|
||||
return app.dbDataSyncLogRepo.GetTaskLogList(condition, pageParam, toEntity, orderBy...)
|
||||
}
|
||||
|
||||
@@ -2,71 +2,131 @@ package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
"mayfly-go/internal/db/domain/repository"
|
||||
"mayfly-go/pkg/logx"
|
||||
"mayfly-go/pkg/model"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func newDbRestoreApp(repositories *repository.Repositories, dbApp Db, scheduler *dbScheduler) (*DbRestoreApp, error) {
|
||||
var jobs []*entity.DbRestore
|
||||
if err := repositories.Restore.ListToDo(&jobs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := scheduler.AddJob(context.Background(), false, entity.DbJobTypeRestore, jobs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
app := &DbRestoreApp{
|
||||
restoreRepo: repositories.Restore,
|
||||
instanceRepo: repositories.Instance,
|
||||
backupHistoryRepo: repositories.BackupHistory,
|
||||
restoreHistoryRepo: repositories.RestoreHistory,
|
||||
binlogHistoryRepo: repositories.BinlogHistory,
|
||||
dbApp: dbApp,
|
||||
scheduler: scheduler,
|
||||
}
|
||||
return app, nil
|
||||
type DbRestoreApp struct {
|
||||
scheduler *dbScheduler `inject:"DbScheduler"`
|
||||
restoreRepo repository.DbRestore `inject:"DbRestoreRepo"`
|
||||
restoreHistoryRepo repository.DbRestoreHistory `inject:"DbRestoreHistoryRepo"`
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
type DbRestoreApp struct {
|
||||
restoreRepo repository.DbRestore
|
||||
instanceRepo repository.Instance
|
||||
backupHistoryRepo repository.DbBackupHistory
|
||||
restoreHistoryRepo repository.DbRestoreHistory
|
||||
binlogHistoryRepo repository.DbBinlogHistory
|
||||
dbApp Db
|
||||
scheduler *dbScheduler
|
||||
func (app *DbRestoreApp) Init() error {
|
||||
var jobs []*entity.DbRestore
|
||||
if err := app.restoreRepo.ListToDo(&jobs); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := app.scheduler.AddJob(context.Background(), jobs); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbRestoreApp) Close() {
|
||||
app.scheduler.Close()
|
||||
}
|
||||
|
||||
func (app *DbRestoreApp) Create(ctx context.Context, job *entity.DbRestore) error {
|
||||
return app.scheduler.AddJob(ctx, true /* 保存到数据库 */, entity.DbJobTypeRestore, job)
|
||||
func (app *DbRestoreApp) Create(ctx context.Context, jobs any) error {
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
if err := app.restoreRepo.AddJob(ctx, jobs); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = app.scheduler.AddJob(ctx, jobs)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbRestoreApp) Update(ctx context.Context, job *entity.DbRestore) error {
|
||||
return app.scheduler.UpdateJob(ctx, job)
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
if err := app.restoreRepo.UpdateById(ctx, job); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = app.scheduler.UpdateJob(ctx, job)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbRestoreApp) Delete(ctx context.Context, jobId uint64) error {
|
||||
// todo: 删除数据库恢复历史文件
|
||||
return app.scheduler.RemoveJob(ctx, entity.DbJobTypeRestore, jobId)
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
if err := app.scheduler.RemoveJob(ctx, entity.DbJobTypeRestore, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
history := &entity.DbRestoreHistory{
|
||||
DbRestoreId: jobId,
|
||||
}
|
||||
if err := app.restoreHistoryRepo.DeleteByCond(ctx, history); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := app.restoreRepo.DeleteById(ctx, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbRestoreApp) Enable(ctx context.Context, jobId uint64) error {
|
||||
return app.scheduler.EnableJob(ctx, entity.DbJobTypeRestore, jobId)
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
repo := app.restoreRepo
|
||||
job := &entity.DbRestore{}
|
||||
if err := repo.GetById(job, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
if job.IsEnabled() {
|
||||
return nil
|
||||
}
|
||||
if job.IsExpired() {
|
||||
return errors.New("任务已过期")
|
||||
}
|
||||
_ = app.scheduler.EnableJob(ctx, job)
|
||||
if err := repo.UpdateEnabled(ctx, jobId, true); err != nil {
|
||||
logx.Errorf("数据库恢复任务已启用( jobId: %d ),任务状态保存失败: %v", jobId, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbRestoreApp) Disable(ctx context.Context, jobId uint64) error {
|
||||
return app.scheduler.DisableJob(ctx, entity.DbJobTypeRestore, jobId)
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
repo := app.restoreRepo
|
||||
job := &entity.DbRestore{}
|
||||
if err := repo.GetById(job, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
if !job.IsEnabled() {
|
||||
return nil
|
||||
}
|
||||
_ = app.scheduler.DisableJob(ctx, entity.DbJobTypeRestore, jobId)
|
||||
if err := repo.UpdateEnabled(ctx, jobId, false); err != nil {
|
||||
logx.Errorf("数据库恢复任务已禁用( jobId: %d ),任务状态保存失败: %v", jobId, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPageList 分页获取数据库恢复任务
|
||||
func (app *DbRestoreApp) GetPageList(condition *entity.DbJobQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
func (app *DbRestoreApp) GetPageList(condition *entity.DbRestoreQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
return app.restoreRepo.GetPageList(condition, pageParam, toEntity, orderBy...)
|
||||
}
|
||||
|
||||
// GetRestoresEnabled 获取数据库恢复任务
|
||||
func (app *DbRestoreApp) GetRestoresEnabled(toEntity any, backupHistoryId ...uint64) error {
|
||||
return app.restoreRepo.GetEnabledRestores(toEntity, backupHistoryId...)
|
||||
}
|
||||
|
||||
// GetDbNamesWithoutRestore 获取未配置定时恢复的数据库名称
|
||||
func (app *DbRestoreApp) GetDbNamesWithoutRestore(instanceId uint64, dbNames []string) ([]string, error) {
|
||||
return app.restoreRepo.GetDbNamesWithoutRestore(instanceId, dbNames)
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"gorm.io/gorm"
|
||||
"mayfly-go/internal/db/dbm/dbi"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
"mayfly-go/internal/db/domain/repository"
|
||||
"mayfly-go/pkg/logx"
|
||||
"mayfly-go/pkg/runner"
|
||||
"reflect"
|
||||
"sync"
|
||||
@@ -21,58 +21,34 @@ const (
|
||||
type dbScheduler struct {
|
||||
mutex sync.Mutex
|
||||
runner *runner.Runner[entity.DbJob]
|
||||
dbApp Db
|
||||
backupRepo repository.DbBackup
|
||||
backupHistoryRepo repository.DbBackupHistory
|
||||
restoreRepo repository.DbRestore
|
||||
restoreHistoryRepo repository.DbRestoreHistory
|
||||
binlogRepo repository.DbBinlog
|
||||
binlogHistoryRepo repository.DbBinlogHistory
|
||||
binlogTimes map[uint64]time.Time
|
||||
dbApp Db `inject:"DbApp"`
|
||||
backupRepo repository.DbBackup `inject:"DbBackupRepo"`
|
||||
backupHistoryRepo repository.DbBackupHistory `inject:"DbBackupHistoryRepo"`
|
||||
restoreRepo repository.DbRestore `inject:"DbRestoreRepo"`
|
||||
restoreHistoryRepo repository.DbRestoreHistory `inject:"DbRestoreHistoryRepo"`
|
||||
binlogRepo repository.DbBinlog `inject:"DbBinlogRepo"`
|
||||
binlogHistoryRepo repository.DbBinlogHistory `inject:"DbBinlogHistoryRepo"`
|
||||
}
|
||||
|
||||
func newDbScheduler(repositories *repository.Repositories) (*dbScheduler, error) {
|
||||
scheduler := &dbScheduler{
|
||||
dbApp: dbApp,
|
||||
backupRepo: repositories.Backup,
|
||||
backupHistoryRepo: repositories.BackupHistory,
|
||||
restoreRepo: repositories.Restore,
|
||||
restoreHistoryRepo: repositories.RestoreHistory,
|
||||
binlogRepo: repositories.Binlog,
|
||||
binlogHistoryRepo: repositories.BinlogHistory,
|
||||
}
|
||||
func newDbScheduler() *dbScheduler {
|
||||
scheduler := &dbScheduler{}
|
||||
scheduler.runner = runner.NewRunner[entity.DbJob](maxRunning, scheduler.runJob,
|
||||
runner.WithScheduleJob[entity.DbJob](scheduler.scheduleJob),
|
||||
runner.WithRunnableJob[entity.DbJob](scheduler.runnableJob),
|
||||
runner.WithUpdateJob[entity.DbJob](scheduler.updateJob),
|
||||
)
|
||||
return scheduler, nil
|
||||
return scheduler
|
||||
}
|
||||
|
||||
func (s *dbScheduler) scheduleJob(job entity.DbJob) (time.Time, error) {
|
||||
return job.Schedule()
|
||||
}
|
||||
|
||||
func (s *dbScheduler) repo(typ entity.DbJobType) repository.DbJob {
|
||||
switch typ {
|
||||
case entity.DbJobTypeBackup:
|
||||
return s.backupRepo
|
||||
case entity.DbJobTypeRestore:
|
||||
return s.restoreRepo
|
||||
case entity.DbJobTypeBinlog:
|
||||
return s.binlogRepo
|
||||
default:
|
||||
panic(errors.New(fmt.Sprintf("无效的数据库任务类型: %v", typ)))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *dbScheduler) UpdateJob(ctx context.Context, job entity.DbJob) error {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
if err := s.repo(job.GetJobType()).UpdateById(ctx, job); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = s.runner.UpdateOrAdd(ctx, job)
|
||||
_ = s.runner.Update(ctx, job)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -80,28 +56,20 @@ func (s *dbScheduler) Close() {
|
||||
s.runner.Close()
|
||||
}
|
||||
|
||||
func (s *dbScheduler) AddJob(ctx context.Context, saving bool, jobType entity.DbJobType, jobs any) error {
|
||||
func (s *dbScheduler) AddJob(ctx context.Context, jobs any) error {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
if saving {
|
||||
if err := s.repo(jobType).AddJob(ctx, jobs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
reflectValue := reflect.ValueOf(jobs)
|
||||
switch reflectValue.Kind() {
|
||||
case reflect.Array, reflect.Slice:
|
||||
reflectLen := reflectValue.Len()
|
||||
for i := 0; i < reflectLen; i++ {
|
||||
job := reflectValue.Index(i).Interface().(entity.DbJob)
|
||||
job.SetJobType(jobType)
|
||||
_ = s.runner.Add(ctx, job)
|
||||
}
|
||||
default:
|
||||
job := jobs.(entity.DbJob)
|
||||
job.SetJobType(jobType)
|
||||
_ = s.runner.Add(ctx, job)
|
||||
}
|
||||
return nil
|
||||
@@ -112,29 +80,16 @@ func (s *dbScheduler) RemoveJob(ctx context.Context, jobType entity.DbJobType, j
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
if err := s.repo(jobType).DeleteById(ctx, jobId); err != nil {
|
||||
if err := s.runner.Remove(ctx, entity.FormatJobKey(jobType, jobId)); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = s.runner.Remove(ctx, entity.FormatJobKey(jobType, jobId))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *dbScheduler) EnableJob(ctx context.Context, jobType entity.DbJobType, jobId uint64) error {
|
||||
func (s *dbScheduler) EnableJob(ctx context.Context, job entity.DbJob) error {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
repo := s.repo(jobType)
|
||||
job := entity.NewDbJob(jobType)
|
||||
if err := repo.GetById(job, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
if job.IsEnabled() {
|
||||
return nil
|
||||
}
|
||||
job.SetEnabled(true)
|
||||
if err := repo.UpdateEnabled(ctx, jobId, true); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = s.runner.Add(ctx, job)
|
||||
return nil
|
||||
}
|
||||
@@ -143,37 +98,19 @@ func (s *dbScheduler) DisableJob(ctx context.Context, jobType entity.DbJobType,
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
repo := s.repo(jobType)
|
||||
job := entity.NewDbJob(jobType)
|
||||
if err := repo.GetById(job, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
if !job.IsEnabled() {
|
||||
return nil
|
||||
}
|
||||
if err := repo.UpdateEnabled(ctx, jobId, false); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = s.runner.Remove(ctx, job.GetKey())
|
||||
_ = s.runner.Remove(ctx, entity.FormatJobKey(jobType, jobId))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *dbScheduler) StartJobNow(ctx context.Context, jobType entity.DbJobType, jobId uint64) error {
|
||||
func (s *dbScheduler) StartJobNow(ctx context.Context, job entity.DbJob) error {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
job := entity.NewDbJob(jobType)
|
||||
if err := s.repo(jobType).GetById(job, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
if !job.IsEnabled() {
|
||||
return errors.New("任务未启用")
|
||||
}
|
||||
_ = s.runner.StartNow(ctx, job)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *dbScheduler) backupMysql(ctx context.Context, job entity.DbJob) error {
|
||||
func (s *dbScheduler) backup(ctx context.Context, dbProgram dbi.DbProgram, job entity.DbJob) error {
|
||||
id, err := NewIncUUID()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -185,19 +122,14 @@ func (s *dbScheduler) backupMysql(ctx context.Context, job entity.DbJob) error {
|
||||
DbInstanceId: backup.DbInstanceId,
|
||||
DbName: backup.DbName,
|
||||
}
|
||||
conn, err := s.dbApp.GetDbConnByInstanceId(backup.DbInstanceId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dbProgram := conn.GetDialect().GetDbProgram()
|
||||
binlogInfo, err := dbProgram.Backup(ctx, history)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
name := backup.Name
|
||||
if len(name) == 0 {
|
||||
name = backup.DbName
|
||||
name := backup.DbName
|
||||
if len(backup.Name) > 0 {
|
||||
name = fmt.Sprintf("%s-%s", backup.DbName, backup.Name)
|
||||
}
|
||||
history.Name = fmt.Sprintf("%s[%s]", name, now.Format(time.DateTime))
|
||||
history.CreateTime = now
|
||||
@@ -211,43 +143,59 @@ func (s *dbScheduler) backupMysql(ctx context.Context, job entity.DbJob) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *dbScheduler) restoreMysql(ctx context.Context, job entity.DbJob) error {
|
||||
func (s *dbScheduler) restore(ctx context.Context, dbProgram dbi.DbProgram, job entity.DbJob) error {
|
||||
restore := job.(*entity.DbRestore)
|
||||
conn, err := s.dbApp.GetDbConnByInstanceId(restore.DbInstanceId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dbProgram := conn.GetDialect().GetDbProgram()
|
||||
if restore.PointInTime.Valid {
|
||||
latestBinlogSequence, earliestBackupSequence := int64(-1), int64(-1)
|
||||
binlogHistory, ok, err := s.binlogHistoryRepo.GetLatestHistory(restore.DbInstanceId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ok {
|
||||
latestBinlogSequence = binlogHistory.Sequence
|
||||
} else {
|
||||
backupHistory, ok, err := s.backupHistoryRepo.GetEarliestHistory(restore.DbInstanceId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
earliestBackupSequence = backupHistory.BinlogSequence
|
||||
}
|
||||
binlogFiles, err := dbProgram.FetchBinlogs(ctx, true, earliestBackupSequence, latestBinlogSequence)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.binlogHistoryRepo.InsertWithBinlogFiles(ctx, restore.DbInstanceId, binlogFiles); err != nil {
|
||||
//if enabled, err := dbProgram.CheckBinlogEnabled(ctx); err != nil {
|
||||
// return err
|
||||
//} else if !enabled {
|
||||
// return errors.New("数据库未启用 BINLOG")
|
||||
//}
|
||||
//if enabled, err := dbProgram.CheckBinlogRowFormat(ctx); err != nil {
|
||||
// return err
|
||||
//} else if !enabled {
|
||||
// return errors.New("数据库未启用 BINLOG 行模式")
|
||||
//}
|
||||
//
|
||||
//latestBinlogSequence, earliestBackupSequence := int64(-1), int64(-1)
|
||||
//binlogHistory, ok, err := s.binlogHistoryRepo.GetLatestHistory(restore.DbInstanceId)
|
||||
//if err != nil {
|
||||
// return err
|
||||
//}
|
||||
//if ok {
|
||||
// latestBinlogSequence = binlogHistory.Sequence
|
||||
//} else {
|
||||
// backupHistory, ok, err := s.backupHistoryRepo.GetEarliestHistory(restore.DbInstanceId)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if !ok {
|
||||
// return nil
|
||||
// }
|
||||
// earliestBackupSequence = backupHistory.BinlogSequence
|
||||
//}
|
||||
//binlogFiles, err := dbProgram.FetchBinlogs(ctx, true, earliestBackupSequence, latestBinlogSequence)
|
||||
//if err != nil {
|
||||
// return err
|
||||
//}
|
||||
//if err := s.binlogHistoryRepo.InsertWithBinlogFiles(ctx, restore.DbInstanceId, binlogFiles); err != nil {
|
||||
// return err
|
||||
//}
|
||||
if err := s.fetchBinlog(ctx, dbProgram, job.GetInstanceId(), true); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.restorePointInTime(ctx, dbProgram, restore); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := s.restoreBackupHistory(ctx, dbProgram, restore); err != nil {
|
||||
backupHistory := &entity.DbBackupHistory{}
|
||||
if err := s.backupHistoryRepo.GetById(backupHistory, restore.DbBackupHistoryId); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
err = errors.New("备份历史已删除")
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err := s.restoreBackupHistory(ctx, dbProgram, backupHistory); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -262,76 +210,108 @@ func (s *dbScheduler) restoreMysql(ctx context.Context, job entity.DbJob) error
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *dbScheduler) runJob(ctx context.Context, job entity.DbJob) {
|
||||
job.SetLastStatus(entity.DbJobRunning, nil)
|
||||
if err := s.repo(job.GetJobType()).UpdateLastStatus(ctx, job); err != nil {
|
||||
logx.Errorf("failed to update job status: %v", err)
|
||||
return
|
||||
}
|
||||
//func (s *dbScheduler) updateLastStatus(ctx context.Context, job entity.DbJob) error {
|
||||
// switch typ := job.GetJobType(); typ {
|
||||
// case entity.DbJobTypeBackup:
|
||||
// return s.backupRepo.UpdateLastStatus(ctx, job)
|
||||
// case entity.DbJobTypeRestore:
|
||||
// return s.restoreRepo.UpdateLastStatus(ctx, job)
|
||||
// case entity.DbJobTypeBinlog:
|
||||
// return s.binlogRepo.UpdateLastStatus(ctx, job)
|
||||
// default:
|
||||
// panic(fmt.Errorf("无效的数据库任务类型: %v", typ))
|
||||
// }
|
||||
//}
|
||||
|
||||
var errRun error
|
||||
func (s *dbScheduler) updateJob(ctx context.Context, job entity.DbJob) error {
|
||||
switch typ := job.GetJobType(); typ {
|
||||
case entity.DbJobTypeBackup:
|
||||
errRun = s.backupMysql(ctx, job)
|
||||
return s.backupRepo.UpdateById(ctx, job)
|
||||
case entity.DbJobTypeRestore:
|
||||
errRun = s.restoreMysql(ctx, job)
|
||||
return s.restoreRepo.UpdateById(ctx, job)
|
||||
case entity.DbJobTypeBinlog:
|
||||
errRun = s.fetchBinlogMysql(ctx, job)
|
||||
return s.binlogRepo.UpdateById(ctx, job)
|
||||
default:
|
||||
errRun = errors.New(fmt.Sprintf("无效的数据库任务类型: %v", typ))
|
||||
}
|
||||
status := entity.DbJobSuccess
|
||||
if errRun != nil {
|
||||
status = entity.DbJobFailed
|
||||
}
|
||||
job.SetLastStatus(status, errRun)
|
||||
if err := s.repo(job.GetJobType()).UpdateLastStatus(ctx, job); err != nil {
|
||||
logx.Errorf("failed to update job status: %v", err)
|
||||
return
|
||||
return fmt.Errorf("无效的数据库任务类型: %v", typ)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *dbScheduler) runnableJob(job entity.DbJob, next runner.NextJobFunc[entity.DbJob]) bool {
|
||||
func (s *dbScheduler) runJob(ctx context.Context, job entity.DbJob) error {
|
||||
//job.SetLastStatus(entity.DbJobRunning, nil)
|
||||
//if err := s.updateLastStatus(ctx, job); err != nil {
|
||||
// logx.Errorf("failed to update job status: %v", err)
|
||||
// return
|
||||
//}
|
||||
|
||||
//var errRun error
|
||||
conn, err := s.dbApp.GetDbConnByInstanceId(job.GetInstanceId())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dbProgram := conn.GetDialect().GetDbProgram()
|
||||
switch typ := job.GetJobType(); typ {
|
||||
case entity.DbJobTypeBackup:
|
||||
return s.backup(ctx, dbProgram, job)
|
||||
case entity.DbJobTypeRestore:
|
||||
return s.restore(ctx, dbProgram, job)
|
||||
case entity.DbJobTypeBinlog:
|
||||
return s.fetchBinlog(ctx, dbProgram, job.GetInstanceId(), false)
|
||||
default:
|
||||
return fmt.Errorf("无效的数据库任务类型: %v", typ)
|
||||
}
|
||||
//status := entity.DbJobSuccess
|
||||
//if errRun != nil {
|
||||
// status = entity.DbJobFailed
|
||||
//}
|
||||
//job.SetLastStatus(status, errRun)
|
||||
//if err := s.updateLastStatus(ctx, job); err != nil {
|
||||
// logx.Errorf("failed to update job status: %v", err)
|
||||
// return
|
||||
//}
|
||||
}
|
||||
|
||||
func (s *dbScheduler) runnableJob(job entity.DbJob, next runner.NextJobFunc[entity.DbJob]) (bool, error) {
|
||||
if job.IsExpired() {
|
||||
return false, runner.ErrJobExpired
|
||||
}
|
||||
const maxCountByInstanceId = 4
|
||||
const maxCountByDbName = 1
|
||||
var countByInstanceId, countByDbName int
|
||||
jobBase := job.GetJobBase()
|
||||
for item, ok := next(); ok; item, ok = next() {
|
||||
itemBase := item.GetJobBase()
|
||||
if jobBase.DbInstanceId == itemBase.DbInstanceId {
|
||||
if job.GetInstanceId() == item.GetInstanceId() {
|
||||
countByInstanceId++
|
||||
if countByInstanceId >= maxCountByInstanceId {
|
||||
return false
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if relatedToBinlog(job.GetJobType()) {
|
||||
// todo: 恢复数据库前触发 BINLOG 同步,BINLOG 同步完成后才能恢复数据库
|
||||
if relatedToBinlog(item.GetJobType()) {
|
||||
return false
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
if job.GetDbName() == item.GetDbName() {
|
||||
countByDbName++
|
||||
if countByDbName >= maxCountByDbName {
|
||||
return false
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func relatedToBinlog(typ entity.DbJobType) bool {
|
||||
return typ == entity.DbJobTypeRestore || typ == entity.DbJobTypeBinlog
|
||||
}
|
||||
|
||||
func (s *dbScheduler) restorePointInTime(ctx context.Context, program dbi.DbProgram, job *entity.DbRestore) error {
|
||||
func (s *dbScheduler) restorePointInTime(ctx context.Context, dbProgram dbi.DbProgram, job *entity.DbRestore) error {
|
||||
binlogHistory, err := s.binlogHistoryRepo.GetHistoryByTime(job.DbInstanceId, job.PointInTime.Time)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
position, err := program.GetBinlogEventPositionAtOrAfterTime(ctx, binlogHistory.FileName, job.PointInTime.Time)
|
||||
position, err := dbProgram.GetBinlogEventPositionAtOrAfterTime(ctx, binlogHistory.FileName, job.PointInTime.Time)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -360,22 +340,63 @@ func (s *dbScheduler) restorePointInTime(ctx context.Context, program dbi.DbProg
|
||||
TargetPosition: target.Position,
|
||||
TargetTime: job.PointInTime.Time,
|
||||
}
|
||||
if err := program.RestoreBackupHistory(ctx, backupHistory.DbName, backupHistory.DbBackupId, backupHistory.Uuid); err != nil {
|
||||
if err := dbProgram.ReplayBinlog(ctx, job.DbName, job.DbName, restoreInfo); err != nil {
|
||||
return err
|
||||
}
|
||||
return program.ReplayBinlog(ctx, job.DbName, job.DbName, restoreInfo)
|
||||
if err := s.restoreBackupHistory(ctx, dbProgram, backupHistory); err != nil {
|
||||
return err
|
||||
}
|
||||
// 由于 ReplayBinlog 未记录 BINLOG 事件,系统自动备份,避免数据丢失
|
||||
backup := &entity.DbBackup{
|
||||
DbInstanceId: backupHistory.DbInstanceId,
|
||||
DbName: backupHistory.DbName,
|
||||
Enabled: true,
|
||||
Repeated: false,
|
||||
StartTime: time.Now(),
|
||||
Interval: 0,
|
||||
Name: "系统备份",
|
||||
}
|
||||
backup.Id = backupHistory.DbBackupId
|
||||
if err := s.backup(ctx, dbProgram, backup); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *dbScheduler) restoreBackupHistory(ctx context.Context, program dbi.DbProgram, job *entity.DbRestore) error {
|
||||
backupHistory := &entity.DbBackupHistory{}
|
||||
if err := s.backupHistoryRepo.GetById(backupHistory, job.DbBackupHistoryId); err != nil {
|
||||
func (s *dbScheduler) restoreBackupHistory(ctx context.Context, program dbi.DbProgram, backupHistory *entity.DbBackupHistory) (retErr error) {
|
||||
ok, err := s.backupHistoryRepo.UpdateRestoring(true, backupHistory.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_, err = s.backupHistoryRepo.UpdateRestoring(false, backupHistory.Id)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if retErr == nil {
|
||||
retErr = err
|
||||
return
|
||||
}
|
||||
retErr = fmt.Errorf("%w, %w", retErr, err)
|
||||
}()
|
||||
if !ok {
|
||||
return errors.New("关联的数据库备份历史已删除")
|
||||
}
|
||||
return program.RestoreBackupHistory(ctx, backupHistory.DbName, backupHistory.DbBackupId, backupHistory.Uuid)
|
||||
}
|
||||
|
||||
func (s *dbScheduler) fetchBinlogMysql(ctx context.Context, backup entity.DbJob) error {
|
||||
instanceId := backup.GetJobBase().DbInstanceId
|
||||
func (s *dbScheduler) fetchBinlog(ctx context.Context, dbProgram dbi.DbProgram, instanceId uint64, downloadLatestBinlogFile bool) error {
|
||||
if enabled, err := dbProgram.CheckBinlogEnabled(ctx); err != nil {
|
||||
return err
|
||||
} else if !enabled {
|
||||
return errors.New("数据库未启用 BINLOG")
|
||||
}
|
||||
if enabled, err := dbProgram.CheckBinlogRowFormat(ctx); err != nil {
|
||||
return err
|
||||
} else if !enabled {
|
||||
return errors.New("数据库未启用 BINLOG 行模式")
|
||||
}
|
||||
|
||||
latestBinlogSequence, earliestBackupSequence := int64(-1), int64(-1)
|
||||
binlogHistory, ok, err := s.binlogHistoryRepo.GetLatestHistory(instanceId)
|
||||
if err != nil {
|
||||
@@ -393,14 +414,9 @@ func (s *dbScheduler) fetchBinlogMysql(ctx context.Context, backup entity.DbJob)
|
||||
}
|
||||
earliestBackupSequence = backupHistory.BinlogSequence
|
||||
}
|
||||
conn, err := s.dbApp.GetDbConnByInstanceId(instanceId)
|
||||
binlogFiles, err := dbProgram.FetchBinlogs(ctx, downloadLatestBinlogFile, earliestBackupSequence, latestBinlogSequence)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dbProgram := conn.GetDialect().GetDbProgram()
|
||||
binlogFiles, err := dbProgram.FetchBinlogs(ctx, false, earliestBackupSequence, latestBinlogSequence)
|
||||
if err == nil {
|
||||
err = s.binlogHistoryRepo.InsertWithBinlogFiles(ctx, instanceId, binlogFiles)
|
||||
}
|
||||
return nil
|
||||
return s.binlogHistoryRepo.InsertWithBinlogFiles(ctx, instanceId, binlogFiles)
|
||||
}
|
||||
|
||||
@@ -14,8 +14,7 @@ type dbSqlAppImpl struct {
|
||||
base.AppImpl[*entity.DbSql, repository.DbSql]
|
||||
}
|
||||
|
||||
func newDbSqlApp(dbSqlRepo repository.DbSql) DbSql {
|
||||
app := new(dbSqlAppImpl)
|
||||
app.Repo = dbSqlRepo
|
||||
return app
|
||||
// 注入DbSqlRepo
|
||||
func (d *dbSqlAppImpl) InjectDbSqlRepo(repo repository.DbSql) {
|
||||
d.Repo = repo
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"mayfly-go/internal/db/domain/repository"
|
||||
"mayfly-go/pkg/contextx"
|
||||
"mayfly-go/pkg/errorx"
|
||||
"mayfly-go/pkg/logx"
|
||||
"mayfly-go/pkg/model"
|
||||
"mayfly-go/pkg/utils/jsonx"
|
||||
"strconv"
|
||||
@@ -56,14 +57,8 @@ type DbSqlExec interface {
|
||||
GetPageList(condition *entity.DbSqlExecQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
||||
}
|
||||
|
||||
func newDbSqlExecApp(dbExecSqlRepo repository.DbSqlExec) DbSqlExec {
|
||||
return &dbSqlExecAppImpl{
|
||||
dbSqlExecRepo: dbExecSqlRepo,
|
||||
}
|
||||
}
|
||||
|
||||
type dbSqlExecAppImpl struct {
|
||||
dbSqlExecRepo repository.DbSqlExec
|
||||
dbSqlExecRepo repository.DbSqlExec `inject:"DbSqlExecRepo"`
|
||||
}
|
||||
|
||||
func createSqlExecRecord(ctx context.Context, execSqlReq *DbSqlExecReq) *entity.DbSqlExec {
|
||||
@@ -93,8 +88,18 @@ func (d *dbSqlExecAppImpl) Exec(ctx context.Context, execSqlReq *DbSqlExecReq) (
|
||||
// 如果配置为0,则不校验分页参数
|
||||
maxCount := config.GetDbQueryMaxCount()
|
||||
if maxCount != 0 {
|
||||
if !strings.Contains(lowerSql, "limit") {
|
||||
return nil, errorx.NewBiz("请完善分页信息后执行")
|
||||
|
||||
if !strings.Contains(lowerSql, "limit") &&
|
||||
// 兼容oracle rownum分页
|
||||
!strings.Contains(lowerSql, "rownum") &&
|
||||
// 兼容mssql offset分页
|
||||
!strings.Contains(lowerSql, "offset") &&
|
||||
// 兼容mssql top 分页 with result as ({query sql}) select top 100 * from result
|
||||
!strings.Contains(lowerSql, " top ") {
|
||||
// 判断是不是count语句
|
||||
if !strings.Contains(lowerSql, "count(") {
|
||||
return nil, errorx.NewBiz("请完善分页信息后执行")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,7 +170,9 @@ func doSelect(ctx context.Context, selectStmt *sqlparser.Select, execSqlReq *DbS
|
||||
len(strings.Split(selectExprsStr, ",")) > 1 {
|
||||
// 如果配置为0,则不校验分页参数
|
||||
maxCount := config.GetDbQueryMaxCount()
|
||||
if maxCount != 0 {
|
||||
// 哪些数据库跳过校验
|
||||
skipped := dbi.DbTypeOracle == execSqlReq.DbConn.Info.Type || dbi.DbTypeMssql == execSqlReq.DbConn.Info.Type
|
||||
if maxCount != 0 && !skipped {
|
||||
limit := selectStmt.Limit
|
||||
if limit == nil {
|
||||
return nil, errorx.NewBiz("请完善分页信息后执行")
|
||||
@@ -204,6 +211,9 @@ func doUpdate(ctx context.Context, update *sqlparser.Update, execSqlReq *DbSqlEx
|
||||
tableStr := sqlparser.String(update.TableExprs)
|
||||
// 可能使用别名,故空格切割
|
||||
tableName := strings.Split(tableStr, " ")[0]
|
||||
if strings.Contains(tableName, ".") {
|
||||
tableName = strings.Split(tableName, ".")[1]
|
||||
}
|
||||
where := sqlparser.String(update.Where)
|
||||
if len(where) == 0 {
|
||||
return nil, errorx.NewBiz("SQL[%s]未执行. 请完善 where 条件后再执行", execSqlReq.Sql)
|
||||
@@ -223,14 +233,26 @@ func doUpdate(ctx context.Context, update *sqlparser.Update, execSqlReq *DbSqlEx
|
||||
|
||||
updateColumnsAndPrimaryKey := strings.Join(updateColumns, ",") + "," + primaryKey
|
||||
// 查询要更新字段数据的旧值,以及主键值
|
||||
selectSql := fmt.Sprintf("SELECT %s FROM %s %s LIMIT 200", updateColumnsAndPrimaryKey, tableStr, where)
|
||||
_, res, err := dbConn.QueryContext(ctx, selectSql)
|
||||
if err == nil {
|
||||
dbSqlExec.OldValue = jsonx.ToStr(res)
|
||||
} else {
|
||||
dbSqlExec.OldValue = err.Error()
|
||||
selectSql := fmt.Sprintf("SELECT %s FROM %s %s", updateColumnsAndPrimaryKey, tableStr, where)
|
||||
|
||||
// WalkQuery查出最多200条数据
|
||||
maxRec := 200
|
||||
nowRec := 0
|
||||
res := make([]map[string]any, 0)
|
||||
err = dbConn.WalkQueryRows(ctx, selectSql, func(row map[string]any, columns []*dbi.QueryColumn) error {
|
||||
nowRec++
|
||||
res = append(res, row)
|
||||
if nowRec == maxRec {
|
||||
return errorx.NewBiz(fmt.Sprintf("超出更新最大查询条数限制: %d", maxRec))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logx.Warn(err.Error())
|
||||
}
|
||||
|
||||
dbSqlExec.OldValue = jsonx.ToStr(res)
|
||||
dbSqlExec.Table = tableName
|
||||
dbSqlExec.Type = entity.DbSqlExecTypeUpdate
|
||||
|
||||
|
||||
@@ -30,16 +30,15 @@ type Instance interface {
|
||||
GetDatabases(entity *entity.DbInstance) ([]string, error)
|
||||
}
|
||||
|
||||
func newInstanceApp(instanceRepo repository.Instance) Instance {
|
||||
app := new(instanceAppImpl)
|
||||
app.Repo = instanceRepo
|
||||
return app
|
||||
}
|
||||
|
||||
type instanceAppImpl struct {
|
||||
base.AppImpl[*entity.DbInstance, repository.Instance]
|
||||
}
|
||||
|
||||
// 注入DbInstanceRepo
|
||||
func (app *instanceAppImpl) InjectDbInstanceRepo(repo repository.Instance) {
|
||||
app.Repo = repo
|
||||
}
|
||||
|
||||
// GetPageList 分页获取数据库实例
|
||||
func (app *instanceAppImpl) GetPageList(condition *entity.InstanceQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
return app.GetRepo().GetInstanceList(condition, pageParam, toEntity, orderBy...)
|
||||
@@ -73,9 +72,11 @@ func (app *instanceAppImpl) Save(ctx context.Context, instanceEntity *entity.DbI
|
||||
|
||||
err := app.GetBy(oldInstance)
|
||||
if instanceEntity.Id == 0 {
|
||||
if instanceEntity.Password == "" {
|
||||
|
||||
if instanceEntity.Type != string(dbi.DbTypeSqlite) && instanceEntity.Password == "" {
|
||||
return errorx.NewBiz("密码不能为空")
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
return errorx.NewBiz("该数据库实例已存在")
|
||||
}
|
||||
|
||||
@@ -77,6 +77,11 @@ func (d *DbConn) WalkQueryRows(ctx context.Context, querySql string, walkFn Walk
|
||||
return walkQueryRows(ctx, d.db, querySql, walkFn, args...)
|
||||
}
|
||||
|
||||
// 游标方式遍历指定表的结果集, walkFn返回error不为nil, 则跳出遍历
|
||||
func (d *DbConn) WalkTableRows(ctx context.Context, tableName string, walkFn WalkQueryRowsFunc) error {
|
||||
return d.WalkQueryRows(ctx, fmt.Sprintf("SELECT * FROM %s", tableName), walkFn)
|
||||
}
|
||||
|
||||
// 执行 update, insert, delete,建表等sql
|
||||
// 返回影响条数和错误
|
||||
func (d *DbConn) Exec(sql string, args ...any) (int64, error) {
|
||||
@@ -122,6 +127,11 @@ func (d *DbConn) GetDialect() Dialect {
|
||||
return d.Info.Meta.GetDialect(d)
|
||||
}
|
||||
|
||||
// 返回数据库连接状态
|
||||
func (d *DbConn) Stats(ctx context.Context, execSql string, args ...any) sql.DBStats {
|
||||
return d.db.Stats()
|
||||
}
|
||||
|
||||
// 关闭连接
|
||||
func (d *DbConn) Close() {
|
||||
if d.db != nil {
|
||||
@@ -163,7 +173,12 @@ func walkQueryRows(ctx context.Context, db *sql.DB, selectSql string, walkFn Wal
|
||||
// 这里表示一行所有列的值,用[]byte表示
|
||||
values := make([][]byte, lenCols)
|
||||
for k, colType := range colTypes {
|
||||
cols[k] = &QueryColumn{Name: colType.Name(), Type: colType.DatabaseTypeName()}
|
||||
// 处理字段名,如果为空,则命名为匿名列
|
||||
colName := colType.Name()
|
||||
if colName == "" {
|
||||
colName = fmt.Sprintf("<anonymous%d>", k+1)
|
||||
}
|
||||
cols[k] = &QueryColumn{Name: colName, Type: colType.DatabaseTypeName()}
|
||||
// 这里scans引用values,把数据填充到[]byte里
|
||||
scans[k] = &values[k]
|
||||
}
|
||||
@@ -177,7 +192,7 @@ func walkQueryRows(ctx context.Context, db *sql.DB, selectSql string, walkFn Wal
|
||||
rowData := make(map[string]any, lenCols)
|
||||
// 把values中的数据复制到row中
|
||||
for i, v := range values {
|
||||
rowData[colTypes[i].Name()] = valueConvert(v, colTypes[i])
|
||||
rowData[cols[i].Name] = valueConvert(v, colTypes[i])
|
||||
}
|
||||
if err = walkFn(rowData, cols); err != nil {
|
||||
logx.Error("游标遍历查询结果集出错,退出遍历: %s", err.Error())
|
||||
|
||||
@@ -8,6 +8,9 @@ import (
|
||||
)
|
||||
|
||||
type DbProgram interface {
|
||||
CheckBinlogEnabled(ctx context.Context) (bool, error)
|
||||
CheckBinlogRowFormat(ctx context.Context) (bool, error)
|
||||
|
||||
Backup(ctx context.Context, backupHistory *entity.DbBackupHistory) (*entity.BinlogInfo, error)
|
||||
|
||||
FetchBinlogs(ctx context.Context, downloadLatestBinlogFile bool, earliestBackupSequence, latestBinlogSequence int64) ([]*entity.BinlogFile, error)
|
||||
@@ -16,6 +19,8 @@ type DbProgram interface {
|
||||
|
||||
RestoreBackupHistory(ctx context.Context, dbName string, dbBackupId uint64, dbBackupHistoryUuid string) error
|
||||
|
||||
RemoveBackupHistory(ctx context.Context, dbBackupId uint64, dbBackupHistoryUuid string) error
|
||||
|
||||
GetBinlogEventPositionAtOrAfterTime(ctx context.Context, binlogName string, targetTime time.Time) (position int64, parseErr error)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,8 +14,11 @@ const (
|
||||
DbTypeMysql DbType = "mysql"
|
||||
DbTypeMariadb DbType = "mariadb"
|
||||
DbTypePostgres DbType = "postgres"
|
||||
DbTypeGauss DbType = "gauss"
|
||||
DbTypeDM DbType = "dm"
|
||||
DbTypeOracle DbType = "oracle"
|
||||
DbTypeSqlite DbType = "sqlite"
|
||||
DbTypeMssql DbType = "mssql"
|
||||
)
|
||||
|
||||
func ToDbType(dbType string) DbType {
|
||||
@@ -41,20 +44,33 @@ func (dbType DbType) QuoteIdentifier(name string) string {
|
||||
switch dbType {
|
||||
case DbTypeMysql, DbTypeMariadb:
|
||||
return quoteIdentifier(name, "`")
|
||||
case DbTypePostgres:
|
||||
case DbTypePostgres, DbTypeGauss:
|
||||
return quoteIdentifier(name, `"`)
|
||||
case DbTypeMssql:
|
||||
return fmt.Sprintf("[%s]", name)
|
||||
default:
|
||||
return quoteIdentifier(name, `"`)
|
||||
}
|
||||
}
|
||||
|
||||
func (dbType DbType) RemoveQuote(name string) string {
|
||||
switch dbType {
|
||||
case DbTypeMysql, DbTypeMariadb:
|
||||
return removeQuote(name, "`")
|
||||
case DbTypePostgres, DbTypeGauss:
|
||||
return removeQuote(name, `"`)
|
||||
default:
|
||||
return removeQuote(name, `"`)
|
||||
}
|
||||
}
|
||||
|
||||
func (dbType DbType) QuoteLiteral(literal string) string {
|
||||
switch dbType {
|
||||
case DbTypeMysql, DbTypeMariadb:
|
||||
literal = strings.ReplaceAll(literal, `\`, `\\`)
|
||||
literal = strings.ReplaceAll(literal, `'`, `''`)
|
||||
return "'" + literal + "'"
|
||||
case DbTypePostgres:
|
||||
case DbTypePostgres, DbTypeGauss:
|
||||
return pq.QuoteLiteral(literal)
|
||||
default:
|
||||
return pq.QuoteLiteral(literal)
|
||||
@@ -65,7 +81,7 @@ func (dbType DbType) MetaDbName() string {
|
||||
switch dbType {
|
||||
case DbTypeMysql, DbTypeMariadb:
|
||||
return ""
|
||||
case DbTypePostgres:
|
||||
case DbTypePostgres, DbTypeGauss:
|
||||
return "postgres"
|
||||
case DbTypeDM:
|
||||
return ""
|
||||
@@ -78,7 +94,7 @@ func (dbType DbType) Dialect() sqlparser.Dialect {
|
||||
switch dbType {
|
||||
case DbTypeMysql, DbTypeMariadb:
|
||||
return sqlparser.MysqlDialect{}
|
||||
case DbTypePostgres:
|
||||
case DbTypePostgres, DbTypeGauss:
|
||||
return sqlparser.PostgresDialect{}
|
||||
default:
|
||||
return sqlparser.PostgresDialect{}
|
||||
@@ -93,6 +109,11 @@ func quoteIdentifier(name, quoter string) string {
|
||||
return quoter + strings.Replace(name, quoter, quoter+quoter, -1) + quoter
|
||||
}
|
||||
|
||||
// 移除相关引号
|
||||
func removeQuote(name, quoter string) string {
|
||||
return strings.ReplaceAll(name, quoter, "")
|
||||
}
|
||||
|
||||
func (dbType DbType) StmtSetForeignKeyChecks(check bool) string {
|
||||
switch dbType {
|
||||
case DbTypeMysql, DbTypeMariadb:
|
||||
@@ -101,7 +122,7 @@ func (dbType DbType) StmtSetForeignKeyChecks(check bool) string {
|
||||
} else {
|
||||
return "SET FOREIGN_KEY_CHECKS = 0;\n"
|
||||
}
|
||||
case DbTypePostgres:
|
||||
case DbTypePostgres, DbTypeGauss:
|
||||
// not currently supported postgres
|
||||
return ""
|
||||
default:
|
||||
@@ -113,7 +134,7 @@ func (dbType DbType) StmtUseDatabase(dbName string) string {
|
||||
switch dbType {
|
||||
case DbTypeMysql, DbTypeMariadb:
|
||||
return fmt.Sprintf("USE %s;\n", dbType.QuoteIdentifier(dbName))
|
||||
case DbTypePostgres:
|
||||
case DbTypePostgres, DbTypeGauss:
|
||||
// not currently supported postgres
|
||||
return ""
|
||||
default:
|
||||
|
||||
@@ -42,7 +42,8 @@ type Column struct {
|
||||
ColumnName string `json:"columnName"` // 列名
|
||||
ColumnType string `json:"columnType"` // 列类型
|
||||
ColumnComment string `json:"columnComment"` // 列备注
|
||||
ColumnKey string `json:"columnKey"` // 是否为主键,逐渐的话值钱为PRI
|
||||
IsPrimaryKey bool `json:"isPrimaryKey"` // 是否为主键
|
||||
IsIdentity bool `json:"isIdentity"` // 是否自增
|
||||
ColumnDefault string `json:"columnDefault"` // 默认值
|
||||
Nullable string `json:"nullable"` // 是否可为null
|
||||
NumScale string `json:"numScale"` // 小数点
|
||||
@@ -56,12 +57,33 @@ type Index struct {
|
||||
IndexType string `json:"indexType"` // 索引类型
|
||||
IndexComment string `json:"indexComment"` // 备注
|
||||
SeqInIndex int `json:"seqInIndex"`
|
||||
NonUnique int `json:"nonUnique"`
|
||||
IsUnique bool `json:"isUnique"`
|
||||
}
|
||||
|
||||
type DbCopyTable struct {
|
||||
Id uint64 `json:"id"`
|
||||
Db string `json:"db" `
|
||||
TableName string `json:"tableName"`
|
||||
CopyData bool `json:"copyData"` // 是否复制数据
|
||||
}
|
||||
|
||||
// 数据转换器
|
||||
type DataConverter interface {
|
||||
// 获取数据对应的类型
|
||||
// @param dbColumnType 数据库原始列类型,如varchar等
|
||||
GetDataType(dbColumnType string) DataType
|
||||
|
||||
// 根据数据类型格式化指定数据
|
||||
FormatData(dbColumnValue any, dataType DataType) string
|
||||
|
||||
// 根据数据类型解析数据为符合要求的指定类型等
|
||||
ParseData(dbColumnValue any, dataType DataType) any
|
||||
}
|
||||
|
||||
// -----------------------------------元数据接口定义------------------------------------------
|
||||
// 数据库方言、元信息接口(表、列、获取表数据等元信息)
|
||||
type Dialect interface {
|
||||
|
||||
// 获取数据库服务实例信息
|
||||
GetDbServer() (*DbServer, error)
|
||||
|
||||
@@ -75,7 +97,7 @@ type Dialect interface {
|
||||
GetColumns(tableNames ...string) ([]Column, error)
|
||||
|
||||
// 获取表主键字段名,没有主键标识则默认第一个字段
|
||||
GetPrimaryKey(tablename string) (string, error)
|
||||
GetPrimaryKey(tableName string) (string, error)
|
||||
|
||||
// 获取表索引信息
|
||||
GetTableIndex(tableName string) ([]Index, error)
|
||||
@@ -83,9 +105,6 @@ type Dialect interface {
|
||||
// 获取建表ddl
|
||||
GetTableDDL(tableName string) (string, error)
|
||||
|
||||
// WalkTableRecord 遍历指定表的数据
|
||||
WalkTableRecord(tableName string, walkFn WalkQueryRowsFunc) error
|
||||
|
||||
GetSchemas() ([]string, error)
|
||||
|
||||
// GetDbProgram 获取数据库程序模块,用于数据库备份与恢复
|
||||
@@ -94,9 +113,10 @@ type Dialect interface {
|
||||
// 批量保存数据
|
||||
BatchInsert(tx *sql.Tx, tableName string, columns []string, values [][]any) (int64, error)
|
||||
|
||||
GetDataType(dbColumnType string) DataType
|
||||
// 获取数据转换器用于解析格式化列数据等
|
||||
GetDataConverter() DataConverter
|
||||
|
||||
FormatStrData(dbColumnValue string, dataType DataType) string
|
||||
CopyTable(copy *DbCopyTable) error
|
||||
}
|
||||
|
||||
// ------------------------- 元数据sql操作 -------------------------
|
||||
|
||||
@@ -2,6 +2,20 @@ package dbi
|
||||
|
||||
import "database/sql"
|
||||
|
||||
var (
|
||||
metas map[DbType]Meta = make(map[DbType]Meta)
|
||||
)
|
||||
|
||||
// 注册数据库类型与dbmeta
|
||||
func Register(dt DbType, meta Meta) {
|
||||
metas[dt] = meta
|
||||
}
|
||||
|
||||
// 根据数据库类型获取对应的Meta
|
||||
func GetMeta(dt DbType) Meta {
|
||||
return metas[dt]
|
||||
}
|
||||
|
||||
// 数据库元信息获取,如获取sql.DB、Dialect等
|
||||
type Meta interface {
|
||||
// 根据数据库信息获取sql.DB
|
||||
|
||||
@@ -36,7 +36,7 @@ ORDER BY a.object_name
|
||||
select
|
||||
a.index_name as INDEX_NAME,
|
||||
a.index_type as INDEX_TYPE,
|
||||
case when a.uniqueness = 'UNIQUE' then 1 else 0 end as NON_UNIQUE,
|
||||
case when a.uniqueness = 'UNIQUE' then 1 else 0 end as IS_UNIQUE,
|
||||
indexdef(b.object_id,1) as INDEX_DEF,
|
||||
c.column_name as COLUMN_NAME,
|
||||
c.column_position as SEQ_IN_INDEX,
|
||||
@@ -64,22 +64,26 @@ select a.table_name
|
||||
b.comments as COLUMN_COMMENT,
|
||||
a.data_default as COLUMN_DEFAULT,
|
||||
a.data_scale as NUM_SCALE,
|
||||
case when t.COL_NAME = a.column_name then 'PRI' else '' end as COLUMN_KEY
|
||||
case when t.COL_NAME = a.column_name then 1 else 0 end as IS_IDENTITY,
|
||||
case when t2.constraint_type = 'P' then 1 else 0 end as IS_PRIMARY_KEY
|
||||
from all_tab_columns a
|
||||
left join user_col_comments b
|
||||
on b.owner = (SELECT SF_GET_SCHEMA_NAME_BY_ID(CURRENT_SCHID)) and b.table_name = a.table_name and
|
||||
a.column_name = b.column_name
|
||||
left join (select b.owner, b.table_name, a.name COL_NAME
|
||||
on b.owner = (SELECT SF_GET_SCHEMA_NAME_BY_ID(CURRENT_SCHID))
|
||||
and b.table_name = a.table_name
|
||||
and a.column_name = b.column_name
|
||||
left join (select b.owner, b.TABLE_NAME, a.NAME as COL_NAME
|
||||
from SYS.SYSCOLUMNS a,
|
||||
all_tables b,
|
||||
sys.sysobjects c,
|
||||
sys.sysobjects d
|
||||
SYS.all_tables b,
|
||||
SYS.SYSOBJECTS c
|
||||
where a.INFO2 & 0x01 = 0x01
|
||||
and a.id=c.id and d.type$ = 'SCH' and d.id = c.schid
|
||||
and b.owner = (SELECT SF_GET_SCHEMA_NAME_BY_ID(CURRENT_SCHID))
|
||||
and c.schid = ( select id from sys.sysobjects where type$ = 'SCH' and name = (SELECT SF_GET_SCHEMA_NAME_BY_ID(CURRENT_SCHID)))
|
||||
and c.name = b.table_name) t
|
||||
on t.table_name = a.table_name
|
||||
and a.ID = c.ID
|
||||
and c.NAME = b.TABLE_NAME) t
|
||||
on t.table_name = a.table_name and t.owner = a.owner
|
||||
left join (select uc.OWNER, uic.column_name, uic.table_name, uc.constraint_type
|
||||
from user_ind_columns uic
|
||||
left join user_constraints uc on uic.index_name = uc.index_name) t2
|
||||
on t2.table_name = t.table_name and a.column_name = t2.column_name
|
||||
where a.owner = (SELECT SF_GET_SCHEMA_NAME_BY_ID(CURRENT_SCHID))
|
||||
and a.table_name in (%s)
|
||||
order by a.table_name, a.column_id
|
||||
order by a.table_name,
|
||||
a.column_id
|
||||
209
server/internal/db/dbm/dbi/metasql/mssql_meta.sql
Normal file
209
server/internal/db/dbm/dbi/metasql/mssql_meta.sql
Normal file
@@ -0,0 +1,209 @@
|
||||
--MSSQL_DBS 数据库名信息
|
||||
SELECT name AS dbname
|
||||
FROM sys.databases
|
||||
WHERE owner_sid = SUSER_SID()
|
||||
and name not in ('master', 'tempdb', 'model', 'msdb')
|
||||
---------------------------------------
|
||||
--MSSQL_TABLE_DETAIL 查询表名和表注释
|
||||
SELECT t.name AS tableName,
|
||||
ep.value AS tableComment
|
||||
FROM sys.tables t
|
||||
left OUTER JOIN sys.schemas ss on t.schema_id = ss.schema_id
|
||||
LEFT OUTER JOIN
|
||||
sys.extended_properties ep ON ep.major_id = t.object_id AND ep.minor_id = 0 AND ep.class = 1
|
||||
WHERE ss.name = ?
|
||||
and t.name = ?
|
||||
---------------------------------------
|
||||
--MSSQL_DB_SCHEMAS 数据库下所有schema
|
||||
SELECT a.SCHEMA_NAME
|
||||
FROM information_schema.schemata a
|
||||
where a.catalog_name = DB_NAME()
|
||||
and (a.SCHEMA_NAME in ('dbo', 'guest') or a.SCHEMA_NAME not like 'db_%')
|
||||
and a.SCHEMA_NAME not in ('sys', 'INFORMATION_SCHEMA')
|
||||
---------------------------------------
|
||||
--MSSQL_TABLE_INFO 表详细信息
|
||||
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 = ?
|
||||
ORDER BY t.name DESC;
|
||||
---------------------------------------
|
||||
--MSSQL_INDEX_INFO 索引信息
|
||||
SELECT ind.name AS indexName,
|
||||
col.name AS columnName,
|
||||
CASE
|
||||
WHEN ind.is_primary_key = 1 THEN 'CLUSTERED'
|
||||
ELSE 'NON-CLUSTERED'
|
||||
END AS indexType,
|
||||
IIF(ind.is_unique = 'true', 1, 0) AS isUnique,
|
||||
ic.key_ordinal AS seqInIndex,
|
||||
idx.value AS indexComment
|
||||
FROM sys.indexes ind
|
||||
LEFT JOIN sys.tables t on t.object_id = ind.object_id
|
||||
LEFT JOIN sys.schemas ss on t.schema_id = ss.schema_id
|
||||
LEFT JOIN
|
||||
sys.index_columns ic ON ind.object_id = ic.object_id AND ind.index_id = ic.index_id
|
||||
LEFT JOIN
|
||||
sys.columns col ON ind.object_id = col.object_id AND ic.column_id = col.column_id
|
||||
LEFT JOIN
|
||||
sys.extended_properties idx ON ind.object_id = idx.major_id AND ind.index_id = idx.minor_id AND idx.class = 7
|
||||
WHERE ss.name = ?
|
||||
and ind.name is not null
|
||||
and t.name = ?
|
||||
---------------------------------------
|
||||
--MSSQL_COLUMN_MA 列信息元数据
|
||||
SELECT t.name AS TABLE_NAME,
|
||||
c.name AS COLUMN_NAME,
|
||||
CASE
|
||||
WHEN c.is_nullable = 1 THEN 'YES'
|
||||
ELSE 'NO'
|
||||
END AS NULLABLE,
|
||||
tp.name +
|
||||
CASE
|
||||
WHEN tp.name IN ('char', 'varchar', 'nchar', 'nvarchar') THEN '(' + CASE
|
||||
WHEN c.max_length = -1 THEN 'max'
|
||||
ELSE CAST(c.max_length AS NVARCHAR(255)) END +
|
||||
')'
|
||||
WHEN tp.name IN ('numeric', 'decimal') THEN '(' + CAST(c.precision AS NVARCHAR(255)) + ',' +
|
||||
CAST(c.scale AS NVARCHAR(255)) + ')'
|
||||
ELSE ''
|
||||
END AS COLUMN_TYPE,
|
||||
ep.value AS COLUMN_COMMENT,
|
||||
COLUMN_DEFAULT = CASE
|
||||
WHEN c.default_object_id IS NOT NULL THEN object_definition(c.default_object_id)
|
||||
ELSE ''
|
||||
END,
|
||||
c.scale AS NUM_SCALE,
|
||||
IS_IDENTITY = COLUMNPROPERTY(c.object_id, c.name, 'IsIdentity'),
|
||||
IS_PRIMARY_KEY = CASE
|
||||
WHEN (SELECT COUNT(*)
|
||||
FROM sys.index_columns ic
|
||||
INNER JOIN sys.indexes i
|
||||
ON ic.index_id = i.index_id AND ic.object_id = i.object_id
|
||||
WHERE ic.object_id = c.object_id
|
||||
AND ic.column_id = c.column_id
|
||||
AND i.is_primary_key = 1) > 0 THEN 1
|
||||
ELSE 0
|
||||
END
|
||||
FROM sys.tables t
|
||||
INNER JOIN sys.schemas ss on t.schema_id = ss.schema_id
|
||||
INNER JOIN
|
||||
sys.columns c ON t.object_id = c.object_id
|
||||
INNER JOIN
|
||||
sys.types tp ON c.system_type_id = tp.system_type_id AND c.user_type_id = tp.user_type_id
|
||||
LEFT JOIN
|
||||
sys.extended_properties ep ON t.object_id = ep.major_id AND c.column_id = ep.minor_id AND ep.class = 1
|
||||
WHERE ss.name = ?
|
||||
and t.name in (%s)
|
||||
ORDER BY t.name, c.column_id
|
||||
---------------------------------------
|
||||
--MSSQL_TABLE_DDL 建表ddl
|
||||
declare
|
||||
@tabname varchar(50)
|
||||
set @tabname= ? --表名
|
||||
if ( object_id('tempdb.dbo.#t') is not null)
|
||||
begin
|
||||
DROP TABLE #t
|
||||
end
|
||||
select 'create table [' + so.name + '] (' + o.list + ')'
|
||||
+ CASE
|
||||
WHEN tc.Constraint_Name IS NULL THEN ''
|
||||
ELSE 'ALTER TABLE ' + so.Name + ' ADD CONSTRAINT ' + tc.Constraint_Name + ' PRIMARY KEY ' +
|
||||
' (' + LEFT(j.List, Len(j.List)-1) + ')' END
|
||||
TABLE_DDL
|
||||
into #t
|
||||
from sysobjects so
|
||||
cross apply
|
||||
(SELECT
|
||||
' \n ['+ column_name +'] ' +
|
||||
data_type + case data_type
|
||||
when 'sql_variant' then ''
|
||||
when 'text' then ''
|
||||
when 'ntext' then ''
|
||||
when 'xml' then ''
|
||||
when 'decimal' then '(' + cast (numeric_precision as varchar) + ', ' + cast (numeric_scale as varchar) + ')'
|
||||
else coalesce ('('+ case when character_maximum_length = -1 then 'MAX' else cast (character_maximum_length as varchar) end +')', '') end + ' ' +
|
||||
case when exists (
|
||||
select id from syscolumns
|
||||
where object_name(id)=so.name
|
||||
and name = column_name
|
||||
and columnproperty(id, name, 'IsIdentity') = 1
|
||||
) then
|
||||
'IDENTITY(' +
|
||||
cast (ident_seed(so.name) as varchar) + ',' +
|
||||
cast (ident_incr(so.name) as varchar) + ')'
|
||||
else ''
|
||||
end + ' ' +
|
||||
(case when IS_NULLABLE = 'No' then 'NOT ' else '' end ) + 'NULL ' +
|
||||
case when information_schema.columns.COLUMN_DEFAULT IS NOT NULL THEN 'DEFAULT '+ information_schema.columns.COLUMN_DEFAULT ELSE '' END + ', '
|
||||
from information_schema.columns where table_name = so.name
|
||||
order by ordinal_position
|
||||
FOR XML PATH ('')) o (list)
|
||||
left join
|
||||
information_schema.table_constraints tc
|
||||
on tc.Table_name = so.Name
|
||||
AND tc.Constraint_Type = 'PRIMARY KEY'
|
||||
cross apply
|
||||
(select '[' + Column_Name + '], '
|
||||
FROM information_schema.key_column_usage kcu
|
||||
WHERE kcu.Constraint_Name = tc.Constraint_Name
|
||||
ORDER BY
|
||||
ORDINAL_POSITION
|
||||
FOR XML PATH ('')) j (list)
|
||||
where xtype = 'U'
|
||||
AND name =@tabname
|
||||
|
||||
select (
|
||||
case
|
||||
when (select count(a.constraint_type)
|
||||
from information_schema.table_constraints a
|
||||
inner join information_schema.constraint_column_usage b
|
||||
on a.constraint_name = b.constraint_name
|
||||
where a.constraint_type = 'PRIMARY KEY'--主键
|
||||
and a.table_name = @tabname) = 1 then replace(table_ddl
|
||||
, ', )ALTER TABLE'
|
||||
, ')' + CHAR (13)+'ALTER TABLE')
|
||||
else SUBSTRING(table_ddl
|
||||
, 1
|
||||
, len(table_ddl) - 3) + ')' end
|
||||
) as TableDDL
|
||||
from #t
|
||||
|
||||
drop table #t
|
||||
---------------------------------------
|
||||
--MSSQL_TABLE_INDEX_DDL 建索引ddl
|
||||
DECLARE
|
||||
@TableName NVARCHAR(255)
|
||||
SET @TableName = ?;
|
||||
|
||||
SELECT 'CREATE ' +
|
||||
CASE
|
||||
WHEN i.is_primary_key = 1 THEN 'CLUSTERED '
|
||||
WHEN i.type_desc = 'HEAP' THEN ''
|
||||
ELSE 'NONCLUSTERED '
|
||||
END +
|
||||
'INDEX ' + i.name + ' ON ' + t.name + ' (' +
|
||||
STUFF((SELECT ',' + c.name +
|
||||
CASE
|
||||
WHEN ic.is_descending_key = 1 THEN ' DESC'
|
||||
ELSE ' ASC'
|
||||
END
|
||||
FROM sys.index_columns ic
|
||||
INNER JOIN
|
||||
sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
|
||||
WHERE ic.object_id = i.object_id
|
||||
AND ic.index_id = i.index_id
|
||||
ORDER BY ic.key_ordinal
|
||||
FOR XML PATH (''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '') + ');' AS IndexDDL
|
||||
FROM sys.tables t
|
||||
INNER JOIN
|
||||
sys.indexes i ON t.object_id = i.object_id
|
||||
WHERE t.name = @TableName;
|
||||
@@ -30,7 +30,7 @@ SELECT
|
||||
index_name indexName,
|
||||
column_name columnName,
|
||||
index_type indexType,
|
||||
non_unique nonUnique,
|
||||
IF(non_unique, 0, 1) isUnique,
|
||||
SEQ_IN_INDEX seqInIndex,
|
||||
INDEX_COMMENT indexComment
|
||||
FROM
|
||||
@@ -46,24 +46,25 @@ ORDER BY
|
||||
SEQ_IN_INDEX asc
|
||||
---------------------------------------
|
||||
--MYSQL_COLUMN_MA 列信息元数据
|
||||
SELECT
|
||||
table_name tableName,
|
||||
column_name columnName,
|
||||
column_type columnType,
|
||||
column_default columnDefault,
|
||||
column_comment columnComment,
|
||||
column_key columnKey,
|
||||
extra extra,
|
||||
is_nullable nullable,
|
||||
NUMERIC_SCALE numScale
|
||||
from
|
||||
information_schema.columns
|
||||
WHERE
|
||||
table_schema = (
|
||||
SELECT
|
||||
database ()
|
||||
)
|
||||
AND table_name in (%s)
|
||||
ORDER BY
|
||||
tableName,
|
||||
ordinal_position
|
||||
SELECT table_name tableName,
|
||||
column_name columnName,
|
||||
column_type columnType,
|
||||
column_default columnDefault,
|
||||
column_comment columnComment,
|
||||
CASE
|
||||
WHEN column_key = 'PRI' THEN
|
||||
1
|
||||
ELSE 0
|
||||
END AS isPrimaryKey,
|
||||
CASE
|
||||
WHEN extra LIKE '%%auto_increment%%' THEN
|
||||
1
|
||||
ELSE 0
|
||||
END AS isIdentity,
|
||||
is_nullable nullable,
|
||||
NUMERIC_SCALE numScale
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE table_schema = (SELECT DATABASE())
|
||||
AND table_name IN (%s)
|
||||
ORDER BY table_name,
|
||||
ordinal_position
|
||||
@@ -21,9 +21,9 @@ ORDER BY a.TABLE_NAME
|
||||
SELECT ai.INDEX_NAME AS INDEX_NAME,
|
||||
ai.INDEX_TYPE AS INDEX_TYPE,
|
||||
CASE
|
||||
WHEN ai.uniqueness = 'UNIQUE' THEN 'NO'
|
||||
ELSE 'YES'
|
||||
END AS NON_UNIQUE,
|
||||
WHEN ai.uniqueness = 'UNIQUE' THEN 1
|
||||
ELSE 0
|
||||
END AS IS_UNIQUE,
|
||||
(SELECT LISTAGG(column_name, ', ') WITHIN GROUP (ORDER BY column_position)
|
||||
FROM ALL_IND_COLUMNS aic
|
||||
WHERE aic.INDEX_NAME = ai.INDEX_NAME
|
||||
@@ -53,9 +53,8 @@ SELECT a.TABLE_NAME as TABLE_NAME,
|
||||
b.COMMENTS as COLUMN_COMMENT,
|
||||
a.DATA_DEFAULT as COLUMN_DEFAULT,
|
||||
a.DATA_SCALE as NUM_SCALE,
|
||||
CASE
|
||||
WHEN d.pri IS NOT NULL THEN 'PRI'
|
||||
END as COLUMN_KEY
|
||||
CASE WHEN d.pri IS NOT NULL THEN 1 ELSE 0 END as IS_PRIMARY_KEY,
|
||||
CASE WHEN a.IDENTITY_COLUMN = 'YES' THEN 1 ELSE 0 END as IS_IDENTITY
|
||||
FROM all_tab_columns a
|
||||
LEFT JOIN all_col_comments b
|
||||
on a.OWNER = b.OWNER AND a.TABLE_NAME = b.TABLE_NAME AND a.COLUMN_NAME = b.COLUMN_NAME
|
||||
|
||||
@@ -35,7 +35,7 @@ order by c.relname
|
||||
SELECT
|
||||
indexname AS "indexName",
|
||||
'BTREE' AS "IndexType",
|
||||
case when indexdef like 'CREATE UNIQUE INDEX%%' then 0 else 1 end as "nonUnique",
|
||||
case when indexdef like 'CREATE UNIQUE INDEX%%' then 1 else 0 end as "isUnique",
|
||||
obj_description(b.oid, 'pg_class') AS "indexComment",
|
||||
indexdef AS "indexDef",
|
||||
c.attname AS "columnName",
|
||||
@@ -47,18 +47,21 @@ WHERE a.schemaname = (select current_schema())
|
||||
AND a.tablename = '%s';
|
||||
---------------------------------------
|
||||
--PGSQL_COLUMN_MA 表列信息
|
||||
SELECT
|
||||
table_name AS "tableName",
|
||||
column_name AS "columnName",
|
||||
is_nullable AS "nullable",
|
||||
SELECT a.table_name AS "tableName",
|
||||
a.column_name AS "columnName",
|
||||
a.is_nullable AS "nullable",
|
||||
case when character_maximum_length > 0 then concat(udt_name, '(',character_maximum_length,')') else udt_name end AS "columnType",
|
||||
column_default as "columnDefault",
|
||||
numeric_scale AS "numScale",
|
||||
case when column_default like 'nextval%%' then 'PRI' else '' end "columnKey",
|
||||
col_description((table_schema || '.' || table_name)::regclass, ordinal_position) AS "columnComment"
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = (select current_schema()) and table_name in (%s)
|
||||
order by table_name, ordinal_position
|
||||
a.column_default as "columnDefault",
|
||||
a.numeric_scale AS "numScale",
|
||||
case when a.column_default like 'nextval%%' then 1 else 0 end "isIdentity",
|
||||
case when b.column_name is not null then 1 else 0 end "isPrimaryKey",
|
||||
col_description((a.table_schema || '.' || a.table_name)::regclass, a.ordinal_position) AS "columnComment"
|
||||
FROM information_schema.columns a
|
||||
left join information_schema.key_column_usage b
|
||||
on a.table_schema = b.table_schema and b.table_name = a.table_name and b.column_name = a.column_name
|
||||
WHERE a.table_schema = (select current_schema())
|
||||
and a.table_name in (%s)
|
||||
order by a.table_name, a.ordinal_position
|
||||
---------------------------------------
|
||||
--PGSQL_TABLE_DDL_FUNC 表ddl函数
|
||||
CREATE OR REPLACE FUNCTION showcreatetable(namespace character varying, tablename character varying)
|
||||
|
||||
21
server/internal/db/dbm/dbi/metasql/sqlite_meta.sql
Normal file
21
server/internal/db/dbm/dbi/metasql/sqlite_meta.sql
Normal file
@@ -0,0 +1,21 @@
|
||||
--SQLITE_TABLE_INFO 表详细信息
|
||||
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_%'
|
||||
ORDER BY tbl_name
|
||||
---------------------------------------
|
||||
--SQLITE_INDEX_INFO 表索引信息
|
||||
select name as indexName,
|
||||
`sql` as indexSql,
|
||||
'normal' as indexType,
|
||||
'' as indexComment
|
||||
FROM sqlite_master
|
||||
WHERE type = 'index'
|
||||
and tbl_name = '%s'
|
||||
ORDER BY name
|
||||
@@ -4,10 +4,12 @@ import (
|
||||
"fmt"
|
||||
"mayfly-go/internal/common/consts"
|
||||
"mayfly-go/internal/db/dbm/dbi"
|
||||
"mayfly-go/internal/db/dbm/dm"
|
||||
"mayfly-go/internal/db/dbm/mysql"
|
||||
"mayfly-go/internal/db/dbm/oracle"
|
||||
"mayfly-go/internal/db/dbm/postgres"
|
||||
_ "mayfly-go/internal/db/dbm/dm"
|
||||
_ "mayfly-go/internal/db/dbm/mssql"
|
||||
_ "mayfly-go/internal/db/dbm/mysql"
|
||||
_ "mayfly-go/internal/db/dbm/oracle"
|
||||
_ "mayfly-go/internal/db/dbm/postgres"
|
||||
_ "mayfly-go/internal/db/dbm/sqlite"
|
||||
"mayfly-go/internal/machine/mcm"
|
||||
"mayfly-go/pkg/cache"
|
||||
"mayfly-go/pkg/logx"
|
||||
@@ -38,21 +40,6 @@ func init() {
|
||||
|
||||
var mutex sync.Mutex
|
||||
|
||||
func getDbMetaByType(dt dbi.DbType) dbi.Meta {
|
||||
switch dt {
|
||||
case dbi.DbTypeMysql, dbi.DbTypeMariadb:
|
||||
return mysql.GetMeta()
|
||||
case dbi.DbTypePostgres:
|
||||
return postgres.GetMeta()
|
||||
case dbi.DbTypeDM:
|
||||
return dm.GetMeta()
|
||||
case dbi.DbTypeOracle:
|
||||
return oracle.GetMeta()
|
||||
default:
|
||||
panic(fmt.Sprintf("invalid database type: %s", dt))
|
||||
}
|
||||
}
|
||||
|
||||
// 从缓存中获取数据库连接信息,若缓存中不存在则会使用回调函数获取dbInfo进行连接并缓存
|
||||
func GetDbConn(dbId uint64, database string, getDbInfo func() (*dbi.DbInfo, error)) (*dbi.DbConn, error) {
|
||||
connId := dbi.GetDbConnId(dbId, database)
|
||||
@@ -89,7 +76,7 @@ func GetDbConn(dbId uint64, database string, getDbInfo func() (*dbi.DbInfo, erro
|
||||
|
||||
// 使用指定dbInfo信息进行连接
|
||||
func Conn(di *dbi.DbInfo) (*dbi.DbConn, error) {
|
||||
return di.Conn(getDbMetaByType(di.Type))
|
||||
return di.Conn(dbi.GetMeta(di.Type))
|
||||
}
|
||||
|
||||
// 根据实例id获取连接
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
package dm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"mayfly-go/internal/db/dbm/dbi"
|
||||
"mayfly-go/pkg/errorx"
|
||||
"mayfly-go/pkg/logx"
|
||||
"mayfly-go/pkg/utils/anyx"
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
"mayfly-go/pkg/utils/stringx"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kanzihuang/vitess/go/vt/sqlparser"
|
||||
|
||||
_ "gitee.com/chunanyong/dm"
|
||||
)
|
||||
|
||||
@@ -67,7 +70,7 @@ func (dd *DMDialect) GetTables() ([]dbi.Table, error) {
|
||||
tables := make([]dbi.Table, 0)
|
||||
for _, re := range res {
|
||||
tables = append(tables, dbi.Table{
|
||||
TableName: re["TABLE_NAME"].(string),
|
||||
TableName: anyx.ConvString(re["TABLE_NAME"]),
|
||||
TableComment: anyx.ConvString(re["TABLE_COMMENT"]),
|
||||
CreateTime: anyx.ConvString(re["CREATE_TIME"]),
|
||||
TableRows: anyx.ConvInt(re["TABLE_ROWS"]),
|
||||
@@ -80,13 +83,10 @@ func (dd *DMDialect) GetTables() ([]dbi.Table, error) {
|
||||
|
||||
// 获取列元信息, 如列名等
|
||||
func (dd *DMDialect) GetColumns(tableNames ...string) ([]dbi.Column, error) {
|
||||
tableName := ""
|
||||
for i := 0; i < len(tableNames); i++ {
|
||||
if i != 0 {
|
||||
tableName = tableName + ", "
|
||||
}
|
||||
tableName = tableName + "'" + tableNames[i] + "'"
|
||||
}
|
||||
dbType := dd.dc.Info.Type
|
||||
tableName := strings.Join(collx.ArrayMap[string, string](tableNames, func(val string) string {
|
||||
return fmt.Sprintf("'%s'", dbType.RemoveQuote(val))
|
||||
}), ",")
|
||||
|
||||
_, res, err := dd.dc.Query(fmt.Sprintf(dbi.GetLocalSql(DM_META_FILE, DM_COLUMN_MA_KEY), tableName))
|
||||
if err != nil {
|
||||
@@ -96,12 +96,13 @@ func (dd *DMDialect) GetColumns(tableNames ...string) ([]dbi.Column, error) {
|
||||
columns := make([]dbi.Column, 0)
|
||||
for _, re := range res {
|
||||
columns = append(columns, dbi.Column{
|
||||
TableName: re["TABLE_NAME"].(string),
|
||||
ColumnName: re["COLUMN_NAME"].(string),
|
||||
TableName: anyx.ConvString(re["TABLE_NAME"]),
|
||||
ColumnName: anyx.ConvString(re["COLUMN_NAME"]),
|
||||
ColumnType: anyx.ConvString(re["COLUMN_TYPE"]),
|
||||
ColumnComment: anyx.ConvString(re["COLUMN_COMMENT"]),
|
||||
Nullable: anyx.ConvString(re["NULLABLE"]),
|
||||
ColumnKey: anyx.ConvString(re["COLUMN_KEY"]),
|
||||
IsPrimaryKey: anyx.ConvInt(re["IS_PRIMARY_KEY"]) == 1,
|
||||
IsIdentity: anyx.ConvInt(re["IS_IDENTITY"]) == 1,
|
||||
ColumnDefault: anyx.ConvString(re["COLUMN_DEFAULT"]),
|
||||
NumScale: anyx.ConvString(re["NUM_SCALE"]),
|
||||
})
|
||||
@@ -118,7 +119,7 @@ func (dd *DMDialect) GetPrimaryKey(tablename string) (string, error) {
|
||||
return "", errorx.NewBiz("[%s] 表不存在", tablename)
|
||||
}
|
||||
for _, v := range columns {
|
||||
if v.ColumnKey == "PRI" {
|
||||
if v.IsPrimaryKey {
|
||||
return v.ColumnName, nil
|
||||
}
|
||||
}
|
||||
@@ -136,11 +137,11 @@ func (dd *DMDialect) GetTableIndex(tableName string) ([]dbi.Index, error) {
|
||||
indexs := make([]dbi.Index, 0)
|
||||
for _, re := range res {
|
||||
indexs = append(indexs, dbi.Index{
|
||||
IndexName: re["INDEX_NAME"].(string),
|
||||
IndexName: anyx.ConvString(re["INDEX_NAME"]),
|
||||
ColumnName: anyx.ConvString(re["COLUMN_NAME"]),
|
||||
IndexType: anyx.ConvString(re["INDEX_TYPE"]),
|
||||
IndexComment: anyx.ConvString(re["INDEX_COMMENT"]),
|
||||
NonUnique: anyx.ConvInt(re["NON_UNIQUE"]),
|
||||
IsUnique: anyx.ConvInt(re["IS_UNIQUE"]) == 1,
|
||||
SeqInIndex: anyx.ConvInt(re["SEQ_IN_INDEX"]),
|
||||
})
|
||||
}
|
||||
@@ -232,10 +233,6 @@ func (dd *DMDialect) GetTableDDL(tableName string) (string, error) {
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
func (dd *DMDialect) WalkTableRecord(tableName string, walkFn dbi.WalkQueryRowsFunc) error {
|
||||
return dd.dc.WalkQueryRows(context.Background(), fmt.Sprintf("SELECT * FROM %s", tableName), walkFn)
|
||||
}
|
||||
|
||||
// 获取DM当前连接的库可访问的schemaNames
|
||||
func (dd *DMDialect) GetSchemas() ([]string, error) {
|
||||
sql := dbi.GetLocalSql(DM_META_FILE, DM_DB_SCHEMAS)
|
||||
@@ -255,24 +252,16 @@ func (dd *DMDialect) GetDbProgram() dbi.DbProgram {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (dd *DMDialect) GetDataType(dbColumnType string) dbi.DataType {
|
||||
if regexp.MustCompile(`(?i)int|double|float|number|decimal|byte|bit`).MatchString(dbColumnType) {
|
||||
return dbi.DataTypeNumber
|
||||
}
|
||||
var (
|
||||
// 数字类型
|
||||
numberRegexp = regexp.MustCompile(`(?i)int|double|float|number|decimal|byte|bit`)
|
||||
// 日期时间类型
|
||||
if regexp.MustCompile(`(?i)datetime|timestamp`).MatchString(dbColumnType) {
|
||||
return dbi.DataTypeDateTime
|
||||
}
|
||||
datetimeRegexp = regexp.MustCompile(`(?i)datetime|timestamp`)
|
||||
// 日期类型
|
||||
if regexp.MustCompile(`(?i)date`).MatchString(dbColumnType) {
|
||||
return dbi.DataTypeDate
|
||||
}
|
||||
dateRegexp = regexp.MustCompile(`(?i)date`)
|
||||
// 时间类型
|
||||
if regexp.MustCompile(`(?i)time`).MatchString(dbColumnType) {
|
||||
return dbi.DataTypeTime
|
||||
}
|
||||
return dbi.DataTypeString
|
||||
}
|
||||
timeRegexp = regexp.MustCompile(`(?i)time`)
|
||||
)
|
||||
|
||||
func (dd *DMDialect) BatchInsert(tx *sql.Tx, tableName string, columns []string, values [][]any) (int64, error) {
|
||||
// 执行批量insert sql
|
||||
@@ -299,17 +288,86 @@ func (dd *DMDialect) BatchInsert(tx *sql.Tx, tableName string, columns []string,
|
||||
return int64(effRows), nil
|
||||
}
|
||||
|
||||
func (dd *DMDialect) FormatStrData(dbColumnValue string, dataType dbi.DataType) string {
|
||||
func (dd *DMDialect) GetDataConverter() dbi.DataConverter {
|
||||
return new(DataConverter)
|
||||
}
|
||||
|
||||
type DataConverter struct {
|
||||
}
|
||||
|
||||
func (dd *DataConverter) GetDataType(dbColumnType string) dbi.DataType {
|
||||
if numberRegexp.MatchString(dbColumnType) {
|
||||
return dbi.DataTypeNumber
|
||||
}
|
||||
if datetimeRegexp.MatchString(dbColumnType) {
|
||||
return dbi.DataTypeDateTime
|
||||
}
|
||||
if dateRegexp.MatchString(dbColumnType) {
|
||||
return dbi.DataTypeDate
|
||||
}
|
||||
if timeRegexp.MatchString(dbColumnType) {
|
||||
return dbi.DataTypeTime
|
||||
}
|
||||
return dbi.DataTypeString
|
||||
}
|
||||
|
||||
func (dd *DataConverter) FormatData(dbColumnValue any, dataType dbi.DataType) string {
|
||||
str := anyx.ToString(dbColumnValue)
|
||||
switch dataType {
|
||||
case dbi.DataTypeDateTime: // "2024-01-02T22:08:22.275697+08:00"
|
||||
res, _ := time.Parse(time.RFC3339, dbColumnValue)
|
||||
res, _ := time.Parse(time.RFC3339, str)
|
||||
return res.Format(time.DateTime)
|
||||
case dbi.DataTypeDate: // "2024-01-02T00:00:00+08:00"
|
||||
res, _ := time.Parse(time.RFC3339, dbColumnValue)
|
||||
res, _ := time.Parse(time.RFC3339, str)
|
||||
return res.Format(time.DateOnly)
|
||||
case dbi.DataTypeTime: // "0000-01-01T22:08:22.275688+08:00"
|
||||
res, _ := time.Parse(time.RFC3339, dbColumnValue)
|
||||
res, _ := time.Parse(time.RFC3339, str)
|
||||
return res.Format(time.TimeOnly)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
func (dd *DataConverter) ParseData(dbColumnValue any, dataType dbi.DataType) any {
|
||||
return dbColumnValue
|
||||
}
|
||||
|
||||
func (dd *DMDialect) CopyTable(copy *dbi.DbCopyTable) error {
|
||||
tableName := copy.TableName
|
||||
ddl, err := dd.GetTableDDL(tableName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 生成新表名,为老表明+_copy_时间戳
|
||||
newTableName := tableName + "_copy_" + time.Now().Format("20060102150405")
|
||||
|
||||
// 替换新表名
|
||||
ddl = strings.ReplaceAll(ddl, fmt.Sprintf("\"%s\"", strings.ToUpper(tableName)), fmt.Sprintf("\"%s\"", strings.ToUpper(newTableName)))
|
||||
// 去除空格换行
|
||||
ddl = stringx.TrimSpaceAndBr(ddl)
|
||||
sqls, err := sqlparser.SplitStatementToPieces(ddl, sqlparser.WithDialect(dd.dc.Info.Type.Dialect()))
|
||||
for _, sql := range sqls {
|
||||
_, _ = dd.dc.Exec(sql)
|
||||
}
|
||||
|
||||
// 复制数据
|
||||
if copy.CopyData {
|
||||
go func() {
|
||||
// 设置允许填充自增列之后,显示指定列名可以插入自增列
|
||||
_, _ = dd.dc.Exec(fmt.Sprintf("set identity_insert \"%s\" on", newTableName))
|
||||
// 获取列名
|
||||
columns, _ := dd.GetColumns(tableName)
|
||||
columnArr := make([]string, 0)
|
||||
for _, column := range columns {
|
||||
columnArr = append(columnArr, fmt.Sprintf("\"%s\"", column.ColumnName))
|
||||
}
|
||||
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("set identity_insert \"%s\" off", newTableName))
|
||||
}()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -4,20 +4,12 @@ import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"mayfly-go/internal/db/dbm/dbi"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
meta dbi.Meta
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
func GetMeta() dbi.Meta {
|
||||
once.Do(func() {
|
||||
meta = new(DmMeta)
|
||||
})
|
||||
return meta
|
||||
func init() {
|
||||
dbi.Register(dbi.DbTypeDM, new(DmMeta))
|
||||
}
|
||||
|
||||
type DmMeta struct {
|
||||
@@ -31,10 +23,12 @@ func (md *DmMeta) GetSqlDb(d *dbi.DbInfo) (*sql.DB, error) {
|
||||
// dm database可以使用db/schema表示,方便连接指定schema, 若不存在schema则使用默认schema
|
||||
ss := strings.Split(db, "/")
|
||||
if len(ss) > 1 {
|
||||
dbParam = fmt.Sprintf("%s?schema=%s", ss[0], ss[len(ss)-1])
|
||||
dbParam = fmt.Sprintf("%s?schema=\"%s\"&escapeProcess=true", ss[0], ss[len(ss)-1])
|
||||
} else {
|
||||
dbParam = db
|
||||
dbParam = db + "?escapeProcess=true"
|
||||
}
|
||||
} else {
|
||||
dbParam = "?escapeProcess=true"
|
||||
}
|
||||
|
||||
err := d.IfUseSshTunnelChangeIpPort()
|
||||
@@ -42,7 +36,7 @@ func (md *DmMeta) GetSqlDb(d *dbi.DbInfo) (*sql.DB, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dsn := fmt.Sprintf("dm://%s:%s@%s:%d/%s", d.Username, d.Password, d.Host, d.Port, dbParam)
|
||||
dsn := fmt.Sprintf("dm://%s:%s@%s:%d/%s", d.Username, url.PathEscape(d.Password), d.Host, d.Port, dbParam)
|
||||
return sql.Open(driverName, dsn)
|
||||
}
|
||||
|
||||
|
||||
425
server/internal/db/dbm/mssql/dialect.go
Normal file
425
server/internal/db/dbm/mssql/dialect.go
Normal file
@@ -0,0 +1,425 @@
|
||||
package mssql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"mayfly-go/internal/db/dbm/dbi"
|
||||
"mayfly-go/pkg/errorx"
|
||||
"mayfly-go/pkg/logx"
|
||||
"mayfly-go/pkg/utils/anyx"
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
type MssqlDialect struct {
|
||||
dc *dbi.DbConn
|
||||
}
|
||||
|
||||
func (md *MssqlDialect) GetDbServer() (*dbi.DbServer, error) {
|
||||
_, res, err := md.dc.Query("SELECT @@VERSION as version")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ds := &dbi.DbServer{
|
||||
Version: anyx.ConvString(res[0]["version"]),
|
||||
}
|
||||
return ds, nil
|
||||
}
|
||||
|
||||
func (md *MssqlDialect) GetDbNames() ([]string, error) {
|
||||
_, res, err := md.dc.Query(dbi.GetLocalSql(MSSQL_META_FILE, MSSQL_DBS_KEY))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
databases := make([]string, 0)
|
||||
for _, re := range res {
|
||||
databases = append(databases, anyx.ConvString(re["dbname"]))
|
||||
}
|
||||
|
||||
return databases, nil
|
||||
}
|
||||
|
||||
// 从连接信息中获取数据库和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
|
||||
}
|
||||
|
||||
// 获取表基础元信息, 如表名等
|
||||
func (md *MssqlDialect) GetTables() ([]dbi.Table, error) {
|
||||
_, res, err := md.dc.Query(dbi.GetLocalSql(MSSQL_META_FILE, MSSQL_TABLE_INFO_KEY), md.currentSchema())
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tables := make([]dbi.Table, 0)
|
||||
for _, re := range res {
|
||||
tables = append(tables, dbi.Table{
|
||||
TableName: anyx.ConvString(re["tableName"]),
|
||||
TableComment: anyx.ConvString(re["tableComment"]),
|
||||
CreateTime: anyx.ConvString(re["createTime"]),
|
||||
TableRows: anyx.ConvInt(re["tableRows"]),
|
||||
DataLength: anyx.ConvInt64(re["dataLength"]),
|
||||
IndexLength: anyx.ConvInt64(re["indexLength"]),
|
||||
})
|
||||
}
|
||||
return tables, nil
|
||||
}
|
||||
|
||||
// 获取列元信息, 如列名等
|
||||
func (md *MssqlDialect) GetColumns(tableNames ...string) ([]dbi.Column, error) {
|
||||
dbType := md.dc.Info.Type
|
||||
tableName := strings.Join(collx.ArrayMap[string, string](tableNames, func(val string) string {
|
||||
return fmt.Sprintf("'%s'", dbType.RemoveQuote(val))
|
||||
}), ",")
|
||||
|
||||
_, res, err := md.dc.Query(fmt.Sprintf(dbi.GetLocalSql(MSSQL_META_FILE, MSSQL_COLUMN_MA_KEY), tableName), md.currentSchema())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
columns := make([]dbi.Column, 0)
|
||||
for _, re := range res {
|
||||
columns = append(columns, dbi.Column{
|
||||
TableName: anyx.ToString(re["TABLE_NAME"]),
|
||||
ColumnName: anyx.ToString(re["COLUMN_NAME"]),
|
||||
ColumnType: anyx.ToString(re["COLUMN_TYPE"]),
|
||||
ColumnComment: anyx.ToString(re["COLUMN_COMMENT"]),
|
||||
Nullable: anyx.ToString(re["NULLABLE"]),
|
||||
IsPrimaryKey: anyx.ConvInt(re["IS_PRIMARY_KEY"]) == 1,
|
||||
IsIdentity: anyx.ConvInt(re["IS_IDENTITY"]) == 1,
|
||||
ColumnDefault: anyx.ToString(re["COLUMN_DEFAULT"]),
|
||||
NumScale: anyx.ToString(re["NUM_SCALE"]),
|
||||
})
|
||||
}
|
||||
return columns, nil
|
||||
}
|
||||
|
||||
// 获取表主键字段名,不存在主键标识则默认第一个字段
|
||||
func (md *MssqlDialect) GetPrimaryKey(tablename string) (string, error) {
|
||||
columns, err := md.GetColumns(tablename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(columns) == 0 {
|
||||
return "", errorx.NewBiz("[%s] 表不存在", tablename)
|
||||
}
|
||||
|
||||
for _, v := range columns {
|
||||
if v.IsPrimaryKey {
|
||||
return v.ColumnName, nil
|
||||
}
|
||||
}
|
||||
|
||||
return columns[0].ColumnName, nil
|
||||
}
|
||||
|
||||
// 获取表索引信息
|
||||
func (md *MssqlDialect) GetTableIndex(tableName string) ([]dbi.Index, error) {
|
||||
_, res, err := md.dc.Query(dbi.GetLocalSql(MSSQL_META_FILE, MSSQL_INDEX_INFO_KEY), md.currentSchema(), tableName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
indexs := make([]dbi.Index, 0)
|
||||
for _, re := range res {
|
||||
indexs = append(indexs, dbi.Index{
|
||||
IndexName: anyx.ConvString(re["indexName"]),
|
||||
ColumnName: anyx.ConvString(re["columnName"]),
|
||||
IndexType: anyx.ConvString(re["indexType"]),
|
||||
IndexComment: anyx.ConvString(re["indexComment"]),
|
||||
IsUnique: anyx.ConvInt(re["isUnique"]) == 1,
|
||||
SeqInIndex: anyx.ConvInt(re["seqInIndex"]),
|
||||
})
|
||||
}
|
||||
// 把查询结果以索引名分组,索引字段以逗号连接
|
||||
result := make([]dbi.Index, 0)
|
||||
key := ""
|
||||
for _, v := range indexs {
|
||||
// 当前的索引名
|
||||
in := v.IndexName
|
||||
// 过滤掉主键索引,主键索引名为PK__开头的
|
||||
if strings.HasPrefix(in, "PK__") {
|
||||
continue
|
||||
}
|
||||
if key == in {
|
||||
// 索引字段已根据名称和顺序排序,故取最后一个即可
|
||||
i := len(result) - 1
|
||||
// 同索引字段以逗号连接
|
||||
result[i].ColumnName = result[i].ColumnName + "," + v.ColumnName
|
||||
} else {
|
||||
key = in
|
||||
result = append(result, v)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (md MssqlDialect) CopyTableDDL(tableName string, newTableName string) (string, error) {
|
||||
if newTableName == "" {
|
||||
newTableName = tableName
|
||||
}
|
||||
|
||||
// 根据列信息生成建表语句
|
||||
var builder strings.Builder
|
||||
var commentBuilder strings.Builder
|
||||
|
||||
// 查询表名和表注释, 设置表注释
|
||||
_, res, err := md.dc.Query(dbi.GetLocalSql(MSSQL_META_FILE, MSSQL_TABLE_DETAIL_KEY), md.currentSchema(), tableName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
tableComment := ""
|
||||
if len(res) > 0 {
|
||||
tableComment = anyx.ToString(res[0]["tableComment"])
|
||||
if tableComment != "" {
|
||||
// 注释转义单引号
|
||||
tableComment = strings.ReplaceAll(tableComment, "'", "\\'")
|
||||
commentBuilder.WriteString(fmt.Sprintf("\nEXEC sp_addextendedproperty N'MS_Description', N'%s', N'SCHEMA', N'%s', N'TABLE',N'%s';\n", tableComment, md.currentSchema(), newTableName))
|
||||
}
|
||||
}
|
||||
|
||||
baseTable := fmt.Sprintf("%s.%s", md.dc.Info.Type.QuoteIdentifier(md.currentSchema()), md.dc.Info.Type.QuoteIdentifier(newTableName))
|
||||
|
||||
// 查询列信息
|
||||
columns, err := md.GetColumns(tableName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
builder.WriteString(fmt.Sprintf("CREATE TABLE %s (\n", baseTable))
|
||||
pks := make([]string, 0)
|
||||
for i, v := range columns {
|
||||
nullAble := "NULL"
|
||||
if v.Nullable == "NO" {
|
||||
nullAble = "NOT NULL"
|
||||
}
|
||||
builder.WriteString(fmt.Sprintf("\t[%s] %s %s", v.ColumnName, v.ColumnType, nullAble))
|
||||
if v.IsIdentity {
|
||||
builder.WriteString(" IDENTITY(1,11)")
|
||||
}
|
||||
if v.ColumnDefault != "" {
|
||||
builder.WriteString(fmt.Sprintf(" DEFAULT %s", v.ColumnDefault))
|
||||
}
|
||||
if v.IsPrimaryKey {
|
||||
pks = append(pks, fmt.Sprintf("[%s]", v.ColumnName))
|
||||
}
|
||||
if i < len(columns)-1 {
|
||||
builder.WriteString(",")
|
||||
}
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
// 设置主键
|
||||
if len(pks) > 0 {
|
||||
builder.WriteString(fmt.Sprintf("\tCONSTRAINT PK_%s PRIMARY KEY ( %s )", newTableName, strings.Join(pks, ",")))
|
||||
}
|
||||
builder.WriteString("\n);\n")
|
||||
|
||||
// 设置字段注释
|
||||
for _, v := range columns {
|
||||
if v.ColumnComment != "" {
|
||||
// 注释转义单引号
|
||||
v.ColumnComment = strings.ReplaceAll(v.ColumnComment, "'", "\\'")
|
||||
commentBuilder.WriteString(fmt.Sprintf("\nEXEC sp_addextendedproperty N'MS_Description', N'%s', N'SCHEMA', N'%s', N'TABLE',N'%s', N'COLUMN', N'%s';\n", v.ColumnComment, md.currentSchema(), newTableName, v.ColumnName))
|
||||
}
|
||||
}
|
||||
|
||||
// 设置索引
|
||||
indexs, err := md.GetTableIndex(tableName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, v := range indexs {
|
||||
builder.WriteString(fmt.Sprintf("\nCREATE NONCLUSTERED INDEX [%s] ON %s (%s);\n", v.IndexName, baseTable, v.ColumnName))
|
||||
// 设置索引注释
|
||||
if v.IndexComment != "" {
|
||||
// 注释转义单引号
|
||||
v.IndexComment = strings.ReplaceAll(v.IndexComment, "'", "\\'")
|
||||
commentBuilder.WriteString(fmt.Sprintf("\nEXEC sp_addextendedproperty N'MS_Description', N'%s', N'SCHEMA', N'%s', N'TABLE',N'%s', N'INDEX', N'%s';\n", v.IndexComment, md.currentSchema(), newTableName, v.IndexName))
|
||||
}
|
||||
}
|
||||
return builder.String() + commentBuilder.String(), nil
|
||||
}
|
||||
|
||||
// 获取建表ddl
|
||||
func (md *MssqlDialect) GetTableDDL(tableName string) (string, error) {
|
||||
return md.CopyTableDDL(tableName, "")
|
||||
}
|
||||
|
||||
func (md *MssqlDialect) WalkTableRecord(tableName string, walkFn dbi.WalkQueryRowsFunc) error {
|
||||
return md.dc.WalkQueryRows(context.Background(), fmt.Sprintf("SELECT * FROM %s", tableName), walkFn)
|
||||
}
|
||||
|
||||
func (md *MssqlDialect) GetSchemas() ([]string, error) {
|
||||
_, res, err := md.dc.Query(dbi.GetLocalSql(MSSQL_META_FILE, MSSQL_DB_SCHEMAS_KEY))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
schemas := make([]string, 0)
|
||||
for _, re := range res {
|
||||
schemas = append(schemas, anyx.ConvString(re["SCHEMA_NAME"]))
|
||||
}
|
||||
return schemas, nil
|
||||
}
|
||||
|
||||
// GetDbProgram 获取数据库程序模块,用于数据库备份与恢复
|
||||
func (md *MssqlDialect) GetDbProgram() dbi.DbProgram {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (md *MssqlDialect) BatchInsert(tx *sql.Tx, tableName string, columns []string, values [][]any) (int64, error) {
|
||||
schema := md.currentSchema()
|
||||
|
||||
// 生成占位符字符串:如:(?,?)
|
||||
// 重复字符串并用逗号连接
|
||||
repeated := strings.Repeat("?,", len(columns))
|
||||
// 去除最后一个逗号,占位符由括号包裹
|
||||
placeholder := fmt.Sprintf("(%s)", strings.TrimSuffix(repeated, ","))
|
||||
|
||||
// 重复占位符字符串n遍
|
||||
repeated = strings.Repeat(placeholder+",", len(values))
|
||||
// 去除最后一个逗号
|
||||
placeholder = strings.TrimSuffix(repeated, ",")
|
||||
|
||||
baseTable := fmt.Sprintf("%s.%s", md.dc.Info.Type.QuoteIdentifier(schema), md.dc.Info.Type.QuoteIdentifier(tableName))
|
||||
|
||||
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...)
|
||||
}
|
||||
// 设置允许填充自增列之后,显示指定列名可以插入自增列
|
||||
_, _ = md.dc.Exec(fmt.Sprintf("set identity_insert \"%s\" on", tableName))
|
||||
|
||||
exec, err := md.dc.TxExec(tx, sqlStr, args...)
|
||||
|
||||
_, _ = md.dc.Exec(fmt.Sprintf("set identity_insert \"%s\" off", tableName))
|
||||
|
||||
return exec, err
|
||||
}
|
||||
|
||||
func (md *MssqlDialect) GetDataConverter() dbi.DataConverter {
|
||||
return new(DataConverter)
|
||||
}
|
||||
|
||||
var (
|
||||
// 数字类型
|
||||
numberRegexp = regexp.MustCompile(`(?i)int|double|float|number|decimal|byte|bit`)
|
||||
// 日期时间类型
|
||||
datetimeRegexp = regexp.MustCompile(`(?i)datetime|timestamp`)
|
||||
// 日期类型
|
||||
dateRegexp = regexp.MustCompile(`(?i)date`)
|
||||
// 时间类型
|
||||
timeRegexp = regexp.MustCompile(`(?i)time`)
|
||||
)
|
||||
|
||||
type DataConverter struct {
|
||||
}
|
||||
|
||||
func (dc *DataConverter) GetDataType(dbColumnType string) dbi.DataType {
|
||||
if numberRegexp.MatchString(dbColumnType) {
|
||||
return dbi.DataTypeNumber
|
||||
}
|
||||
// 日期时间类型
|
||||
if datetimeRegexp.MatchString(dbColumnType) {
|
||||
return dbi.DataTypeDateTime
|
||||
}
|
||||
// 日期类型
|
||||
if dateRegexp.MatchString(dbColumnType) {
|
||||
return dbi.DataTypeDate
|
||||
}
|
||||
// 时间类型
|
||||
if timeRegexp.MatchString(dbColumnType) {
|
||||
return dbi.DataTypeTime
|
||||
}
|
||||
return dbi.DataTypeString
|
||||
}
|
||||
|
||||
func (dc *DataConverter) FormatData(dbColumnValue any, dataType dbi.DataType) string {
|
||||
return anyx.ToString(dbColumnValue)
|
||||
}
|
||||
|
||||
func (dc *DataConverter) ParseData(dbColumnValue any, dataType dbi.DataType) any {
|
||||
return dbColumnValue
|
||||
}
|
||||
|
||||
func (md *MssqlDialect) CopyTable(copy *dbi.DbCopyTable) error {
|
||||
|
||||
schema := md.currentSchema()
|
||||
|
||||
// 生成新表名,为老表明+_copy_时间戳
|
||||
newTableName := copy.TableName + "_copy_" + time.Now().Format("20060102150405")
|
||||
|
||||
// 复制建表语句
|
||||
ddl, err := md.CopyTableDDL(copy.TableName, newTableName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 执行建表
|
||||
_, err = md.dc.Exec(ddl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 复制数据
|
||||
if copy.CopyData {
|
||||
go func() {
|
||||
// 查询所有的列
|
||||
columns, err := md.GetColumns(copy.TableName)
|
||||
if err != nil {
|
||||
logx.Warnf("复制表[%s]数据失败: %s", copy.TableName, err.Error())
|
||||
return
|
||||
}
|
||||
// 取出每列名, 需要显示指定列名插入数据
|
||||
columnNames := make([]string, 0)
|
||||
hasIdentity := false
|
||||
for _, v := range columns {
|
||||
columnNames = append(columnNames, fmt.Sprintf("[%s]", v.ColumnName))
|
||||
if v.IsIdentity {
|
||||
hasIdentity = true
|
||||
}
|
||||
|
||||
}
|
||||
columnsSql := strings.Join(columnNames, ",")
|
||||
|
||||
// 复制数据
|
||||
// 设置允许填充自增列之后,显示指定列名可以插入自增列
|
||||
identityInsertOn := ""
|
||||
identityInsertOff := ""
|
||||
if hasIdentity {
|
||||
identityInsertOn = fmt.Sprintf("SET IDENTITY_INSERT [%s].[%s] ON", schema, newTableName)
|
||||
identityInsertOff = fmt.Sprintf("SET IDENTITY_INSERT [%s].[%s] OFF", schema, newTableName)
|
||||
}
|
||||
_, err = md.dc.Exec(fmt.Sprintf(" %s INSERT INTO [%s].[%s] (%s) SELECT * FROM [%s].[%s] %s", identityInsertOn, schema, newTableName, columnsSql, schema, copy.TableName, identityInsertOff))
|
||||
if err != nil {
|
||||
logx.Warnf("复制表[%s]数据失败: %s", copy.TableName, err.Error())
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user