24 Commits

Author SHA1 Message Date
meilin.huang
252fc553f2 feat: v1.7.2 2024-01-31 12:53:27 +08:00
meilin.huang
ac2ceed3f9 refactor: code review 2024-01-30 21:56:49 +08:00
kanzihuang
3f828cc5b0 !96 删除数据库备份和恢复历史
* feat: 删除数据库备份历史
* refactor dbScheduler
* feat: 从数据库备份历史中恢复数据库
* feat: 删除数据库恢复历史记录
* refactor dbScheuler
2024-01-30 13:12:43 +00:00
zongyangleo
fc1b9ef35d !97 一些优化
* refactor: 重构表格分页组件,适配大数据量分页
* fix:定时任务修复
* feat: gaussdb单独提出来
2024-01-30 13:09:26 +00:00
meilin.huang
d0b71a1c40 refactor: dialect使用方式调整 2024-01-29 16:02:28 +08:00
meilin.huang
a743a6a05a Merge branch 'master' into dev 2024-01-29 12:21:22 +08:00
zongyangleo
0e6b9713ce !93 feat: DBMS支持mssql和一些功能优化
* feat: 表格+表格元数据缓存
* feat:跳板机支持多段跳
* fix: 所有数据库区分字段主键和自增
* feat: DBMS支持mssql
* refactor: 去除无用的getter方法
2024-01-29 04:20:23 +00:00
meilin.huang
b9afbc764d refactor: 去除无用的getter方法 2024-01-29 11:34:48 +08:00
meilin.huang
923e183a67 refactor: code review 2024-01-26 17:17:26 +08:00
meilin.huang
7e9a381641 refactor: 数据库meta使用注册方式,方便可插拔 2024-01-24 17:01:17 +08:00
zongyangleo
bed95254d0 !91 fix: oracle数据同步 bug
* fix: oracle数据同步 bug
2024-01-24 08:29:16 +00:00
meilin.huang
e4d13f3377 refactor: 引入日志切割库、indexApi拆分等 2024-01-23 19:30:28 +08:00
Coder慌
d530365ef9 !90 fix: 依赖注入支持私有变量
Merge pull request !90 from kanzihuang/feat-db-bak
2024-01-23 09:02:37 +00:00
wanli
070d4ea104 fix: 依赖注入支持私有变量 2024-01-23 16:29:41 +08:00
zongyangleo
3fc86f0fae !88 feat: dbms表支持右键菜单:删除表、编辑表、新建表、复制表
* feat: 支持复制表
* feat: dbms表支持右键菜单:删除表、编辑表、新建表
2024-01-23 04:08:02 +00:00
kanzihuang
3b77ab2727 !89 feat: 给数据库备份和恢复配置操作权限
* feat: 给数据库备份和恢复配置操作权限
* refactor: 数据库备份与恢复采用最新依赖注入机制
2024-01-23 04:06:08 +00:00
meilin.huang
76cb991282 fix: 数据同步更新时间展示等问题 2024-01-23 09:27:05 +08:00
meilin.huang
9efd20f1b9 refactor: ioc与系统初始化处理方式调整 2024-01-22 11:35:28 +08:00
kanzihuang
de5b9e46d3 !87 fix: 修复数据库备份与恢复问题
* feat: 修复数据库备份与恢复问题
* feat: 启用 BINLOG 支持全量备份和增量备份,未启用 BINLOG 仅支持全量备份
* feat: 数据库恢复后自动备份,避免数据丢失
2024-01-22 03:12:16 +00:00
meilin.huang
f27d3d200f feat: 新增简易版ioc 2024-01-21 22:52:20 +08:00
meilin.huang
f4a64b96a9 feat: v1.7.1新增支持sqlite&oracle分页限制等问题修复 2024-01-19 21:33:37 +08:00
zongyangleo
9a59749763 !86 dbms支持sqlite和一些bug修复
* fix: 达梦数据库连接修复,以支持带特殊字符的密码和schema
* fix: oracle bug修复
* feat: dbms支持sqlite
* fix: dbms 修改字段名bug
2024-01-19 08:59:35 +00:00
kanzihuang
b017b902f8 !85 fix: 修复 BINLOG同步任务加载问题
* Merge branch 'dev' of gitee.com:dromara/mayfly-go into feat-db-bak
* fix: 修复 BINLOG 同步任务加载问题
2024-01-19 00:40:44 +00:00
meilin.huang
7c53353c60 fix: sqlite数据问题时间类型问题修复等 2024-01-18 17:18:17 +08:00
247 changed files with 7022 additions and 2358 deletions

View File

@@ -5,7 +5,7 @@ WORKDIR /mayfly
COPY mayfly_go_web . 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 install && \
yarn build yarn build
@@ -24,7 +24,7 @@ COPY --from=fe-builder /mayfly/dist /mayfly/static/static
# Build # Build
RUN GO111MODULE=on CGO_ENABLED=0 GOOS=linux \ RUN GO111MODULE=on CGO_ENABLED=0 GOOS=linux \
go build -a \ go build -a -ldflags=-w \
-o mayfly-go main.go -o mayfly-go main.go
FROM debian:bookworm-slim FROM debian:bookworm-slim

View File

@@ -22,7 +22,7 @@
### 介绍 ### 介绍
web 版 **linux(终端[终端回放] 文件 脚本 进程 计划任务)、数据库mysql postgres oracle 达梦 高斯、redis(单机 哨兵 集群)、mongo 统一管理操作平台** web 版 **linux(终端[终端回放] 文件 脚本 进程 计划任务)、数据库mysql postgres oracle 达梦 高斯 sqlite、redis(单机 哨兵 集群)、mongo 统一管理操作平台**
### 开发语言与主要框架 ### 开发语言与主要框架

View File

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

View File

@@ -17,7 +17,7 @@
"countup.js": "^2.7.0", "countup.js": "^2.7.0",
"cropperjs": "^1.5.11", "cropperjs": "^1.5.11",
"echarts": "^5.4.3", "echarts": "^5.4.3",
"element-plus": "^2.5.1", "element-plus": "^2.5.3",
"js-base64": "^3.7.5", "js-base64": "^3.7.5",
"jsencrypt": "^3.3.2", "jsencrypt": "^3.3.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
@@ -33,7 +33,7 @@
"splitpanes": "^3.1.5", "splitpanes": "^3.1.5",
"sql-formatter": "^15.0.2", "sql-formatter": "^15.0.2",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"vue": "^3.4.14", "vue": "^3.4.15",
"vue-router": "^4.2.5", "vue-router": "^4.2.5",
"xterm": "^5.3.0", "xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0", "xterm-addon-fit": "^0.8.0",
@@ -49,13 +49,14 @@
"@typescript-eslint/parser": "^6.7.4", "@typescript-eslint/parser": "^6.7.4",
"@vitejs/plugin-vue": "^5.0.3", "@vitejs/plugin-vue": "^5.0.3",
"@vue/compiler-sfc": "^3.4.14", "@vue/compiler-sfc": "^3.4.14",
"code-inspector-plugin": "^0.4.5",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"eslint": "^8.35.0", "eslint": "^8.35.0",
"eslint-plugin-vue": "^9.19.2", "eslint-plugin-vue": "^9.19.2",
"prettier": "^3.1.0", "prettier": "^3.1.0",
"sass": "^1.69.0", "sass": "^1.69.0",
"typescript": "^5.3.2", "typescript": "^5.3.2",
"vite": "^5.0.11", "vite": "^5.0.12",
"vue-eslint-parser": "^9.4.0" "vue-eslint-parser": "^9.4.0"
}, },
"browserslist": [ "browserslist": [

File diff suppressed because one or more lines are too long

View File

@@ -55,11 +55,11 @@
"unicode_decimal": 58905 "unicode_decimal": 58905
}, },
{ {
"icon_id": "11617944", "icon_id": "25271976",
"name": "oracle", "name": "oracle",
"font_class": "oracle", "font_class": "oracle",
"unicode": "e6ea", "unicode": "e507",
"unicode_decimal": 59114 "unicode_decimal": 58631
}, },
{ {
"icon_id": "8105644", "icon_id": "8105644",
@@ -67,6 +67,27 @@
"font_class": "mariadb", "font_class": "mariadb",
"unicode": "e513", "unicode": "e513",
"unicode_decimal": 58643 "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
} }
] ]
} }

View File

@@ -15,7 +15,7 @@ const config = {
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`, baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
// 系统版本 // 系统版本
version: 'v1.7.0', version: 'v1.7.2',
}; };
export default config; export default config;

View File

@@ -1,5 +1,5 @@
<template> <template>
<el-input v-model="cron" placeholder="可点击左边按钮进行可视化配置"> <el-input v-model="cron" placeholder="可点击左边按钮配置">
<template #prepend> <template #prepend>
<el-button @click="showCron = true" icon="Pointer"></el-button> <el-button @click="showCron = true" icon="Pointer"></el-button>
</template> </template>

View File

@@ -1,17 +1,22 @@
<template> <template>
<div class="layout-search-dialog"> <div class="layout-search-dialog">
<el-dialog v-model="state.isShowSearch" width="300px" destroy-on-close :modal="false" fullscreen :show-close="false"> <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="菜单搜索" <el-autocomplete
prefix-icon="el-icon-search" ref="layoutMenuAutocompleteRef" @select="onHandleSelect" @blur="onSearchBlur"> v-model="state.menuQuery"
:fetch-suggestions="menuSearch"
placeholder="菜单搜索"
prefix-icon="el-icon-search"
ref="layoutMenuAutocompleteRef"
@select="onHandleSelect"
@blur="onSearchBlur"
>
<template #prefix> <template #prefix>
<el-icon class="el-input__icon"> <el-icon class="el-input__icon">
<search /> <search />
</el-icon> </el-icon>
</template> </template>
<template #default="{ item }"> <template #default="{ item }">
<div> <div><SvgIcon :name="item.meta.icon" class="mr5" />{{ item.meta.title }}</div>
<SvgIcon :name="item.meta.icon" class="mr5" />{{ item.meta.title }}
</div>
</template> </template>
</el-autocomplete> </el-autocomplete>
</el-dialog> </el-dialog>
@@ -23,7 +28,7 @@ import { reactive, ref, nextTick } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useRoutesList } from '@/store/routesList'; import { useRoutesList } from '@/store/routesList';
const layoutMenuAutocompleteRef: any = ref(null);; const layoutMenuAutocompleteRef: any = ref(null);
const router = useRouter(); const router = useRouter();
const state: any = reactive({ const state: any = reactive({
isShowSearch: false, isShowSearch: false,
@@ -54,8 +59,7 @@ const menuSearch = (queryString: any, cb: any) => {
const createFilter = (queryString: any) => { const createFilter = (queryString: any) => {
return (restaurant: any) => { return (restaurant: any) => {
return ( return (
restaurant.path.toLowerCase().indexOf(queryString.toLowerCase()) > -1 || restaurant.path.toLowerCase().indexOf(queryString.toLowerCase()) > -1 || restaurant.meta.title.toLowerCase().indexOf(queryString.toLowerCase()) > -1
restaurant.meta.title.toLowerCase().indexOf(queryString.toLowerCase()) > -1
); );
}; };
}; };
@@ -97,7 +101,7 @@ const onSearchBlur = () => {
closeSearch(); closeSearch();
}; };
defineExpose({openSearch}) defineExpose({ openSearch });
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -73,12 +73,28 @@ const currentTime = computed(() => {
// 初始化数字滚动 // 初始化数字滚动
const initNumCountUp = async () => { const initNumCountUp = async () => {
const res: any = await indexApi.getIndexCount.request(); 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(() => { nextTick(() => {
new CountUp('mongoNum', res.mongoNum).start(); new CountUp('mongoNum', res.mongoNum).start();
new CountUp('machineNum', res.machineNum).start(); });
new CountUp('dbNum', res.dbNum).start();
new CountUp('redisNum', res.redisNum).start();
}); });
}; };

View File

@@ -1,6 +1,8 @@
import Api from '@/common/Api'; import Api from '@/common/Api';
export const indexApi = { 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'),
};

View File

@@ -23,7 +23,7 @@
</el-form-item> </el-form-item>
<el-form-item prop="name" label="任务名称"> <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>
<el-form-item prop="startTime" label="开始时间"> <el-form-item prop="startTime" label="开始时间">
<el-date-picker v-model="state.form.startTime" type="datetime" placeholder="开始时间" /> <el-date-picker v-model="state.form.startTime" type="datetime" placeholder="开始时间" />
@@ -101,7 +101,7 @@ const state = reactive({
id: 0, id: 0,
dbId: 0, dbId: 0,
dbNames: '', dbNames: '',
name: null as any, name: '',
intervalDay: null, intervalDay: null,
startTime: null as any, startTime: null as any,
repeated: null as any, repeated: null as any,

View 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>

View File

@@ -21,6 +21,7 @@
<el-button type="primary" icon="plus" @click="createDbBackup()">添加</el-button> <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-play" @click="enableDbBackup(null)">启用</el-button>
<el-button type="primary" icon="video-pause" @click="disableDbBackup(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>
<template #action="{ data }"> <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="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="disableDbBackup(data)" type="primary" link>禁用</el-button>
<el-button v-if="data.enabled" @click="startDbBackup(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> </div>
</template> </template>
</page-table> </page-table>
@@ -49,7 +51,7 @@ import { dbApi } from './api';
import PageTable from '@/components/pagetable/PageTable.vue'; import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable'; import { TableColumn } from '@/components/pagetable';
import { SearchItem } from '@/components/SearchForm'; import { SearchItem } from '@/components/SearchForm';
import { ElMessage } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
const DbBackupEdit = defineAsyncComponent(() => import('./DbBackupEdit.vue')); const DbBackupEdit = defineAsyncComponent(() => import('./DbBackupEdit.vue'));
const pageTableRef: Ref<any> = ref(null); const pageTableRef: Ref<any> = ref(null);
@@ -72,10 +74,10 @@ const columns = [
TableColumn.new('name', '任务名称'), TableColumn.new('name', '任务名称'),
TableColumn.new('startTime', '启动时间').isTime(), TableColumn.new('startTime', '启动时间').isTime(),
TableColumn.new('intervalDay', '备份周期'), TableColumn.new('intervalDay', '备份周期'),
TableColumn.new('enabled', '是否启用'), TableColumn.new('enabledDesc', '是否启用'),
TableColumn.new('lastResult', '执行结果'), TableColumn.new('lastResult', '执行结果'),
TableColumn.new('lastTime', '执行时间').isTime(), TableColumn.new('lastTime', '执行时间').isTime(),
TableColumn.new('action', '操作').isSlot().setMinWidth(180).fixedRight(), TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight(),
]; ];
const emptyQuery = { const emptyQuery = {
@@ -168,5 +170,25 @@ const startDbBackup = async (data: any) => {
await search(); await search();
ElMessage.success('备份任务启动成功'); 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> </script>
<style lang="scss"></style> <style lang="scss"></style>

View File

@@ -62,8 +62,21 @@
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item :command="{ type: 'detail', data }"> 详情 </el-dropdown-item> <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: '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: 'backupDb', data }" v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)">
<el-dropdown-item :command="{ type: 'dbRestore', data }" v-if="supportAction('dbRestore', data.type)"> 恢复 </el-dropdown-item> 备份任务
</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> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
@@ -131,6 +144,16 @@
<db-backup-list :dbId="dbBackupDialog.dbId" :dbNames="dbBackupDialog.dbs" /> <db-backup-list :dbId="dbBackupDialog.dbId" :dbNames="dbBackupDialog.dbs" />
</el-dialog> </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 <el-dialog
width="80%" width="80%"
:title="`${dbRestoreDialog.title} - 数据库恢复`" :title="`${dbRestoreDialog.title} - 数据库恢复`"
@@ -185,6 +208,7 @@ import { getDbDialect } from './dialect/index';
import { getTagPathSearchItem } from '../component/tag'; import { getTagPathSearchItem } from '../component/tag';
import { SearchItem } from '@/components/SearchForm'; import { SearchItem } from '@/components/SearchForm';
import DbBackupList from './DbBackupList.vue'; import DbBackupList from './DbBackupList.vue';
import DbBackupHistoryList from './DbBackupHistoryList.vue';
import DbRestoreList from './DbRestoreList.vue'; import DbRestoreList from './DbRestoreList.vue';
const DbEdit = defineAsyncComponent(() => import('./DbEdit.vue')); const DbEdit = defineAsyncComponent(() => import('./DbEdit.vue'));
@@ -193,6 +217,8 @@ const perms = {
base: 'db', base: 'db',
saveDb: 'db:save', saveDb: 'db:save',
delDb: 'db:del', delDb: 'db:del',
backupDb: 'db:backup',
restoreDb: 'db:restore',
}; };
const searchItems = [getTagPathSearchItem(TagResourceTypeEnum.Db.value), SearchItem.slot('instanceId', '实例', 'instanceSelect')]; 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 actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight().alignCenter();
const route = useRoute(); const route = useRoute();
@@ -253,6 +280,13 @@ const state = reactive({
dbs: [], dbs: [],
dbId: 0, dbId: 0,
}, },
// 数据库备份历史弹框
dbBackupHistoryDialog: {
title: '',
visible: false,
dbs: [],
dbId: 0,
},
// 数据库恢复弹框 // 数据库恢复弹框
dbRestoreDialog: { dbRestoreDialog: {
title: '', 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 () => { onMounted(async () => {
if (Object.keys(actionBtns).length > 0) { if (Object.keys(actionBtns).length > 0) {
@@ -345,11 +380,15 @@ const handleMoreActionCommand = (commond: any) => {
onDumpDbs(data); onDumpDbs(data);
return; return;
} }
case 'dbBackup': { case 'backupDb': {
onShowDbBackupDialog(data); onShowDbBackupDialog(data);
return; return;
} }
case 'dbRestore': { case 'backupHistory': {
onShowDbBackupHistoryDialog(data);
return;
}
case 'restoreDb': {
onShowDbRestoreDialog(data); onShowDbRestoreDialog(data);
return; return;
} }
@@ -402,6 +441,13 @@ const onShowDbBackupDialog = async (row: any) => {
state.dbBackupDialog.visible = true; 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) => { const onShowDbRestoreDialog = async (row: any) => {
state.dbRestoreDialog.title = `${row.name}`; state.dbRestoreDialog.title = `${row.name}`;
state.dbRestoreDialog.dbId = row.id; state.dbRestoreDialog.dbId = row.id;
@@ -455,7 +501,7 @@ const supportAction = (action: string, dbType: string): boolean => {
switch (dbType) { switch (dbType) {
case DbType.mysql: case DbType.mysql:
case DbType.mariadb: case DbType.mariadb:
actions = ['dumpDb', 'dbBackup', 'dbRestore']; actions = ['dumpDb', 'backupDb', 'restoreDb'];
} }
return actions.includes(action); return actions.includes(action);
}; };

View File

@@ -35,7 +35,13 @@
clearable clearable
class="w100" 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-select>
</el-form-item> </el-form-item>
<el-form-item prop="startTime" label="开始时间"> <el-form-item prop="startTime" label="开始时间">
@@ -56,7 +62,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, reactive, ref, watch } from 'vue'; import { onMounted, reactive, ref, watch } from 'vue';
import { dbApi } from './api'; import { dbApi } from './api';
import { ElMessage } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
const props = defineProps({ const props = defineProps({
data: { data: {
@@ -83,20 +89,30 @@ const visible = defineModel<boolean>('visible', {
}); });
const validatePointInTime = (rule: any, value: any, callback: any) => { 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()) { if (value > new Date()) {
callback(new Error('恢复时间点晚于当前时间')); callback(new Error('恢复时间点晚于当前时间'));
return; return;
} }
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(); callback();
return;
}
last = history;
}
if (!last) {
callback(new Error('现有数据库备份不支持指定时间恢复'));
return;
}
callback(last.name + ' 之前的数据库备份不支持指定时间恢复');
}; };
const rules = { const rules = {
@@ -110,7 +126,6 @@ const rules = {
pointInTime: [ pointInTime: [
{ {
required: true, required: true,
// message: '请选择恢复时间点',
validator: validatePointInTime, validator: validatePointInTime,
trigger: ['change', 'blur'], trigger: ['change', 'blur'],
}, },
@@ -146,7 +161,7 @@ const state = reactive({
id: 0, id: 0,
dbId: 0, dbId: 0,
dbName: null as any, dbName: null as any,
intervalDay: 1, intervalDay: 0,
startTime: null as any, startTime: null as any,
repeated: null as any, repeated: null as any,
dbBackupId: null as any, dbBackupId: null as any,
@@ -218,7 +233,8 @@ const init = async (data: any) => {
} else { } else {
state.form.dbName = ''; state.form.dbName = '';
state.editOrCreate = false; state.editOrCreate = false;
state.form.intervalDay = 1; state.form.intervalDay = 0;
state.form.repeated = false;
state.form.pointInTime = new Date(); state.form.pointInTime = new Date();
state.form.startTime = new Date(); state.form.startTime = new Date();
state.histories = []; state.histories = [];
@@ -237,6 +253,12 @@ const getDbNamesWithoutRestore = async () => {
const btnOk = async () => { const btnOk = async () => {
restoreForm.value.validate(async (valid: any) => { restoreForm.value.validate(async (valid: any) => {
if (valid) { if (valid) {
await ElMessageBox.confirm(`确定恢复数据库吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
if (state.restoreMode == 'point-in-time') { if (state.restoreMode == 'point-in-time') {
state.form.dbBackupId = 0; state.form.dbBackupId = 0;
state.form.dbBackupHistoryId = 0; state.form.dbBackupHistoryId = 0;
@@ -245,13 +267,14 @@ const btnOk = async () => {
state.form.pointInTime = null; state.form.pointInTime = null;
} }
state.form.repeated = false; state.form.repeated = false;
state.form.intervalDay = 0;
const reqForm = { ...state.form }; const reqForm = { ...state.form };
let api = dbApi.createDbRestore; let api = dbApi.createDbRestore;
if (props.data) { if (props.data) {
api = dbApi.saveDbRestore; api = dbApi.saveDbRestore;
} }
api.request(reqForm).then(() => { api.request(reqForm).then(() => {
ElMessage.success('保存成功'); ElMessage.success('成功创建数据库恢复任务');
emit('val-change', state.form); emit('val-change', state.form);
state.btnLoading = true; state.btnLoading = true;
setTimeout(() => { setTimeout(() => {

View File

@@ -21,12 +21,14 @@
<el-button type="primary" icon="plus" @click="createDbRestore()">添加</el-button> <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-play" @click="enableDbRestore(null)">启用</el-button>
<el-button type="primary" icon="video-pause" @click="disableDbRestore(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>
<template #action="{ data }"> <template #action="{ data }">
<el-button @click="showDbRestore(data)" type="primary" link>详情</el-button> <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="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="disableDbRestore(data)" v-if="data.enabled" type="primary" link>禁用</el-button>
<el-button @click="deleteDbRestore(data)" type="danger" link>删除</el-button>
</template> </template>
</page-table> </page-table>
@@ -49,7 +51,7 @@
infoDialog.data.dbBackupHistoryName infoDialog.data.dbBackupHistoryName
}}</el-descriptions-item> }}</el-descriptions-item>
<el-descriptions-item :span="1" label="开始时间">{{ dateFormat(infoDialog.data.startTime) }}</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="执行时间">{{ dateFormat(infoDialog.data.lastTime) }}</el-descriptions-item>
<el-descriptions-item :span="1" label="执行结果">{{ infoDialog.data.lastResult }}</el-descriptions-item> <el-descriptions-item :span="1" label="执行结果">{{ infoDialog.data.lastResult }}</el-descriptions-item>
</el-descriptions> </el-descriptions>
@@ -63,7 +65,7 @@ import { dbApi } from './api';
import PageTable from '@/components/pagetable/PageTable.vue'; import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable'; import { TableColumn } from '@/components/pagetable';
import { SearchItem } from '@/components/SearchForm'; import { SearchItem } from '@/components/SearchForm';
import { ElMessage } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import { dateFormat } from '@/common/utils/date'; import { dateFormat } from '@/common/utils/date';
const DbRestoreEdit = defineAsyncComponent(() => import('./DbRestoreEdit.vue')); const DbRestoreEdit = defineAsyncComponent(() => import('./DbRestoreEdit.vue'));
const pageTableRef: Ref<any> = ref(null); const pageTableRef: Ref<any> = ref(null);
@@ -85,7 +87,7 @@ const searchItems = [SearchItem.slot('dbName', '数据库名称', 'dbSelect')];
const columns = [ const columns = [
TableColumn.new('dbName', '数据库名称'), TableColumn.new('dbName', '数据库名称'),
TableColumn.new('startTime', '启动时间').isTime(), TableColumn.new('startTime', '启动时间').isTime(),
TableColumn.new('enabled', '是否启用'), TableColumn.new('enabledDesc', '是否启用'),
TableColumn.new('lastTime', '执行时间').isTime(), TableColumn.new('lastTime', '执行时间').isTime(),
TableColumn.new('lastResult', '执行结果'), TableColumn.new('lastResult', '执行结果'),
TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight().alignCenter(), TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight().alignCenter(),
@@ -135,19 +137,39 @@ const createDbRestore = async () => {
state.dbRestoreEditDialog.visible = true; 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) => { const showDbRestore = async (data: any) => {
state.infoDialog.data = data; state.infoDialog.data = data;
state.infoDialog.visible = true; state.infoDialog.visible = true;
}; };
const enableDbRestore = async (data: any) => { const enableDbRestore = async (data: any) => {
let restoreId: String; let restoreId: string;
if (data) { if (data) {
restoreId = data.id; restoreId = data.id;
} else if (state.selectedData.length > 0) { } else if (state.selectedData.length > 0) {
restoreId = state.selectedData.map((x: any) => x.id).join(' '); restoreId = state.selectedData.map((x: any) => x.id).join(' ');
} else { } else {
ElMessage.error('请选择需要启用的恢复任务'); ElMessage.error('请选择需要启用的数据库恢复任务');
return; return;
} }
await dbApi.enableDbRestore.request({ dbId: props.dbId, restoreId: restoreId }); await dbApi.enableDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
@@ -156,13 +178,13 @@ const enableDbRestore = async (data: any) => {
}; };
const disableDbRestore = async (data: any) => { const disableDbRestore = async (data: any) => {
let restoreId: String; let restoreId: string;
if (data) { if (data) {
restoreId = data.id; restoreId = data.id;
} else if (state.selectedData.length > 0) { } else if (state.selectedData.length > 0) {
restoreId = state.selectedData.map((x: any) => x.id).join(' '); restoreId = state.selectedData.map((x: any) => x.id).join(' ');
} else { } else {
ElMessage.error('请选择需要禁用的恢复任务'); ElMessage.error('请选择需要禁用的数据库恢复任务');
return; return;
} }
await dbApi.disableDbRestore.request({ dbId: props.dbId, restoreId: restoreId }); await dbApi.disableDbRestore.request({ dbId: props.dbId, restoreId: restoreId });

View File

@@ -36,7 +36,7 @@
</template> </template>
<script lang="ts" setup> <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 { dbApi } from './api';
import { DbSqlExecTypeEnum } from './enums'; import { DbSqlExecTypeEnum } from './enums';
import PageTable from '@/components/pagetable/PageTable.vue'; import PageTable from '@/components/pagetable/PageTable.vue';
@@ -120,6 +120,12 @@ const onShowRollbackSql = async (sqlExecLog: any) => {
const primaryKey = getPrimaryKey(columns); const primaryKey = getPrimaryKey(columns);
const oldValue = JSON.parse(sqlExecLog.oldValue); const oldValue = JSON.parse(sqlExecLog.oldValue);
let schema = '';
let dbArr = sqlExecLog.db.split('/');
if (dbArr.length == 2) {
schema = dbArr[1] + '.';
}
const rollbackSqls = []; const rollbackSqls = [];
if (sqlExecLog.type == DbSqlExecTypeEnum.Update.value) { if (sqlExecLog.type == DbSqlExecTypeEnum.Update.value) {
for (let ov of oldValue) { for (let ov of oldValue) {
@@ -130,7 +136,7 @@ const onShowRollbackSql = async (sqlExecLog: any) => {
} }
setItems.push(`${key} = ${wrapValue(ov[key])}`); 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) { } else if (sqlExecLog.type == DbSqlExecTypeEnum.Delete.value) {
const columnNames = columns.map((c: any) => c.columnName); const columnNames = columns.map((c: any) => c.columnName);
@@ -139,7 +145,7 @@ const onShowRollbackSql = async (sqlExecLog: any) => {
for (let column of columnNames) { for (let column of columnNames) {
values.push(wrapValue(ov[column])); 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 getPrimaryKey = (columns: any) => {
const col = columns.find((c: any) => c.columnKey == 'PRI'); const col = columns.find((c: any) => c.isPrimaryKey);
if (col) { if (col) {
return col.columnName; return col.columnName;
} }

View File

@@ -9,13 +9,22 @@
</el-form-item> </el-form-item>
<el-form-item prop="type" label="类型" required> <el-form-item prop="type" label="类型" required>
<el-select @change="changeDbType" style="width: 100%" v-model="form.type" placeholder="请选择数据库类型"> <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"> <el-option
<SvgIcon :name="getDbDialect(dt.type).getInfo().icon" :size="18" /> v-for="(dbTypeAndDialect, key) in getDbDialectMap()"
{{ dt.label }} :key="key"
:value="dbTypeAndDialect[0]"
:label="dbTypeAndDialect[1].getInfo().name"
>
<SvgIcon :name="dbTypeAndDialect[1].getInfo().icon" :size="20" />
{{ dbTypeAndDialect[1].getInfo().name }}
</el-option> </el-option>
<template #prefix>
<SvgIcon :name="getDbDialect(form.type).getInfo().icon" :size="20" />
</template>
</el-select> </el-select>
</el-form-item> </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-col :span="18">
<el-input :disabled="form.id !== undefined" v-model.trim="form.host" placeholder="请输入主机ip" auto-complete="off"></el-input> <el-input :disabled="form.id !== undefined" v-model.trim="form.host" placeholder="请输入主机ip" auto-complete="off"></el-input>
</el-col> </el-col>
@@ -24,13 +33,18 @@
<el-input type="number" v-model.number="form.port" placeholder="端口"></el-input> <el-input type="number" v-model.number="form.port" placeholder="端口"></el-input>
</el-col> </el-col>
</el-form-item> </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-form-item v-if="form.type === DbType.oracle" prop="sid" label="SID">
<el-input v-model.trim="form.sid" placeholder="请输入服务id"></el-input> <el-input v-model.trim="form.sid" placeholder="请输入服务id"></el-input>
</el-form-item> </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-input v-model.trim="form.username" placeholder="请输入用户名"></el-input>
</el-form-item> </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"> <el-input type="password" show-password v-model.trim="form.password" placeholder="请输入密码" autocomplete="new-password">
<template v-if="form.id && form.id != 0" #suffix> <template v-if="form.id && form.id != 0" #suffix>
<el-popover @hide="pwd = ''" placement="right" title="原密码" :width="200" trigger="click" :content="pwd"> <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 { notBlank } from '@/common/assert';
import { RsaEncrypt } from '@/common/rsa'; import { RsaEncrypt } from '@/common/rsa';
import SshTunnelSelect from '../component/SshTunnelSelect.vue'; import SshTunnelSelect from '../component/SshTunnelSelect.vue';
import { DbType, getDbDialect } from './dialect'; import { DbType, getDbDialect, getDbDialectMap } from './dialect';
import SvgIcon from '@/components/svgIcon/index.vue'; import SvgIcon from '@/components/svgIcon/index.vue';
const props = defineProps({ const props = defineProps({
@@ -148,35 +162,12 @@ const rules = {
const dbForm: any = ref(null); 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({ const state = reactive({
dialogVisible: false, dialogVisible: false,
tabActiveName: 'basic', tabActiveName: 'basic',
form: { form: {
id: null, id: null,
type: null, type: '',
name: null, name: null,
host: '', host: '',
port: null, port: null,
@@ -187,17 +178,17 @@ const state = reactive({
remark: '', remark: '',
sshTunnelMachineId: null as any, sshTunnelMachineId: null as any,
}, },
subimtForm: {}, submitForm: {},
// 原密码 // 原密码
pwd: '', pwd: '',
// 原用户名 // 原用户名
oldUserName: null, 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: saveBtnLoading, execute: saveInstanceExec } = dbApi.saveInstance.useApi(submitForm);
const { isFetching: testConnBtnLoading, execute: testConnExec } = dbApi.testConn.useApi(subimtForm); const { isFetching: testConnBtnLoading, execute: testConnExec } = dbApi.testConn.useApi(submitForm);
watch(props, (newValue: any) => { watch(props, (newValue: any) => {
state.dialogVisible = newValue.visible; state.dialogVisible = newValue.visible;
@@ -209,7 +200,7 @@ watch(props, (newValue: any) => {
state.form = { ...newValue.data }; state.form = { ...newValue.data };
state.oldUserName = state.form.username; state.oldUserName = state.form.username;
} else { } else {
state.form = { port: null } as any; state.form = { port: null, type: DbType.mysql } as any;
state.oldUserName = null; state.oldUserName = null;
} }
}); });
@@ -240,18 +231,20 @@ const testConn = async () => {
return false; return false;
} }
state.subimtForm = await getReqForm(); state.submitForm = await getReqForm();
await testConnExec(); await testConnExec();
ElMessage.success('连接成功'); ElMessage.success('连接成功');
}); });
}; };
const btnOk = async () => { const btnOk = async () => {
if (state.form.type !== DbType.sqlite) {
if (!state.form.id) { if (!state.form.id) {
notBlank(state.form.password, '新增操作,密码不可为空'); notBlank(state.form.password, '新增操作,密码不可为空');
} else if (state.form.username != state.oldUserName) { } else if (state.form.username != state.oldUserName) {
notBlank(state.form.password, '已修改用户名,请输入密码'); notBlank(state.form.password, '已修改用户名,请输入密码');
} }
}
dbForm.value.validate(async (valid: boolean) => { dbForm.value.validate(async (valid: boolean) => {
if (!valid) { if (!valid) {
@@ -259,7 +252,7 @@ const btnOk = async () => {
return false; return false;
} }
state.subimtForm = await getReqForm(); state.submitForm = await getReqForm();
await saveInstanceExec(); await saveInstanceExec();
ElMessage.success('保存成功'); ElMessage.success('保存成功');
emit('val-change', state.form); emit('val-change', state.form);

View File

@@ -151,12 +151,22 @@
</div> </div>
</Pane> </Pane>
</Splitpanes> </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> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { defineAsyncComponent, onBeforeUnmount, onMounted, reactive, ref, toRefs } from 'vue'; import { defineAsyncComponent, h, onBeforeUnmount, onMounted, reactive, ref, toRefs } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElCheckbox, ElMessage, ElMessageBox } from 'element-plus';
import { formatByteSize } from '@/common/utils/format'; import { formatByteSize } from '@/common/utils/format';
import { DbInst, registerDbCompletionItemProvider, TabInfo, TabType } from './db'; import { DbInst, registerDbCompletionItemProvider, TabInfo, TabType } from './db';
import { NodeType, TagTreeNode } from '../component/tag'; import { NodeType, TagTreeNode } from '../component/tag';
@@ -165,12 +175,13 @@ import { dbApi } from './api';
import { dispposeCompletionItemProvider } from '@/components/monaco/completionItemProvider'; import { dispposeCompletionItemProvider } from '@/components/monaco/completionItemProvider';
import SvgIcon from '@/components/svgIcon/index.vue'; import SvgIcon from '@/components/svgIcon/index.vue';
import { ContextmenuItem } from '@/components/contextmenu'; import { ContextmenuItem } from '@/components/contextmenu';
import { DbType, getDbDialect } from './dialect/index'; import { getDbDialect, schemaDbTypes } from './dialect/index';
import { sleep } from '@/common/utils/loading'; import { sleep } from '@/common/utils/loading';
import { TagResourceTypeEnum } from '@/common/commonEnum'; import { TagResourceTypeEnum } from '@/common/commonEnum';
import { Pane, Splitpanes } from 'splitpanes'; import { Pane, Splitpanes } from 'splitpanes';
import { useEventListener } from '@vueuse/core'; import { useEventListener } from '@vueuse/core';
const DbTableOp = defineAsyncComponent(() => import('./component/table/DbTableOp.vue'));
const DbSqlEditor = defineAsyncComponent(() => import('./component/sqleditor/DbSqlEditor.vue')); const DbSqlEditor = defineAsyncComponent(() => import('./component/sqleditor/DbSqlEditor.vue'));
const DbTableDataOp = defineAsyncComponent(() => import('./component/table/DbTableDataOp.vue')); const DbTableDataOp = defineAsyncComponent(() => import('./component/table/DbTableDataOp.vue'));
const DbTablesOp = defineAsyncComponent(() => import('./component/table/DbTablesOp.vue')); const DbTablesOp = defineAsyncComponent(() => import('./component/table/DbTablesOp.vue'));
@@ -218,8 +229,11 @@ const nodeClickChangeDb = (nodeData: TagTreeNode) => {
} }
}; };
const ContextmenuItemRefresh = new ContextmenuItem('refresh', '刷新').withIcon('RefreshRight').withOnClick((data: any) => reloadNode(data.key));
// tagpath 节点类型 // tagpath 节点类型
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (parentNode: TagTreeNode) => { const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath)
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const dbInfoRes = await dbApi.dbs.request({ tagPath: parentNode.key }); const dbInfoRes = await dbApi.dbs.request({ tagPath: parentNode.key });
const dbInfos = dbInfoRes.list; const dbInfos = dbInfoRes.list;
if (!dbInfos) { if (!dbInfos) {
@@ -232,7 +246,8 @@ const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(asyn
x.tagPath = parentNode.key; x.tagPath = parentNode.key;
return new TagTreeNode(`${parentNode.key}.${x.id}`, x.name, NodeTypeDbInst).withParams(x); return new TagTreeNode(`${parentNode.key}.${x.id}`, x.name, NodeTypeDbInst).withParams(x);
}); });
}); })
.withContextMenuItems([ContextmenuItemRefresh]);
// 数据库实例节点类型 // 数据库实例节点类型
const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((parentNode: TagTreeNode) => { 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) const NodeTypeDb = new NodeType(SqlExecNodeType.Db)
.withContextMenuItems([new ContextmenuItem('reloadTables', '刷新').withIcon('RefreshRight').withOnClick((data: any) => reloadNode(data.key))]) .withContextMenuItems([ContextmenuItemRefresh])
.withLoadNodesFunc(async (parentNode: TagTreeNode) => { .withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const params = parentNode.params; const params = parentNode.params;
params.parentKey = parentNode.key;
// pg类数据库会多一层schema // pg类数据库会多一层schema
if (params.type == DbType.postgresql || params.type === DbType.dm || params.type === DbType.oracle) { if (schemaDbTypes.includes(params.type)) {
const params = parentNode.params;
const { id, db } = params; const { id, db } = params;
const schemaNames = await dbApi.pgSchemas.request({ id, db }); const schemaNames = await dbApi.pgSchemas.request({ id, db });
return schemaNames.map((sn: any) => { return schemaNames.map((sn: any) => {
@@ -269,33 +284,37 @@ const NodeTypeDb = new NodeType(SqlExecNodeType.Db)
nParams.schema = sn; nParams.schema = sn;
nParams.db = nParams.db + '/' + sn; nParams.db = nParams.db + '/' + sn;
nParams.dbs = schemaNames; 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 [ return NodeTypeTables(params);
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),
];
}) })
.withNodeClickFunc(nodeClickChangeDb); .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模式 // postgres schema模式
const NodeTypePostgresScheam = new NodeType(SqlExecNodeType.PgSchema) const NodeTypePostgresSchema = new NodeType(SqlExecNodeType.PgSchema)
.withContextMenuItems([new ContextmenuItem('reloadTables', '刷新').withIcon('RefreshRight').withOnClick((data: any) => reloadNode(data.key))]) .withContextMenuItems([ContextmenuItemRefresh])
.withLoadNodesFunc(async (parentNode: TagTreeNode) => { .withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const params = parentNode.params; const params = parentNode.params;
return [ params.parentKey = parentNode.key;
new TagTreeNode(`${params.id}.${params.db}.table-menu`, '表', NodeTypeTableMenu).withParams(params).withIcon(TableIcon), return NodeTypeTables(params);
new TagTreeNode(getSqlMenuNodeKey(params.id, params.db), 'SQL', NodeTypeSqlMenu).withParams(params).withIcon(SqlIcon),
];
}) })
.withNodeClickFunc(nodeClickChangeDb); .withNodeClickFunc(nodeClickChangeDb);
// 数据库表菜单节点 // 数据库表菜单节点
const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu) const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
.withContextMenuItems([ .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) => { new ContextmenuItem('tablesOp', '表操作').withIcon('Setting').withOnClick((data: any) => {
const params = data.params; const params = data.params;
addTablesOpTab({ id: params.id, db: params.db, type: params.type, nodeKey: data.key }); 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) => { .withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const params = parentNode.params; const params = parentNode.params;
let { id, db } = params; let { id, db, type } = params;
// 获取当前库的所有表信息 // 获取当前库的所有表信息
let tables = await DbInst.getInst(id).loadTables(db, state.reloadStatus); let tables = await DbInst.getInst(id).loadTables(db, state.reloadStatus);
state.reloadStatus = false; state.reloadStatus = false;
let dbTableSize = 0; let dbTableSize = 0;
const tablesNode = tables.map((x: any) => { const tablesNode = tables.map((x: any) => {
dbTableSize += x.dataLength + x.indexLength; const tableSize = x.dataLength + x.indexLength;
return new TagTreeNode(`${id}.${db}.${x.tableName}`, x.tableName, NodeTypeTable) dbTableSize += tableSize;
const key = `${id}.${db}.${x.tableName}`;
return new TagTreeNode(key, x.tableName, NodeTypeTable)
.withIsLeaf(true) .withIsLeaf(true)
.withParams({ .withParams({
id, id,
db, db,
type,
key: key,
parentKey: parentNode.key,
tableName: x.tableName, tableName: x.tableName,
tableComment: x.tableComment, tableComment: x.tableComment,
size: formatByteSize(x.dataLength + x.indexLength, 1), size: tableSize == 0 ? '' : formatByteSize(tableSize, 1),
}) })
.withIcon(TableIcon) .withIcon(TableIcon)
.withLabelRemark(`${x.tableName} ${x.tableComment ? '| ' + x.tableComment : ''}`); .withLabelRemark(`${x.tableName} ${x.tableComment ? '| ' + x.tableComment : ''}`);
}); });
// 设置父节点参数的表大小 // 设置父节点参数的表大小
parentNode.params.dbTableSize = formatByteSize(dbTableSize); parentNode.params.dbTableSize = dbTableSize == 0 ? '' : formatByteSize(dbTableSize);
return tablesNode; return tablesNode;
}) })
.withNodeClickFunc(nodeClickChangeDb); .withNodeClickFunc(nodeClickChangeDb);
@@ -340,22 +364,23 @@ const NodeTypeSqlMenu = new NodeType(SqlExecNodeType.SqlMenu)
return sqls.map((x: any) => { return sqls.map((x: any) => {
return new TagTreeNode(`${id}.${db}.${x.name}`, x.name, NodeTypeSql) return new TagTreeNode(`${id}.${db}.${x.name}`, x.name, NodeTypeSql)
.withIsLeaf(true) .withIsLeaf(true)
.withParams({ .withParams({ id, db, dbs, sqlName: x.name })
id,
db,
dbs,
sqlName: x.name,
})
.withIcon(SqlIcon); .withIcon(SqlIcon);
}); });
}) })
.withNodeClickFunc(nodeClickChangeDb); .withNodeClickFunc(nodeClickChangeDb);
// 表节点类型 // 表节点类型
const NodeTypeTable = new NodeType(SqlExecNodeType.Table).withNodeClickFunc((nodeData: TagTreeNode) => { 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; const params = nodeData.params;
loadTableData({ id: params.id, nodeKey: nodeData.key }, params.db, params.tableName); loadTableData({ id: params.id, nodeKey: nodeData.key }, params.db, params.tableName);
}); });
// sql模板节点类型 // sql模板节点类型
const NodeTypeSql = new NodeType(SqlExecNodeType.Sql) const NodeTypeSql = new NodeType(SqlExecNodeType.Sql)
@@ -385,9 +410,19 @@ const state = reactive({
loading: true, loading: true,
version: '', version: '',
}, },
tableCreateDialog: {
visible: false,
title: '',
activeName: '',
dbId: 0,
db: '',
dbType: '',
data: {},
parentKey: '',
},
}); });
const { nowDbInst } = toRefs(state); const { nowDbInst, tableCreateDialog } = toRefs(state);
const serverInfoReqParam = ref({ const serverInfoReqParam = ref({
instanceId: 0, instanceId: 0,
@@ -408,7 +443,7 @@ onBeforeUnmount(() => {
* 设置editor高度和数据表高度 * 设置editor高度和数据表高度
*/ */
const setHeight = () => { const setHeight = () => {
state.dataTabsTableHeight = window.innerHeight - 270 + 'px'; state.dataTabsTableHeight = window.innerHeight - 253 + 'px';
state.tablesOpHeight = window.innerHeight - 225 + 'px'; state.tablesOpHeight = window.innerHeight - 225 + 'px';
}; };
@@ -603,6 +638,85 @@ const reloadNode = (nodeKey: string) => {
tagTreeRef.value.reloadNode(nodeKey); 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);
};
/** /**
* 获取当前操作的数据库信息 * 获取当前操作的数据库信息
*/ */

View File

@@ -47,6 +47,7 @@
v-model:db-id="form.srcDbId" v-model:db-id="form.srcDbId"
v-model:db-name="form.srcDbName" v-model:db-name="form.srcDbName"
v-model:tag-path="form.srcTagPath" v-model:tag-path="form.srcTagPath"
v-model:db-type="form.srcDbType"
@select-db="onSelectSrcDb" @select-db="onSelectSrcDb"
/> />
</el-form-item> </el-form-item>
@@ -181,7 +182,7 @@ import { ElMessage } from 'element-plus';
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue'; import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue'; import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { DbInst, registerDbCompletionItemProvider } from '@/views/ops/db/db'; 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'; import CrontabInput from '@/components/crontab/CrontabInput.vue';
const props = defineProps({ const props = defineProps({
@@ -227,6 +228,7 @@ type FormData = {
taskCron: string; taskCron: string;
srcDbId?: number; srcDbId?: number;
srcDbName?: string; srcDbName?: string;
srcDbType?: string;
srcTagPath?: string; srcTagPath?: string;
targetDbId?: number; targetDbId?: number;
targetDbName?: string; targetDbName?: string;
@@ -245,7 +247,7 @@ const basicFormData = {
targetDbId: -1, targetDbId: -1,
dataSql: 'select * from', dataSql: 'select * from',
pageSize: 1000, pageSize: 1000,
updField: 'id', updField: '',
updFieldVal: '0', updFieldVal: '0',
fieldMap: [{ src: 'a', target: 'b' }], fieldMap: [{ src: 'a', target: 'b' }],
status: 1, status: 1,
@@ -302,6 +304,7 @@ watch(dialogVisible, async (newValue: boolean) => {
// 初始化实例 // 初始化实例
db.databases = db.database?.split(' ').sort() || []; db.databases = db.database?.split(' ').sort() || [];
state.srcDbInst = DbInst.getOrNewInst(db); state.srcDbInst = DbInst.getOrNewInst(db);
state.form.srcDbType = state.srcDbInst.type
} }
// 初始化target数据源 // 初始化target数据源
@@ -396,8 +399,8 @@ const handleGetSrcFields = async () => {
} }
// 判断sql是否是查询语句 // 判断sql是否是查询语句
if (!/^select/i.test(state.form.dataSql!)) { if (!/^select/i.test(state.form.dataSql.trim()!)) {
let msg = 'sql语句错误请输入查询语句'; let msg = 'sql语句错误请输入select语句';
ElMessage.warning(msg); ElMessage.warning(msg);
return; return;
} }
@@ -410,10 +413,16 @@ const handleGetSrcFields = async () => {
} }
// 执行sql // 执行sql
// oracle的分页关键字不一样
let limit = ' limit 1'
if(state.form.srcDbType === DbType.oracle){
limit = ' where rownum <= 1'
}
const res = await dbApi.sqlExec.request({ const res = await dbApi.sqlExec.request({
id: state.form.srcDbId, id: state.form.srcDbId,
db: state.form.srcDbName, db: state.form.srcDbName,
sql: state.form.dataSql.trim() + ' limit 1', sql: `select * from (${state.form.dataSql}) t ${limit}`
}); });
if (!res.columns) { if (!res.columns) {

View File

@@ -11,6 +11,7 @@ export const dbApi = {
tableInfos: Api.newGet('/dbs/{id}/t-infos'), tableInfos: Api.newGet('/dbs/{id}/t-infos'),
tableIndex: Api.newGet('/dbs/{id}/t-index'), tableIndex: Api.newGet('/dbs/{id}/t-index'),
tableDdl: Api.newGet('/dbs/{id}/t-create-ddl'), tableDdl: Api.newGet('/dbs/{id}/t-create-ddl'),
copyTable: Api.newPost('/dbs/{id}/copy-table'),
columnMetadata: Api.newGet('/dbs/{id}/c-metadata'), columnMetadata: Api.newGet('/dbs/{id}/c-metadata'),
pgSchemas: Api.newGet('/dbs/{id}/pg/schemas'), pgSchemas: Api.newGet('/dbs/{id}/pg/schemas'),
// 获取表即列提示 // 获取表即列提示
@@ -48,16 +49,20 @@ export const dbApi = {
// 获取数据库备份列表 // 获取数据库备份列表
getDbBackups: Api.newGet('/dbs/{dbId}/backups'), getDbBackups: Api.newGet('/dbs/{dbId}/backups'),
createDbBackup: Api.newPost('/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'), getDbNamesWithoutBackup: Api.newGet('/dbs/{dbId}/db-names-without-backup'),
enableDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/enable'), enableDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/enable'),
disableDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/disable'), disableDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/disable'),
startDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/start'), startDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/start'),
saveDbBackup: Api.newPut('/dbs/{dbId}/backups/{id}'), saveDbBackup: Api.newPut('/dbs/{dbId}/backups/{id}'),
getDbBackupHistories: Api.newGet('/dbs/{dbId}/backup-histories'), 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'), getDbRestores: Api.newGet('/dbs/{dbId}/restores'),
createDbRestore: Api.newPost('/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'), getDbNamesWithoutRestore: Api.newGet('/dbs/{dbId}/db-names-without-restore'),
enableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/enable'), enableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/enable'),
disableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/disable'), disableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/disable'),

View File

@@ -19,7 +19,7 @@ import { NodeType, TagTreeNode } from '@/views/ops/component/tag';
import { dbApi } from '@/views/ops/db/api'; import { dbApi } from '@/views/ops/db/api';
import { sleep } from '@/common/utils/loading'; import { sleep } from '@/common/utils/loading';
import SvgIcon from '@/components/svgIcon/index.vue'; 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 TagTreeResourceSelect from '../../component/TagTreeResourceSelect.vue';
import { computed } from 'vue'; import { computed } from 'vue';
@@ -33,9 +33,12 @@ const props = defineProps({
tagPath: { tagPath: {
type: String, 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层 */ /** mysql类型的数据库没有schema层 */
const mysqlType = (type: string) => { const noSchemaType = (type: string) => {
return type === DbType.mysql; return noSchemaTypes.includes(type);
}; };
// 数据库实例节点类型 // 数据库实例节点类型
@@ -96,7 +99,7 @@ const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((p
const params = parentNode.params; const params = parentNode.params;
const dbs = params.database.split(' ')?.sort(); const dbs = params.database.split(' ')?.sort();
let fn: NodeType; let fn: NodeType;
if (mysqlType(params.type)) { if (noSchemaType(params.type)) {
fn = MysqlNodeTypes; fn = MysqlNodeTypes;
} else { } else {
fn = PgNodeTypes; fn = PgNodeTypes;
@@ -114,7 +117,7 @@ const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((p
db: x, db: x,
}) })
.withIcon(DbIcon); .withIcon(DbIcon);
if (mysqlType(params.type)) { if (noSchemaType(params.type)) {
tagTreeNode.isLeaf = true; tagTreeNode.isLeaf = true;
} }
return tagTreeNode; return tagTreeNode;
@@ -150,6 +153,7 @@ const changeNode = (nodeData: TagTreeNode) => {
emits('update:dbName', params.db); emits('update:dbName', params.db);
emits('update:dbId', params.id); emits('update:dbId', params.id);
emits('update:tagPath', params.tagPath); emits('update:tagPath', params.tagPath);
emits('update:dbType', params.type);
emits('selectDb', params); emits('selectDb', params);
}; };
</script> </script>

View File

@@ -128,12 +128,12 @@
</template> </template>
<script lang="ts" setup> <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 { getToken } from '@/common/utils/storage';
import { notBlank } from '@/common/assert'; import { notBlank } from '@/common/assert';
import { format as sqlFormatter } from 'sql-formatter'; import { format as sqlFormatter } from 'sql-formatter';
import config from '@/common/config'; 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 * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import { editor } from 'monaco-editor'; import { editor } from 'monaco-editor';
@@ -146,11 +146,10 @@ import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { joinClientParams } from '@/common/request'; import { joinClientParams } from '@/common/request';
import { buildProgressProps } from '@/components/progress-notify/progress-notify'; import { buildProgressProps } from '@/components/progress-notify/progress-notify';
import ProgressNotify from '@/components/progress-notify/progress-notify.vue'; import ProgressNotify from '@/components/progress-notify/progress-notify.vue';
import { ElNotification } from 'element-plus';
import syssocket from '@/common/syssocket'; import syssocket from '@/common/syssocket';
import SvgIcon from '@/components/svgIcon/index.vue'; import SvgIcon from '@/components/svgIcon/index.vue';
import { getDbDialect } from '../../dialect'; import { getDbDialect } from '../../dialect';
import { Splitpanes, Pane } from 'splitpanes'; import { Pane, Splitpanes } from 'splitpanes';
const emits = defineEmits(['saveSqlSuccess']); const emits = defineEmits(['saveSqlSuccess']);
@@ -357,6 +356,7 @@ const onRunSql = async (newTab = false) => {
const colAndData: any = data.value; const colAndData: any = data.value;
if (!colAndData.res || colAndData.res.length === 0) { if (!colAndData.res || colAndData.res.length === 0) {
ElMessage.warning('未查询到结果集'); ElMessage.warning('未查询到结果集');
return;
} }
// 要实时响应,故需要用索引改变数据才生效 // 要实时响应,故需要用索引改变数据才生效

View File

@@ -3,6 +3,7 @@
<el-input <el-input
v-if="dataType == DataType.String" v-if="dataType == DataType.String"
:ref="(el: any) => focus && el?.focus()" :ref="(el: any) => focus && el?.focus()"
:disabled="disabled"
@blur="handleBlur" @blur="handleBlur"
:class="`w100 mb4 ${showEditorIcon ? 'string-input-container-show-icon' : ''}`" :class="`w100 mb4 ${showEditorIcon ? 'string-input-container-show-icon' : ''}`"
input-style="text-align: center; height: 26px;" input-style="text-align: center; height: 26px;"
@@ -16,6 +17,7 @@
<el-input <el-input
v-else-if="dataType == DataType.Number" v-else-if="dataType == DataType.Number"
:ref="(el: any) => focus && el?.focus()" :ref="(el: any) => focus && el?.focus()"
:disabled="disabled"
@blur="handleBlur" @blur="handleBlur"
class="w100 mb4" class="w100 mb4"
input-style="text-align: center; height: 26px;" input-style="text-align: center; height: 26px;"
@@ -28,6 +30,7 @@
<el-date-picker <el-date-picker
v-else-if="dataType == DataType.Date" v-else-if="dataType == DataType.Date"
:ref="(el: any) => focus && el?.focus()" :ref="(el: any) => focus && el?.focus()"
:disabled="disabled"
@change="emit('blur')" @change="emit('blur')"
@blur="handleBlur" @blur="handleBlur"
class="edit-time-picker mb4" class="edit-time-picker mb4"
@@ -43,6 +46,7 @@
<el-date-picker <el-date-picker
v-else-if="dataType == DataType.DateTime" v-else-if="dataType == DataType.DateTime"
:ref="(el: any) => focus && el?.focus()" :ref="(el: any) => focus && el?.focus()"
:disabled="disabled"
@change="handleBlur" @change="handleBlur"
@blur="handleBlur" @blur="handleBlur"
class="edit-time-picker mb4" class="edit-time-picker mb4"
@@ -58,6 +62,7 @@
<el-time-picker <el-time-picker
v-else-if="dataType == DataType.Time" v-else-if="dataType == DataType.Time"
:ref="(el: any) => focus && el?.focus()" :ref="(el: any) => focus && el?.focus()"
:disabled="disabled"
@change="handleBlur" @change="handleBlur"
@blur="handleBlur" @blur="handleBlur"
class="edit-time-picker mb4" class="edit-time-picker mb4"
@@ -71,7 +76,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { Ref, ref, computed } from 'vue'; import { computed, ref, Ref } from 'vue';
import { ElInput } from 'element-plus'; import { ElInput } from 'element-plus';
import { DataType } from '../../dialect/index'; import { DataType } from '../../dialect/index';
import SvgIcon from '@/components/svgIcon/index.vue'; import SvgIcon from '@/components/svgIcon/index.vue';
@@ -83,11 +88,13 @@ export interface ColumnFormItemProps {
focus?: boolean; // 是否获取焦点 focus?: boolean; // 是否获取焦点
placeholder?: string; placeholder?: string;
columnName?: string; columnName?: string;
disabled?: boolean;
} }
const props = withDefaults(defineProps<ColumnFormItemProps>(), { const props = withDefaults(defineProps<ColumnFormItemProps>(), {
focus: false, focus: false,
dataType: DataType.String, dataType: DataType.String,
disabled: false,
}); });
const emit = defineEmits(['update:modelValue', 'blur']); const emit = defineEmits(['update:modelValue', 'blur']);

View File

@@ -46,14 +46,6 @@
<b :title="column.remark" class="el-text" style="cursor: pointer"> <b :title="column.remark" class="el-text" style="cursor: pointer">
{{ column.title }} {{ column.title }}
</b> </b>
<span>
<SvgIcon
color="var(--el-color-primary)"
v-if="column.title == nowSortColumn?.columnName"
:name="nowSortColumn?.order == 'asc' ? 'top' : 'bottom'"
></SvgIcon>
</span>
</div> </div>
<!-- 字段备注信息 --> <!-- 字段备注信息 -->
@@ -71,6 +63,13 @@
{{ column.title }} {{ column.title }}
</b> </b>
</div> </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> </div>
</div> </div>
@@ -715,9 +714,13 @@ const submitUpdateFields = async () => {
const db = state.db; const db = state.db;
let res = ''; let res = '';
const dbDialect = getDbDialect(dbInst.type); 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()) { 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 rowData = updateRow.rowData;
// 主键列信息 // 主键列信息
const primaryKey = await dbInst.loadTableColumn(db, state.table); const primaryKey = await dbInst.loadTableColumn(db, state.table);
@@ -868,9 +871,15 @@ defineExpose({
color: var(--el-color-info-light-3); color: var(--el-color-info-light-3);
font-weight: bold; font-weight: bold;
position: absolute; position: absolute;
top: -7px; top: -5px;
padding: 2px;
}
.column-right {
position: absolute;
top: 2px;
right: 0;
padding: 2px; padding: 2px;
height: 12px;
} }
} }
</style> </style>

View File

@@ -158,21 +158,52 @@
@data-delete="onRefresh" @data-delete="onRefresh"
></db-table-data> ></db-table-data>
<el-row type="flex" class="mt5" justify="center"> <el-row type="flex" class="mt5" :gutter="10" justify="space-between" style="user-select: none">
<el-pagination <el-col :span="12">
small <el-text
:total="count" id="copyValue"
@size-change="handleSizeChange" style="color: var(--el-color-info-light-3)"
@current-change="pageChange()" class="is-truncated font12 mt5"
layout="prev, pager, next, total, sizes, jumper" @click="copyToClipboard(sql)"
v-model:current-page="pageNum" :title="sql"
v-model:page-size="pageSize" >{{ sql }}</el-text
:page-sizes="pageSizes" >
></el-pagination> </el-col>
</el-row> <el-col :span="12">
<div style="font-size: 12px; padding: 0 10px; color: #606266"> <el-row :gutter="10" justify="left">
<span>{{ state.sql }}</span> <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> </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>
<el-dialog v-model="conditionDialog.visible" :title="conditionDialog.title" width="420px"> <el-dialog v-model="conditionDialog.visible" :title="conditionDialog.title" width="420px">
<el-row> <el-row>
@@ -211,13 +242,14 @@
class="w100 mb5" class="w100 mb5"
:prop="column.columnName" :prop="column.columnName"
:label="column.columnName" :label="column.columnName"
:required="column.nullable != 'YES' && column.columnKey != 'PRI'" :required="column.nullable != 'YES' && !column.isPrimaryKey && !column.isIdentity"
> >
<ColumnFormItem <ColumnFormItem
v-model="addDataDialog.data[`${column.columnName}`]" v-model="addDataDialog.data[`${column.columnName}`]"
:data-type="dbDialect.getDataType(column.columnType)" :data-type="dbDialect.getDataType(column.columnType)"
:placeholder="`${column.columnType} ${column.columnComment}`" :placeholder="`${column.columnType} ${column.columnComment}`"
:column-name="column.columnName" :column-name="column.columnName"
:disabled="column.isIdentity"
/> />
</el-form-item> </el-form-item>
</el-form> </el-form>
@@ -241,6 +273,7 @@ import { DbDialect, getDbDialect } from '@/views/ops/db/dialect';
import SvgIcon from '@/components/svgIcon/index.vue'; import SvgIcon from '@/components/svgIcon/index.vue';
import ColumnFormItem from './ColumnFormItem.vue'; import ColumnFormItem from './ColumnFormItem.vue';
import { useEventListener, useStorage } from '@vueuse/core'; import { useEventListener, useStorage } from '@vueuse/core';
import { copyToClipboard } from '@/common/utils/string';
const props = defineProps({ const props = defineProps({
dbId: { dbId: {
@@ -289,7 +322,10 @@ const state = reactive({
defaultPageSize * 40, defaultPageSize * 40,
defaultPageSize * 80, defaultPageSize * 80,
], ],
count: 0, setPageNum: 0,
total: 0,
showTotal: false,
counting: false,
selectionDatas: [] as any, selectionDatas: [] as any,
condPopVisible: false, condPopVisible: false,
columnNameSearch: '', columnNameSearch: '',
@@ -313,7 +349,7 @@ const state = reactive({
dbDialect: {} as DbDialect, 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( watch(
() => props.tableHeight, () => props.tableHeight,
@@ -346,18 +382,19 @@ const onRefresh = async () => {
await selectData(); await selectData();
}; };
/** watch(
* 数据tab修改页数 () => state.pageNum,
*/ async () => {
const pageChange = async () => {
await selectData(); await selectData();
}; }
);
/** /**
* 单表数据信息查询数据 * 单表数据信息查询数据
*/ */
const selectData = async () => { const selectData = async () => {
state.loading = true; state.loading = true;
state.setPageNum = state.pageNum;
const dbInst = getNowDbInst(); const dbInst = getNowDbInst();
const db = props.dbName; const db = props.dbName;
const table = props.tableName; const table = props.tableName;
@@ -370,16 +407,10 @@ const selectData = async () => {
state.columns = columns; state.columns = columns;
} }
const countRes = await dbInst.runSql(db, dbInst.getDefaultCountSql(table, state.condition)); let sql = dbInst.getDefaultSelectSql(db, table, state.condition, state.orderBy, state.pageNum, state.pageSize);
state.count = countRes.res[0].count || countRes.res[0].COUNT || 0;
let sql = dbInst.getDefaultSelectSql(table, state.condition, state.orderBy, state.pageNum, state.pageSize);
state.sql = sql; state.sql = sql;
if (state.count > 0) {
const colAndData: any = await dbInst.runSql(db, sql); const colAndData: any = await dbInst.runSql(db, sql);
state.datas = colAndData.res; state.datas = colAndData.res;
} else {
state.datas = [];
}
} finally { } finally {
state.loading = false; state.loading = false;
} }
@@ -391,6 +422,33 @@ const handleSizeChange = async (size: any) => {
await selectData(); 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 = ''; let completeCond = '';
// 是否存在列建议 // 是否存在列建议
@@ -566,7 +624,13 @@ const addRow = async () => {
} }
let columnNames = Object.keys(obj).join(','); let columnNames = Object.keys(obj).join(',');
let values = Object.values(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, () => { dbInst.promptExeSql(props.dbName, sql, null, () => {
closeAddDataDialog(); closeAddDataDialog();
onRefresh(); onRefresh();
@@ -579,4 +643,8 @@ const addRow = async () => {
}; };
</script> </script>
<style lang="scss"></style> <style lang="scss">
.op-page {
margin-left: 5px;
}
</style>

View File

@@ -1,6 +1,6 @@
<template> <template>
<div> <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-form label-position="left" ref="formRef" :model="tableData" label-width="80px">
<el-row> <el-row>
<el-col :span="12"> <el-col :span="12">
@@ -26,7 +26,7 @@
:width="item.width" :width="item.width"
> >
<template #default="scope"> <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-select v-else-if="item.prop === 'type'" filterable size="small" v-model="scope.row.type">
<el-option <el-option
@@ -42,35 +42,30 @@
</el-option> </el-option>
</el-select> </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 <el-checkbox
v-else-if="item.prop === 'auto_increment'" v-else-if="item.prop === 'auto_increment'"
size="small" size="small"
v-model="scope.row.auto_increment" v-model="scope.row.auto_increment"
:disabled="dbType === DbType.postgresql" :disabled="disableEditIncr()"
> />
</el-checkbox>
<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 <el-popconfirm v-else-if="item.prop === 'action'" title="确定删除?" @confirm="deleteRow(scope.$index)">
v-else-if="item.prop === 'action'" <template #reference>
type="danger" <el-link type="danger" plain size="small" :underline="false">删除</el-link>
plain </template>
size="small" </el-popconfirm>
:underline="false"
@click.prevent="deleteRow(scope.$index)"
>删除</el-link
>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </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 v-if="item.prop === 'unique'" size="small" v-model="scope.row.unique" @change="indexChanges(scope.row)">
</el-checkbox> </el-checkbox>
<el-select v-if="item.prop === 'indexType'" disabled size="small" v-model="scope.row.indexType"> <el-input 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 === 'indexComment'" size="small" v-model="scope.row.indexComment"> </el-input> <el-input v-if="item.prop === 'indexComment'" size="small" v-model="scope.row.indexComment"> </el-input>
<el-link <el-popconfirm v-else-if="item.prop === 'action'" title="确定删除?" @confirm="deleteIndex(scope.$index)">
v-if="item.prop === 'action'" <template #reference>
type="danger" <el-link type="danger" plain size="small" :underline="false">删除</el-link>
plain </template>
size="small" </el-popconfirm>
:underline="false"
@click.prevent="deleteIndex(scope.$index)"
>删除</el-link
>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -130,6 +119,7 @@
</el-tabs> </el-tabs>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="cancel()">取消</el-button>
<el-button :loading="btnloading" @click="submit()" type="primary">保存</el-button> <el-button :loading="btnloading" @click="submit()" type="primary">保存</el-button>
</template> </template>
</el-dialog> </el-dialog>
@@ -166,7 +156,7 @@ const props = defineProps({
//定义事件 //定义事件
const emit = defineEmits(['update:visible', 'cancel', 'val-change', 'submit-sql']); const emit = defineEmits(['update:visible', 'cancel', 'val-change', 'submit-sql']);
const dbDialect = getDbDialect(props.dbType); let dbDialect = getDbDialect(props.dbType);
type ColName = { type ColName = {
prop: string; prop: string;
@@ -180,29 +170,33 @@ const state = reactive({
btnloading: false, btnloading: false,
activeName: '1', activeName: '1',
columnTypeList: dbDialect.getInfo().columnTypes, columnTypeList: dbDialect.getInfo().columnTypes,
indexTypeList: ['BTREE', 'NORMAL'], // mysql索引类型详解 http://c.biancheng.net/view/7897.html
tableData: { tableData: {
fields: { fields: {
colNames: [ colNames: [
{ {
prop: 'name', prop: 'name',
label: '字段名称', label: '字段名称',
width: 200,
}, },
{ {
prop: 'type', prop: 'type',
label: '字段类型', label: '字段类型',
width: 120,
}, },
{ {
prop: 'length', prop: 'length',
label: '长度', label: '长度',
width: 120,
}, },
{ {
prop: 'numScale', prop: 'numScale',
label: '小数点', label: '小数点',
width: 120,
}, },
{ {
prop: 'value', prop: 'value',
label: '默认值', label: '默认值',
width: 120,
}, },
{ {
@@ -231,6 +225,7 @@ const state = reactive({
}, },
] as ColName[], ] as ColName[],
res: [] as RowDefinition[], res: [] as RowDefinition[],
oldFields: [] as RowDefinition[],
}, },
indexs: { indexs: {
colNames: [ colNames: [
@@ -261,17 +256,20 @@ const state = reactive({
], ],
columns: [{ name: '', remark: '' }], columns: [{ name: '', remark: '' }],
res: [] as IndexDefinition[], res: [] as IndexDefinition[],
oldIndexs: [] as IndexDefinition[],
}, },
tableName: '', tableName: '',
tableComment: '', tableComment: '',
height: 450, height: 450,
db: '',
}, },
}); });
const { dialogVisible, btnloading, activeName, indexTypeList, tableData } = toRefs(state); const { dialogVisible, btnloading, activeName, tableData } = toRefs(state);
watch(props, async (newValue) => { watch(props, async (newValue) => {
state.dialogVisible = newValue.visible; state.dialogVisible = newValue.visible;
dbDialect = getDbDialect(newValue.dbType);
}); });
const cancel = () => { const cancel = () => {
@@ -359,7 +357,10 @@ const filterChangedData = (oldArr: object[], nowArr: object[], key: string): { d
nowArr.forEach((a) => { nowArr.forEach((a) => {
let k = a[key]; let k = a[key];
newMap[k] = a; 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); data.add.push(a);
} }
@@ -376,7 +377,7 @@ const filterChangedData = (oldArr: object[], nowArr: object[], key: string): { d
for (let f in a) { for (let f in a) {
let oldV = a[f]; let oldV = a[f];
let newV = newData[f]; let newV = newData[f];
if (oldV.toString() !== newV.toString()) { if (oldV?.toString() !== newV?.toString()) {
data.upd.push(newData); data.upd.push(newData);
break; break;
} }
@@ -399,12 +400,12 @@ const genSql = () => {
// 修改 // 修改
if (state.activeName === '1') { if (state.activeName === '1') {
// 修改列 // 修改列
let changeData = filterChangedData(oldData.fields, state.tableData.fields.res, 'name'); let changeData = filterChangedData(state.tableData.fields.oldFields, state.tableData.fields.res, 'name');
return dbDialect.getModifyColumnSql(data.tableName, changeData); return dbDialect.getModifyColumnSql(data, data.tableName, changeData);
} else if (state.activeName === '2') { } else if (state.activeName === '2') {
// 修改索引 // 修改索引
let changeData = filterChangedData(oldData.indexs, state.tableData.indexs.res, 'indexName'); let changeData = filterChangedData(state.tableData.indexs.oldIndexs, state.tableData.indexs.res, 'indexName');
return dbDialect.getModifyIndexSql(data.tableName, changeData); return dbDialect.getModifyIndexSql(data, data.tableName, changeData);
} }
} }
}; };
@@ -414,28 +415,8 @@ const reset = () => {
formRef.value.resetFields(); formRef.value.resetFields();
state.tableData.tableName = ''; state.tableData.tableName = '';
state.tableData.tableComment = ''; state.tableData.tableComment = '';
state.tableData.fields.res = [ state.tableData.fields.res = [];
{ state.tableData.indexs.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: '',
},
];
}; };
const indexChanges = (row: any) => { const indexChanges = (row: any) => {
@@ -456,7 +437,21 @@ const indexChanges = (row: any) => {
row.indexComment = `${tableData.value.tableName}表(${name.replaceAll('_', ',')})${commentSuffix}`; 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( watch(
() => props.data, () => props.data,
(newValue: any) => { (newValue: any) => {
@@ -464,9 +459,10 @@ watch(
// 回显表名表注释 // 回显表名表注释
state.tableData.tableName = row.tableName; state.tableData.tableName = row.tableName;
state.tableData.tableComment = row.tableComment; state.tableData.tableComment = row.tableComment;
state.tableData.db = props.db!;
// 回显列 // 回显列
if (columns && Array.isArray(columns) && columns.length > 0) { if (columns && Array.isArray(columns) && columns.length > 0) {
oldData.fields = []; state.tableData.fields.oldFields = [];
state.tableData.fields.res = []; state.tableData.fields.res = [];
// 索引列下拉选 // 索引列下拉选
state.tableData.indexs.columns = []; state.tableData.indexs.columns = [];
@@ -474,26 +470,33 @@ watch(
let typeObj = a.columnType.replace(')', '').split('('); let typeObj = a.columnType.replace(')', '').split('(');
let type = typeObj[0]; let type = typeObj[0];
let length = (typeObj.length > 1 && typeObj[1]) || ''; 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 = { let data = {
name: a.columnName, name: a.columnName,
oldName: a.columnName,
type, type,
value: a.columnDefault || '', value: defaultValue,
length, length,
numScale: a.numScale, numScale: a.numScale,
notNull: a.nullable !== 'YES', notNull: a.nullable !== 'YES',
pri: a.columnKey === 'PRI', pri: a.isPrimaryKey,
auto_increment: a.columnKey === 'PRI' /*a.extra?.indexOf('auto_increment') > -1*/, auto_increment: a.isIdentity /*a.extra?.indexOf('auto_increment') > -1*/,
remark: a.columnComment, remark: a.columnComment,
}; };
state.tableData.fields.res.push(data); 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 }); state.tableData.indexs.columns.push({ name: a.columnName, remark: a.columnComment });
}); });
} }
// 回显索引 // 回显索引
if (indexs && Array.isArray(indexs) && indexs.length > 0) { if (indexs && Array.isArray(indexs) && indexs.length > 0) {
oldData.indexs = []; state.tableData.indexs.oldIndexs = [];
state.tableData.indexs.res = []; state.tableData.indexs.res = [];
// 索引过滤掉主键 // 索引过滤掉主键
indexs indexs
@@ -502,12 +505,12 @@ watch(
let data = { let data = {
indexName: a.indexName, indexName: a.indexName,
columnNames: a.columnName?.split(','), columnNames: a.columnName?.split(','),
unique: a.nonUnique === 0 || false, unique: a.isUnique || false,
indexType: a.indexType, indexType: a.indexType,
indexComment: a.indexComment, indexComment: a.indexComment,
}; };
state.tableData.indexs.res.push(data); state.tableData.indexs.res.push(data);
oldData.indexs.push(JSON.parse(JSON.stringify(data))); state.tableData.indexs.oldIndexs.push(JSON.parse(JSON.stringify(data)));
}); });
} }
} }

View File

@@ -68,9 +68,7 @@
<template #default="scope"> <template #default="scope">
<el-link @click.prevent="showColumns(scope.row)" type="primary">字段</el-link> <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" @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 class="ml5" v-if="editDbTypes.indexOf(dbType) > -1" @click.prevent="openEditTable(scope.row)" type="warning">编辑表</el-link>
>编辑表</el-link
>
<el-link class="ml5" @click.prevent="showCreateDdl(scope.row)" type="info">DDL</el-link> <el-link class="ml5" @click.prevent="showCreateDdl(scope.row)" type="info">DDL</el-link>
</template> </template>
</el-table-column> </el-table-column>
@@ -127,7 +125,7 @@ import SqlExecBox from '../sqleditor/SqlExecBox';
import config from '@/common/config'; import config from '@/common/config';
import { joinClientParams } from '@/common/request'; import { joinClientParams } from '@/common/request';
import { isTrue } from '@/common/assert'; import { isTrue } from '@/common/assert';
import { compatibleMysql, DbType } from '../../dialect/index'; import { compatibleMysql, DbType, editDbTypes } from '../../dialect/index';
const DbTableOp = defineAsyncComponent(() => import('./DbTableOp.vue')); const DbTableOp = defineAsyncComponent(() => import('./DbTableOp.vue'));
@@ -181,7 +179,6 @@ const state = reactive({
visible: false, visible: false,
activeName: '1', activeName: '1',
type: '', type: '',
enableEditTypes: [DbType.mysql, DbType.mariadb, DbType.postgresql, DbType.dm, DbType.oracle], // 支持"编辑表"的数据库类型
data: { data: {
// 修改表时,传递修改数据 // 修改表时,传递修改数据
edit: false, edit: false,

View File

@@ -7,6 +7,10 @@ import { editor, languages, Position } from 'monaco-editor';
import { registerCompletionItemProvider } from '@/components/monaco/completionItemProvider'; import { registerCompletionItemProvider } from '@/components/monaco/completionItemProvider';
import { DbDialect, EditorCompletionItem, getDbDialect } from './dialect'; 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(); const dbInstCache: Map<number, DbInst> = new Map();
@@ -58,14 +62,15 @@ export class DbInst {
if (!dbName) { if (!dbName) {
throw new Error('dbName不能为空'); throw new Error('dbName不能为空');
} }
let db = this.dbs.get(dbName); let key = `${this.id}_${dbName}`;
let db = this.dbs.get(key);
if (db) { if (db) {
return db; return db;
} }
console.info(`new db -> dbId: ${this.id}, dbName: ${dbName}`); console.info(`new db -> dbId: ${this.id}, dbName: ${dbName}`);
db = new Db(); db = new Db();
db.name = dbName; db.name = dbName;
this.dbs.set(dbName, db); this.dbs.set(key, db);
return db; return db;
} }
@@ -77,17 +82,22 @@ export class DbInst {
*/ */
async loadTables(dbName: string, reload?: boolean) { async loadTables(dbName: string, reload?: boolean) {
const db = this.getDb(dbName); const db = this.getDb(dbName);
// 优先从 table map中获取 let key = this.dbTablesKey(dbName);
let tables = db.tables; let tables = tableStorage.value.get(key);
// 优先从 table 缓存中获取
if (!reload && tables) { if (!reload && tables) {
db.tables = tables;
return tables; return tables;
} }
// 重置列信息缓存与表提示信息 // 重置列信息缓存与表提示信息
db.columnsMap?.clear(); db.columnsMap?.clear();
db.tableHints = null;
console.log(`load tables -> dbName: ${dbName}`); console.log(`load tables -> dbName: ${dbName}`);
tables = await dbApi.tableInfos.request({ id: this.id, db: dbName }); tables = await dbApi.tableInfos.request({ id: this.id, db: dbName });
tableStorage.value.set(key, tables);
db.tables = tables; db.tables = tables;
// 异步加载表提示信息
this.loadDbHints(dbName, true).then(() => {});
return tables; return tables;
} }
@@ -169,18 +179,30 @@ export class DbInst {
return this.getDb(dbName).getColumn(table, columnName); 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); const db = this.getDb(dbName);
if (db.tableHints) { let key = this.dbTableHintsKey(dbName);
return db.tableHints; let hints = hintsStorage.value.get(key);
if (!reload && hints) {
db.tableHints = hints;
return hints;
} }
console.log(`load db-hits -> dbName: ${dbName}`); console.log(`load db-hits -> dbName: ${dbName}`);
const hits = await dbApi.hintTables.request({ id: this.id, db: db.name }); hints = await dbApi.hintTables.request({ id: this.id, db: db.name });
db.tableHints = hits; db.tableHints = hints;
return hits; hintsStorage.value.set(key, hints);
return hints;
} }
/** /**
@@ -225,8 +247,8 @@ export class DbInst {
}; };
// 获取指定表的默认查询sql // 获取指定表的默认查询sql
getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number = DbInst.DefaultLimit) { getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number = DbInst.DefaultLimit) {
return getDbDialect(this.type).getDefaultSelectSql(table, condition, orderBy, pageNum, limit); return getDbDialect(this.type).getDefaultSelectSql(db, table, condition, orderBy, pageNum, limit);
} }
/** /**
@@ -275,6 +297,7 @@ export class DbInst {
sql, sql,
dbId: this.id, dbId: this.id,
db, db,
dbType: getDbDialect(this.type).getInfo().formatSqlDialect,
runSuccessCallback: successFunc, runSuccessCallback: successFunc,
cancelCallback: cancelFunc, cancelCallback: cancelFunc,
}); });
@@ -363,7 +386,7 @@ export class DbInst {
return value; return value;
} }
if (!dbDialect) { if (!dbDialect) {
return `${value}`; return `'${value}'`;
} }
return dbDialect.wrapStrValue(columnType, value); return dbDialect.wrapStrValue(columnType, value);
} }
@@ -441,7 +464,7 @@ class Db {
getColumn(table: string, columnName: string = '') { getColumn(table: string, columnName: string = '') {
const cols = this.getColumns(table); const cols = this.getColumns(table);
if (!columnName) { if (!columnName) {
const col = cols.find((c: any) => c.columnKey == 'PRI'); const col = cols.find((c: any) => c.isPrimaryKey);
return col || cols[0]; return col || cols[0];
} }
return cols.find((c: any) => c.columnName == columnName); return cols.find((c: any) => c.columnName == columnName);

View File

@@ -54,6 +54,7 @@ const DM_TYPE_LIST: sqlColumnType[] = [
{ udtName: 'BFILE', dataType: 'BFILE', desc: '二进制文件', space: '', range: '100G-1' }, { udtName: 'BFILE', dataType: 'BFILE', desc: '二进制文件', space: '', range: '100G-1' },
]; ];
// 参考官方文档https://eco.dameng.com/document/dm/zh-cn/pm/function.html
const replaceFunctions: EditorCompletionItem[] = [ const replaceFunctions: EditorCompletionItem[] = [
// 数值函数 // 数值函数
{ label: 'ABS', insertText: 'ABS(n)', description: '求数值 n 的绝对值' }, { label: 'ABS', insertText: 'ABS(n)', description: '求数值 n 的绝对值' },
@@ -365,21 +366,22 @@ class DMDialect implements DbDialect {
}; };
dmDialectInfo = { dmDialectInfo = {
name: 'DM',
icon: 'iconfont icon-db-dm', icon: 'iconfont icon-db-dm',
defaultPort: 5236, defaultPort: 5236,
formatSqlDialect: 'postgresql', formatSqlDialect: 'plsql',
columnTypes: DM_TYPE_LIST.sort((a, b) => a.udtName.localeCompare(b.udtName)), columnTypes: DM_TYPE_LIST.sort((a, b) => a.udtName.localeCompare(b.udtName)),
editorCompletions, editorCompletions,
}; };
return dmDialectInfo; 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)};`; return `SELECT * FROM "${table}" ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''} ${this.getPageSql(pageNum, limit)};`;
} }
getPageSql(pageNum: number, limit: number) { getPageSql(pageNum: number, limit: number) {
return ` OFFSET ${(pageNum - 1) * limit} LIMIT ${limit};`; return ` OFFSET ${(pageNum - 1) * limit} LIMIT ${limit}`;
} }
getDefaultRows(): RowDefinition[] { getDefaultRows(): RowDefinition[] {
@@ -500,7 +502,9 @@ class DMDialect implements DbDialect {
// 默认值 // 默认值
let defVal = this.getDefaultValueSql(cl); let defVal = this.getDefaultValueSql(cl);
let incr = cl.auto_increment ? 'IDENTITY' : ''; 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 { getCreateTableSql(data: any): string {
@@ -546,35 +550,78 @@ class DMDialect implements DbDialect {
return sql.join(';'); return sql.join(';');
} }
getModifyColumnSql(tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string { getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
let sql: 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) { if (changeData.add.length > 0) {
changeData.add.forEach((a) => { 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) { 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) { if (changeData.upd.length > 0) {
changeData.upd.forEach((a) => { changeData.upd.forEach((a) => {
sql.push(`ALTER TABLE "${tableName}" MODIFY ${this.genColumnBasicSql(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) { if (a.remark) {
sql.push(`comment on COLUMN "${tableName}"."${a.name}" is '${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) { if (changeData.del.length > 0) {
changeData.del.forEach((a) => { 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;`;
}
} }
getModifyIndexSql(tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string { let addPkSql = priArr.size > 0 ? `ALTER TABLE ${dbTable} ADD PRIMARY KEY (${Array.from(priArr).join(',')});` : '';
return dropPkSql + modifySql + dropSql + renameSql + addPkSql + commentSql;
}
getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
// 不能直接修改索引名或字段、需要先删后加 // 不能直接修改索引名或字段、需要先删后加
let dropIndexNames: string[] = []; let dropIndexNames: string[] = [];
let addIndexs: any[] = []; let addIndexs: any[] = [];

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

View File

@@ -3,6 +3,9 @@ import { PostgresqlDialect } from './postgres_dialect';
import { DMDialect } from '@/views/ops/db/dialect/dm_dialect'; import { DMDialect } from '@/views/ops/db/dialect/dm_dialect';
import { OracleDialect } from '@/views/ops/db/dialect/oracle_dialect'; import { OracleDialect } from '@/views/ops/db/dialect/oracle_dialect';
import { MariadbDialect } from '@/views/ops/db/dialect/mariadb_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 { export interface sqlColumnType {
udtName: string; udtName: string;
@@ -14,6 +17,7 @@ export interface sqlColumnType {
export interface RowDefinition { export interface RowDefinition {
name: string; name: string;
oldName?: string;
type: string; type: string;
value: string; value: string;
length: string; length: string;
@@ -78,6 +82,11 @@ export const ColumnTypeSubscript = {
// 数据库基础信息 // 数据库基础信息
export interface DialectInfo { export interface DialectInfo {
/**
* 数据库类型label
*/
name: string;
/** /**
* 图标 * 图标
*/ */
@@ -108,10 +117,21 @@ export const DbType = {
mysql: 'mysql', mysql: 'mysql',
mariadb: 'mariadb', mariadb: 'mariadb',
postgresql: 'postgres', postgresql: 'postgres',
gauss: 'gauss',
dm: 'dm', // 达梦 dm: 'dm', // 达梦
oracle: 'oracle', 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 => { export const compatibleMysql = (dbType: string): boolean => {
switch (dbType) { switch (dbType) {
case DbType.mysql: case DbType.mysql:
@@ -130,13 +150,14 @@ export interface DbDialect {
/** /**
* 获取默认查询sql * 获取默认查询sql
* @param db 数据库信息
* @param table 表名 * @param table 表名
* @param condition 条件 * @param condition 条件
* @param orderBy 排序 * @param orderBy 排序
* @param pageNum 页数 * @param pageNum 页数
* @param limit 条数 * @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; getPageSql(pageNum: number, limit: number): string;
@@ -164,47 +185,51 @@ export interface DbDialect {
/** /**
* 生成编辑列sql * 生成编辑列sql
* @param tableData 表数据,包含表名、列数据、索引数据
* @param tableName 表名 * @param tableName 表名
* @param changeData 改变信息 * @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 * 生成编辑索引sql
* @param tableData 表数据,包含表名、列数据、索引数据
* @param tableName 表名 * @param tableName 表名
* @param changeData 改变数据 * @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') */ /** 包装字符串数据, 如oracle需要把date类型改为 to_date(str, 'yyyy-mm-dd hh24:mi:ss') */
wrapStrValue(columnType: string, value: string): string; wrapStrValue(columnType: string, value: string): string;
} }
let mysqlDialect = new MysqlDialect(); 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 => { let dbType2DialectMap: Map<string, DbDialect> = new Map();
if (!dbType) {
return mysqlDialect; export const registerDbDialect = (dbType: string, dd: DbDialect) => {
} dbType2DialectMap.set(dbType, dd);
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('不支持的数据库');
}
}; };
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());
})();

View File

@@ -12,6 +12,7 @@ class MariadbDialect extends MysqlDialect implements DbDialect {
mariadbDialectInfo = {} as DialectInfo; mariadbDialectInfo = {} as DialectInfo;
Object.assign(mariadbDialectInfo, super.getInfo()); Object.assign(mariadbDialectInfo, super.getInfo());
mariadbDialectInfo.name = 'MariaDB';
mariadbDialectInfo.icon = 'iconfont icon-mariadb'; mariadbDialectInfo.icon = 'iconfont icon-mariadb';
return mariadbDialectInfo; return mariadbDialectInfo;
} }

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

View File

@@ -4,6 +4,7 @@ import { language as mysqlLanguage } from 'monaco-editor/esm/vs/basic-languages/
export { MYSQL_TYPE_LIST, MysqlDialect }; export { MYSQL_TYPE_LIST, MysqlDialect };
// 参考官方文档https://dev.mysql.com/doc/refman/8.0/en/data-types.html
const MYSQL_TYPE_LIST = [ const MYSQL_TYPE_LIST = [
'bigint', 'bigint',
'binary', 'binary',
@@ -31,6 +32,7 @@ const MYSQL_TYPE_LIST = [
'varchar', 'varchar',
]; ];
// 参考官方文档https://dev.mysql.com/doc/refman/8.3/en/functions.html
const replaceFunctions: EditorCompletionItem[] = [ const replaceFunctions: EditorCompletionItem[] = [
/** 字符串相关函数 */ /** 字符串相关函数 */
{ label: 'CONCAT', insertText: 'CONCAT(str1,str2,...)', description: '多字符串合并' }, { label: 'CONCAT', insertText: 'CONCAT(str1,str2,...)', description: '多字符串合并' },
@@ -102,6 +104,7 @@ class MysqlDialect implements DbDialect {
}; };
mysqlDialectInfo = { mysqlDialectInfo = {
name: 'MySQL',
icon: 'iconfont icon-op-mysql', icon: 'iconfont icon-op-mysql',
defaultPort: 3306, defaultPort: 3306,
formatSqlDialect: 'mysql', formatSqlDialect: 'mysql',
@@ -111,7 +114,7 @@ class MysqlDialect implements DbDialect {
return mysqlDialectInfo; 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( return `SELECT * FROM ${this.quoteIdentifier(table)} ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''} ${this.getPageSql(
pageNum, pageNum,
limit limit
@@ -193,7 +196,7 @@ class MysqlDialect implements DbDialect {
let defVal = val ? `DEFAULT ${val}` : ''; let defVal = val ? `DEFAULT ${val}` : '';
let length = cl.length ? `(${cl.length})` : ''; let length = cl.length ? `(${cl.length})` : '';
let onUpdate = 'update_time' === cl.name ? ' ON UPDATE CURRENT_TIMESTAMP ' : ''; 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' : '' cl.auto_increment ? 'AUTO_INCREMENT' : ''
} ${defVal} ${onUpdate} comment '${cl.remark || ''}' `; } ${defVal} ${onUpdate} comment '${cl.remark || ''}' `;
} }
@@ -223,38 +226,34 @@ class MysqlDialect implements DbDialect {
return sql.substring(0, sql.length - 1) + ';'; return sql.substring(0, sql.length - 1) + ';';
} }
getModifyColumnSql(tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string { getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
let addSql = '', let sql = `ALTER TABLE ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(tableName)}`;
updSql = '', let arr = [] as string[];
delSql = ''; if (changeData.del.length > 0) {
if (changeData.add.length > 0) { changeData.del.forEach((a) => {
addSql = `ALTER TABLE ${tableName}`; arr.push(` DROP COLUMN ${this.quoteIdentifier(a.name)} `);
changeData.add.forEach((a) => { });
addSql += ` ADD ${this.genColumnBasicSql(a)},`; }
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) { if (changeData.upd.length > 0) {
updSql = `ALTER TABLE ${tableName}`;
let arr = [] as string[];
changeData.upd.forEach((a) => { changeData.upd.forEach((a) => {
arr.push(` MODIFY ${this.genColumnBasicSql(a)}`); if (a.name === a.oldName) {
}); arr.push(` MODIFY COLUMN ${this.genColumnBasicSql(a)} `);
updSql += arr.join(','); } else {
updSql += ';'; arr.push(` CHANGE COLUMN ${this.quoteIdentifier(a.oldName!)} ${this.genColumnBasicSql(a)} `);
} }
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 // 搜集修改和删除的索引添加到drop index xx
// 收集新增和修改的索引添加到ADD xx // 收集新增和修改的索引添加到ADD xx
// ALTER TABLE `test1` // ALTER TABLE `test1`

View File

@@ -14,7 +14,7 @@ import { language as sqlLanguage } from 'monaco-editor/esm/vs/basic-languages/sq
export { OracleDialect, ORACLE_TYPE_LIST }; 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[] = [ const ORACLE_TYPE_LIST: sqlColumnType[] = [
// 字符数据类型 // 字符数据类型
{ udtName: 'CHAR', dataType: 'CHAR', desc: '定长字符串,自动在末尾用空格补全,非unicode', space: '', range: '1 - 2000' }, { 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: '' }, { udtName: 'BFILE', dataType: 'BFILE', desc: '二进制文件', space: '', range: '' },
]; ];
// 参考官方文档https://docs.oracle.com/cd/B19306_01/server.102/b14200/functions001.htm
const replaceFunctions: EditorCompletionItem[] = [ const replaceFunctions: EditorCompletionItem[] = [
// 字符函数 // 字符函数
{ label: 'ASCII', insertText: 'ASCII(x)', description: '返回字符X的ASCII码' }, { 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' }, { 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; let oracleDialectInfo: DialectInfo;
class OracleDialect implements DbDialect { class OracleDialect implements DbDialect {
@@ -103,6 +132,7 @@ class OracleDialect implements DbDialect {
let { keywords, operators, builtinVariables } = sqlLanguage; let { keywords, operators, builtinVariables } = sqlLanguage;
let functionNames = replaceFunctions.map((a) => a.label); let functionNames = replaceFunctions.map((a) => a.label);
let excludeKeywords = new Set(functionNames.concat(operators)); let excludeKeywords = new Set(functionNames.concat(operators));
excludeKeywords.add('SELECT');
let editorCompletions: EditorCompletion = { let editorCompletions: EditorCompletion = {
keywords: keywords keywords: keywords
@@ -117,21 +147,14 @@ class OracleDialect implements DbDialect {
}) })
) )
) )
.concat( .concat(addCustomKeywords),
// 加上自定义的关键字
addCustomKeywords.map(
(a): EditorCompletionItem => ({
label: a,
description: 'keyword',
})
)
),
operators: operators.map((a: string): EditorCompletionItem => ({ label: a, description: 'operator' })), operators: operators.map((a: string): EditorCompletionItem => ({ label: a, description: 'operator' })),
functions: replaceFunctions, functions: replaceFunctions,
variables: builtinVariables.map((a: string): EditorCompletionItem => ({ label: a, description: 'var' })), variables: builtinVariables.map((a: string): EditorCompletionItem => ({ label: a, description: 'var' })),
}; };
oracleDialectInfo = { oracleDialectInfo = {
name: 'Oracle',
icon: 'iconfont icon-oracle', icon: 'iconfont icon-oracle',
defaultPort: 1521, defaultPort: 1521,
formatSqlDialect: 'plsql', formatSqlDialect: 'plsql',
@@ -141,7 +164,7 @@ class OracleDialect implements DbDialect {
return oracleDialectInfo; 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 ` return `
SELECT * SELECT *
FROM ( FROM (
@@ -268,16 +291,22 @@ class OracleDialect implements DbDialect {
return ''; return '';
} }
genColumnBasicSql(cl: RowDefinition): string { genColumnBasicSql(cl: RowDefinition, create: boolean): string {
let length = this.getTypeLengthSql(cl); let length = this.getTypeLengthSql(cl);
// 默认值 // 默认值
let defVal = this.getDefaultValueSql(cl); let defVal = this.getDefaultValueSql(cl);
let incr = cl.auto_increment ? 'generated by default as IDENTITY' : ''; let incr = cl.auto_increment && create ? '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 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 { 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 createSql = '';
let tableCommentSql = ''; let tableCommentSql = '';
let columCommentSql = ''; let columCommentSql = '';
@@ -285,17 +314,17 @@ class OracleDialect implements DbDialect {
// 创建表结构 // 创建表结构
let fields: string[] = []; let fields: string[] = [];
data.fields.res.forEach((item: any) => { data.fields.res.forEach((item: any) => {
item.name && fields.push(this.genColumnBasicSql(item)); item.name && fields.push(this.genColumnBasicSql(item, true));
// 列注释 // 列注释
if (item.remark) { 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) { 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; return createSql + tableCommentSql + columCommentSql;
@@ -304,43 +333,95 @@ class OracleDialect implements DbDialect {
getCreateIndexSql(tableData: any): string { getCreateIndexSql(tableData: any): string {
// CREATE UNIQUE INDEX idx_column_name ON your_table (column1, column2); // CREATE UNIQUE INDEX idx_column_name ON your_table (column1, column2);
// COMMENT ON INDEX idx_column_name IS 'Your index comment here'; // 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[] = []; let sql: string[] = [];
tableData.indexs.res.forEach((a: any) => { 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(';'); return sql.join(';');
} }
getModifyColumnSql(tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string { getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
let sql: string[] = []; let schemaArr = tableData.db.split('/');
if (changeData.add.length > 0) { let schema = schemaArr.length > 1 ? schemaArr[schemaArr.length - 1] : schemaArr[0];
changeData.add.forEach((a) => { let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableName)}`;
sql.push(`ALTER TABLE "${tableName}" add COLUMN ${this.genColumnBasicSql(a)}`);
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) { if (a.remark) {
sql.push(`comment on COLUMN "${tableName}"."${a.name}" is '${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) { if (changeData.add.length > 0) {
changeData.upd.forEach((a) => { changeData.add.forEach((a) => {
sql.push(`ALTER TABLE "${tableName}" MODIFY ${this.genColumnBasicSql(a)}`); modifyArr.push(` ADD (${this.genColumnBasicSql(a, false)})`);
if (a.remark) { 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) { if (changeData.del.length > 0) {
changeData.del.forEach((a) => { 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;`;
}
} }
getModifyIndexSql(tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string { 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(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
// 不能直接修改索引名或字段、需要先删后加 // 不能直接修改索引名或字段、需要先删后加
let dropIndexNames: string[] = []; let dropIndexNames: string[] = [];
let addIndexs: any[] = []; let addIndexs: any[] = [];

View File

@@ -123,6 +123,7 @@ class PostgresqlDialect implements DbDialect {
}; };
pgDialectInfo = { pgDialectInfo = {
name: 'PostgreSQL',
icon: 'iconfont icon-op-postgres', icon: 'iconfont icon-op-postgres',
defaultPort: 5432, defaultPort: 5432,
formatSqlDialect: 'postgresql', formatSqlDialect: 'postgresql',
@@ -132,7 +133,7 @@ class PostgresqlDialect implements DbDialect {
return pgDialectInfo; 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( return `SELECT * FROM ${this.quoteIdentifier(table)} ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''} ${this.getPageSql(
pageNum, pageNum,
limit limit
@@ -228,7 +229,7 @@ class PostgresqlDialect implements DbDialect {
let marks = false; let marks = false;
if (this.matchType(cl.type, ['char', 'time', 'date', 'text'])) { if (this.matchType(cl.type, ['char', 'time', 'date', 'text'])) {
// 默认值是now()的time或date不需要加引号 // 默认值是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; marks = false;
} else { } else {
marks = true; marks = true;
@@ -260,7 +261,10 @@ class PostgresqlDialect implements DbDialect {
let length = this.getTypeLengthSql(cl); let length = this.getTypeLengthSql(cl);
// 默认值 // 默认值
let defVal = this.getDefaultValueSql(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 { getCreateTableSql(data: any): string {
@@ -301,7 +305,7 @@ class PostgresqlDialect implements DbDialect {
// 创建索引 // 创建索引
let sql: string[] = []; let sql: string[] = [];
tableData.indexs.res.forEach((a: any) => { 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) { if (a.indexComment) {
sql.push(`COMMENT ON INDEX ${a.indexName} IS '${a.indexComment}'`); sql.push(`COMMENT ON INDEX ${a.indexName} IS '${a.indexComment}'`);
} }
@@ -309,42 +313,60 @@ class PostgresqlDialect implements DbDialect {
return sql.join(';'); return sql.join(';');
} }
getModifyColumnSql(tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string { getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
let sql: 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) { if (changeData.add.length > 0) {
changeData.add.forEach((a) => { changeData.add.forEach((a) => {
let typeLength = this.getTypeLengthSql(a); modifySql += `alter table ${dbTable} add ${this.genColumnBasicSql(a)};`;
let defaultSql = this.getDefaultValueSql(a);
sql.push(`ALTER TABLE ${tableName} add ${a.name} ${a.type}${typeLength} ${defaultSql}`);
if (a.remark) { 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) { if (changeData.upd.length > 0) {
changeData.upd.forEach((a) => { 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); 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); let defaultSql = this.getDefaultValueSql(a);
if (defaultSql) { if (defaultSql) {
sql.push(`alter table ${tableName} alter column ${a.name} set ${defaultSql}`); modifySql += `alter table ${dbTable} alter column ${this.quoteIdentifier(name)} set ${defaultSql} ;`;
}
if (a.remark) {
sql.push(`comment on column "${tableName}"."${a.name}" is '${a.remark}'`);
} }
}); });
} }
if (changeData.del.length > 0) { if (changeData.del.length > 0) {
changeData.del.forEach((a) => { 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 dropIndexNames: string[] = [];
let addIndexs: any[] = []; let addIndexs: any[] = [];

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

View File

@@ -41,7 +41,15 @@
<el-input v-model="state.keySeparator" placeholder="分割符" size="small" class="ml5" /> <el-input v-model="state.keySeparator" placeholder="分割符" size="small" class="ml5" />
</el-col> </el-col>
<el-col :span="18"> <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>
<el-col :span="4"> <el-col :span="4">
<el-button <el-button

View File

@@ -107,10 +107,10 @@ defineExpose({ getContent });
.format-viewer-container .el-textarea textarea { .format-viewer-container .el-textarea textarea {
font-size: 14px; font-size: 14px;
height: calc(100vh - 546px + v-bind(height)); height: calc(100vh - 550px + v-bind(height));
} }
.format-viewer-container .monaco-editor-content { .format-viewer-container .monaco-editor-content {
height: calc(100vh - 560px + v-bind(height)) !important; height: calc(100vh - 565px + v-bind(height)) !important;
} }
</style> </style>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<el-button @click="showEditDialog(null)" icon="plus" size="small" plain type="primary" class="mb10">添加新行</el-button> <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 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="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> <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" class="key-detail-filter-value"
v-model="state.filterValue" v-model="state.filterValue"
@keyup.enter="hscan(true, true)" @keyup.enter="hscan(true, true)"
placeholder="输入关键词回车搜索" placeholder="关键词回车搜索"
clearable clearable
size="small" size="small"
/> />
@@ -51,7 +51,7 @@
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted, reactive, watch, toRefs } from 'vue'; import { ref, onMounted, reactive, toRefs } from 'vue';
import { redisApi } from './api'; import { redisApi } from './api';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { notBlank } from '@/common/assert'; import { notBlank } from '@/common/assert';

View File

@@ -142,7 +142,7 @@ const search = async () => {
const changeStatus = async (row: any) => { const changeStatus = async (row: any) => {
let id = row.id; 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({ await accountApi.changeStatus.request({
id, id,
status, status,

View File

@@ -2,6 +2,7 @@ import vue from '@vitejs/plugin-vue';
import { resolve } from 'path'; import { resolve } from 'path';
import type { UserConfig } from 'vite'; import type { UserConfig } from 'vite';
import { loadEnv } from './src/common/utils/viteBuild'; import { loadEnv } from './src/common/utils/viteBuild';
import { CodeInspectorPlugin } from 'code-inspector-plugin';
const pathResolve = (dir: string): any => { const pathResolve = (dir: string): any => {
return resolve(__dirname, '.', dir); return resolve(__dirname, '.', dir);
@@ -14,7 +15,12 @@ const alias: Record<string, string> = {
}; };
const viteConfig: UserConfig = { const viteConfig: UserConfig = {
plugins: [vue()], plugins: [
vue(),
CodeInspectorPlugin({
bundler: 'vite',
}),
],
root: process.cwd(), root: process.cwd(),
resolve: { resolve: {
alias, alias,

View File

@@ -43,16 +43,13 @@ log:
type: text type: text
# 是否记录方法调用栈信息 # 是否记录方法调用栈信息
add-source: false add-source: false
# 日志文件配置
# file: # file:
# path: ./ # path: ./log
# name: mayfly-go.log # name: mayfly-go.log
db: # # 日志文件的最大大小(以兆字节为单位)。当日志文件大小达到该值时,将触发切割操作
backup-path: ./backup # max-size: 500
mysqlutil-path: # # 根据文件名中的时间戳,设置保留旧日志文件的最大天数
mysql: ./mysqlutil/bin/mysql # max-age: 60
mysqldump: ./mysqlutil/bin/mysqldump # # 是否使用 gzip 压缩方式压缩轮转后的日志文件
mysqlbinlog: ./mysqlutil/bin/mysqlbinlog # compress: true
mariadbutil-path:
mysql: ./mariadbutil/bin/mariadb
mysqldump: ./mariadbutil/bin/mariadb-dump
mysqlbinlog: ./mariadbutil/bin/mariadb-binlog

View File

@@ -10,31 +10,35 @@ require (
github.com/gin-gonic/gin v1.9.1 github.com/gin-gonic/gin v1.9.1
github.com/glebarez/sqlite v1.10.0 github.com/glebarez/sqlite v1.10.0
github.com/go-gormigrate/gormigrate/v2 v2.1.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/locales v0.14.1
github.com/go-playground/universal-translator v0.18.1 github.com/go-playground/universal-translator v0.18.1
github.com/go-playground/validator/v10 v10.14.0 github.com/go-playground/validator/v10 v10.14.0
github.com/go-sql-driver/mysql v1.7.1 github.com/go-sql-driver/mysql v1.7.1
github.com/golang-jwt/jwt/v5 v5.2.0 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/gorilla/websocket v1.5.1
github.com/kanzihuang/vitess/go/vt/sqlparser v0.0.0-20231018071450-ac8d9f0167e9 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/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/mojocn/base64Captcha v1.3.6 //
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/pkg/sftp v1.13.6 github.com/pkg/sftp v1.13.6
github.com/pquerna/otp v1.4.0 github.com/pquerna/otp v1.4.0
github.com/redis/go-redis/v9 v9.4.0 github.com/redis/go-redis/v9 v9.4.0
github.com/robfig/cron/v3 v3.0.1 // 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 github.com/stretchr/testify v1.8.4
go.mongodb.org/mongo-driver v1.13.1 // mongo go.mongodb.org/mongo-driver v1.13.1 // mongo
golang.org/x/crypto v0.18.0 // ssh golang.org/x/crypto v0.18.0 // ssh
golang.org/x/oauth2 v0.15.0 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 gopkg.in/yaml.v3 v3.0.1
// gorm // gorm
gorm.io/driver/mysql v1.5.2 gorm.io/driver/mysql v1.5.2
gorm.io/gorm v1.25.5 gorm.io/gorm v1.25.6
) )
require ( require (
@@ -49,8 +53,10 @@ require (
github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect
github.com/glebarez/go-sqlite v1.21.2 // 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/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/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/glog v1.0.0 // indirect github.com/golang/glog v1.0.0 // indirect
github.com/golang/protobuf v1.5.3 // 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/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
golang.org/x/arch v0.3.0 // 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/image v0.13.0 // indirect
golang.org/x/net v0.19.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/sys v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect golang.org/x/text v0.14.0 // indirect
google.golang.org/appengine v1.6.7 // indirect google.golang.org/appengine v1.6.7 // indirect

View File

@@ -1,11 +1,45 @@
package initialize package initialize
import ( import (
dbInit "mayfly-go/internal/db/init" "mayfly-go/pkg/biz"
machineInit "mayfly-go/internal/machine/init" "mayfly-go/pkg/ioc"
) )
func InitOther() { // 初始化ioc函数
machineInit.Init() type InitIocFunc func()
dbInit.Init()
// 初始化函数
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
} }

View File

@@ -3,15 +3,6 @@ package initialize
import ( import (
"fmt" "fmt"
"io/fs" "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/config"
"mayfly-go/pkg/middleware" "mayfly-go/pkg/middleware"
"mayfly-go/static" "mayfly-go/static"
@@ -20,6 +11,18 @@ import (
"github.com/gin-gonic/gin" "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 { func InitRouter() *gin.Engine {
// server配置 // server配置
serverConfig := config.Conf.Server serverConfig := config.Conf.Server
@@ -43,20 +46,11 @@ func InitRouter() *gin.Engine {
// 设置路由组 // 设置路由组
api := router.Group(serverConfig.ContextPath + "/api") api := router.Group(serverConfig.ContextPath + "/api")
{ // 调用所有模块注册的初始化路由函数
common_router.Init(api) for _, initRouterFunc := range initRouterFuncs {
initRouterFunc(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)
} }
initRouterFuncs = nil
return router return router
} }

View File

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

View File

@@ -1,25 +1,20 @@
package initialize package initialize
import ( // 系统进程退出终止函数
dbApp "mayfly-go/internal/db/application" type TerminateFunc func()
var (
terminateFuncs = make([]TerminateFunc, 0)
) )
// 终止服务后的一些操作 // 添加系统退出终止时执行的函数,由各个默认自行添加
func Terminate() { func AddTerminateFunc(terminateFunc TerminateFunc) {
closeDbTasks() terminateFuncs = append(terminateFuncs, terminateFunc)
} }
func closeDbTasks() { // 终止进程服务后的一些操作
restoreApp := dbApp.GetDbRestoreApp() func Terminate() {
if restoreApp != nil { for _, terminateFunc := range terminateFuncs {
restoreApp.Close() terminateFunc()
}
binlogApp := dbApp.GetDbBinlogApp()
if binlogApp != nil {
binlogApp.Close()
}
backupApp := dbApp.GetDbBackupApp()
if backupApp != nil {
backupApp.Close()
} }
} }

View File

@@ -25,8 +25,8 @@ import (
) )
type AccountLogin struct { type AccountLogin struct {
AccountApp sysapp.Account AccountApp sysapp.Account `inject:""`
MsgApp msgapp.Msg MsgApp msgapp.Msg `inject:""`
} }
/** 用户账号密码登录 **/ /** 用户账号密码登录 **/

View File

@@ -28,8 +28,8 @@ import (
) )
type LdapLogin struct { type LdapLogin struct {
AccountApp sysapp.Account AccountApp sysapp.Account `inject:""`
MsgApp msgapp.Msg MsgApp msgapp.Msg `inject:""`
} }
// @router /auth/ldap/enabled [get] // @router /auth/ldap/enabled [get]

View File

@@ -28,9 +28,9 @@ import (
) )
type Oauth2Login struct { type Oauth2Login struct {
Oauth2App application.Oauth2 Oauth2App application.Oauth2 `inject:""`
AccountApp sysapp.Account AccountApp sysapp.Account `inject:""`
MsgApp msgapp.Msg MsgApp msgapp.Msg `inject:""`
} }
func (a *Oauth2Login) OAuth2Login(rc *req.Ctx) { func (a *Oauth2Login) OAuth2Login(rc *req.Ctx) {

View File

@@ -1,11 +1,12 @@
package application package application
import "mayfly-go/internal/auth/infrastructure/persistence" import (
"mayfly-go/internal/auth/infrastructure/persistence"
var ( "mayfly-go/pkg/ioc"
authApp = newAuthApp(persistence.GetOauthAccountRepo())
) )
func GetAuthApp() Oauth2 { func InitIoc() {
return authApp persistence.Init()
ioc.Register(new(oauth2AppImpl), ioc.WithComponentName("Oauth2App"))
} }

View File

@@ -14,27 +14,21 @@ type Oauth2 interface {
Unbind(accountId uint64) Unbind(accountId uint64)
} }
func newAuthApp(oauthAccountRepo repository.Oauth2Account) Oauth2 {
return &oauth2AppImpl{
oauthAccountRepo: oauthAccountRepo,
}
}
type oauth2AppImpl struct { type oauth2AppImpl struct {
oauthAccountRepo repository.Oauth2Account Oauth2AccountRepo repository.Oauth2Account `inject:""`
} }
func (a *oauth2AppImpl) GetOAuthAccount(condition *entity.Oauth2Account, cols ...string) error { 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 { func (a *oauth2AppImpl) BindOAuthAccount(e *entity.Oauth2Account) error {
if e.Id == 0 { 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) { func (a *oauth2AppImpl) Unbind(accountId uint64) {
a.oauthAccountRepo.DeleteByCond(context.Background(), &entity.Oauth2Account{AccountId: accountId}) a.Oauth2AccountRepo.DeleteByCond(context.Background(), &entity.Oauth2Account{AccountId: accountId})
} }

View File

@@ -2,6 +2,7 @@ package config
import ( import (
sysapp "mayfly-go/internal/sys/application" sysapp "mayfly-go/internal/sys/application"
"mayfly-go/pkg/utils/conv"
"mayfly-go/pkg/utils/stringx" "mayfly-go/pkg/utils/stringx"
) )
@@ -26,8 +27,8 @@ func GetAccountLoginSecurity() *AccountLoginSecurity {
als := new(AccountLoginSecurity) als := new(AccountLoginSecurity)
als.UseCaptcha = c.ConvBool(jm["useCaptcha"], true) als.UseCaptcha = c.ConvBool(jm["useCaptcha"], true)
als.UseOtp = c.ConvBool(jm["useOtp"], false) als.UseOtp = c.ConvBool(jm["useOtp"], false)
als.LoginFailCount = stringx.ConvInt(jm["loginFailCount"], 5) als.LoginFailCount = conv.Str2Int(jm["loginFailCount"], 5)
als.LoginFailMin = stringx.ConvInt(jm["loginFailMin"], 10) als.LoginFailMin = conv.Str2Int(jm["loginFailMin"], 10)
otpIssuer := jm["otpIssuer"] otpIssuer := jm["otpIssuer"]
if otpIssuer == "" { if otpIssuer == "" {
otpIssuer = "mayfly-go" otpIssuer = "mayfly-go"

View File

@@ -1,11 +1,9 @@
package persistence package persistence
import "mayfly-go/internal/auth/domain/repository" import (
"mayfly-go/pkg/ioc"
var (
authAccountRepo = newAuthAccountRepo()
) )
func GetOauthAccountRepo() repository.Oauth2Account { func Init() {
return authAccountRepo ioc.Register(newAuthAccountRepo(), ioc.WithComponentName("Oauth2AccountRepo"))
} }

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

View File

@@ -2,30 +2,22 @@ package router
import ( import (
"mayfly-go/internal/auth/api" "mayfly-go/internal/auth/api"
"mayfly-go/internal/auth/application" "mayfly-go/pkg/biz"
msgapp "mayfly-go/internal/msg/application" "mayfly-go/pkg/ioc"
sysapp "mayfly-go/internal/sys/application"
"mayfly-go/pkg/req" "mayfly-go/pkg/req"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
func Init(router *gin.RouterGroup) { func Init(router *gin.RouterGroup) {
accountLogin := &api.AccountLogin{ accountLogin := new(api.AccountLogin)
AccountApp: sysapp.GetAccountApp(), biz.ErrIsNil(ioc.Inject(accountLogin))
MsgApp: msgapp.GetMsgApp(),
}
ldapLogin := &api.LdapLogin{ ldapLogin := new(api.LdapLogin)
AccountApp: sysapp.GetAccountApp(), biz.ErrIsNil(ioc.Inject(ldapLogin))
MsgApp: msgapp.GetMsgApp(),
}
oauth2Login := &api.Oauth2Login{ oauth2Login := new(api.Oauth2Login)
Oauth2App: application.GetAuthApp(), biz.ErrIsNil(ioc.Inject(oauth2Login))
AccountApp: sysapp.GetAccountApp(),
MsgApp: msgapp.GetMsgApp(),
}
rg := router.Group("/auth") rg := router.Group("/auth")

View File

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

View File

@@ -0,0 +1,10 @@
package init
import (
"mayfly-go/initialize"
"mayfly-go/internal/common/router"
)
func init() {
initialize.AddInitRouterFunc(router.Init)
}

View File

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

View File

@@ -4,5 +4,4 @@ import "github.com/gin-gonic/gin"
func Init(router *gin.RouterGroup) { func Init(router *gin.RouterGroup) {
InitCommonRouter(router) InitCommonRouter(router)
InitIndexRouter(router)
} }

View 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,
}
}

View File

@@ -32,11 +32,11 @@ import (
) )
type Db struct { type Db struct {
InstanceApp application.Instance InstanceApp application.Instance `inject:"DbInstanceApp"`
DbApp application.Db DbApp application.Db `inject:""`
DbSqlExecApp application.DbSqlExec DbSqlExecApp application.DbSqlExec `inject:""`
MsgApp msgapp.Msg MsgApp msgapp.Msg `inject:""`
TagApp tagapp.TagTree TagApp tagapp.TagTree `inject:"TagTreeApp"`
} }
// @router /api/dbs [get] // @router /api/dbs [get]
@@ -355,7 +355,7 @@ func (d *Db) dumpDb(writer *gzipWriter, dbId uint64, dbName string, tables []str
writer.WriteString("BEGIN;\n") writer.WriteString("BEGIN;\n")
} }
insertSql := "INSERT INTO %s VALUES (%s);\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 var values []string
writer.TryFlush() writer.TryFlush()
for _, column := range columns { for _, column := range columns {
@@ -462,6 +462,20 @@ func (d *Db) GetSchemas(rc *req.Ctx) {
rc.ResData = res 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 { func getDbId(g *gin.Context) uint64 {
dbId, _ := strconv.Atoi(g.Param("dbId")) dbId, _ := strconv.Atoi(g.Param("dbId"))
biz.IsTrue(dbId > 0, "dbId错误") biz.IsTrue(dbId > 0, "dbId错误")

View File

@@ -9,13 +9,16 @@ import (
"mayfly-go/pkg/biz" "mayfly-go/pkg/biz"
"mayfly-go/pkg/ginx" "mayfly-go/pkg/ginx"
"mayfly-go/pkg/req" "mayfly-go/pkg/req"
"mayfly-go/pkg/utils/timex"
"strconv" "strconv"
"strings" "strings"
"time"
) )
type DbBackup struct { type DbBackup struct {
DbBackupApp *application.DbBackupApp backupApp *application.DbBackupApp `inject:"DbBackupApp"`
DbApp application.Db dbApp application.Db `inject:"DbApp"`
restoreApp *application.DbRestoreApp `inject:"DbRestoreApp"`
} }
// todo: 鉴权,避免未经授权进行数据库备份和恢复 // todo: 鉴权,避免未经授权进行数据库备份和恢复
@@ -25,13 +28,13 @@ type DbBackup struct {
func (d *DbBackup) GetPageList(rc *req.Ctx) { func (d *DbBackup) GetPageList(rc *req.Ctx) {
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId")) dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
biz.IsTrue(dbId > 0, "无效的 dbId: %v", 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") 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.DbInstanceId = db.InstanceId
queryCond.InDbNames = strings.Fields(db.Database) 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") biz.ErrIsNilAppendErr(err, "获取数据库备份任务失败: %v")
rc.ResData = res rc.ResData = res
} }
@@ -48,23 +51,22 @@ func (d *DbBackup) Create(rc *req.Ctx) {
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId")) dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
biz.IsTrue(dbId > 0, "无效的 dbId: %v", 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") biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
jobs := make([]*entity.DbBackup, 0, len(dbNames)) jobs := make([]*entity.DbBackup, 0, len(dbNames))
for _, dbName := range dbNames { for _, dbName := range dbNames {
job := &entity.DbBackup{ job := &entity.DbBackup{
DbJobBaseImpl: entity.NewDbBJobBase(db.InstanceId, entity.DbJobTypeBackup), DbInstanceId: db.InstanceId,
DbName: dbName,
Enabled: true, Enabled: true,
Repeated: backupForm.Repeated, Repeated: backupForm.Repeated,
StartTime: backupForm.StartTime, StartTime: backupForm.StartTime,
Interval: backupForm.Interval, Interval: backupForm.Interval,
Name: backupForm.Name, Name: backupForm.Name,
} }
job.DbName = dbName
jobs = append(jobs, job) jobs = append(jobs, job)
} }
biz.ErrIsNilAppendErr(d.DbBackupApp.Create(rc.MetaCtx, jobs), "添加数据库备份任务失败: %v") biz.ErrIsNilAppendErr(d.backupApp.Create(rc.MetaCtx, jobs), "添加数据库备份任务失败: %v")
} }
// Update 保存数据库备份任务 // Update 保存数据库备份任务
@@ -74,17 +76,17 @@ func (d *DbBackup) Update(rc *req.Ctx) {
ginx.BindJsonAndValid(rc.GinCtx, backupForm) ginx.BindJsonAndValid(rc.GinCtx, backupForm)
rc.ReqParam = backupForm rc.ReqParam = backupForm
job := entity.NewDbJob(entity.DbJobTypeBackup).(*entity.DbBackup) job := &entity.DbBackup{}
job.Id = backupForm.Id job.Id = backupForm.Id
job.Name = backupForm.Name job.Name = backupForm.Name
job.StartTime = backupForm.StartTime job.StartTime = backupForm.StartTime
job.Interval = backupForm.Interval 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 { func (d *DbBackup) walk(rc *req.Ctx, paramName string, fn func(ctx context.Context, id uint64) error) error {
idsStr := ginx.PathParam(rc.GinCtx, "backupId") idsStr := ginx.PathParam(rc.GinCtx, paramName)
biz.NotEmpty(idsStr, "backupId 为空") biz.NotEmpty(idsStr, paramName+" 为空")
rc.ReqParam = idsStr rc.ReqParam = idsStr
ids := strings.Fields(idsStr) ids := strings.Fields(idsStr)
for _, v := range ids { for _, v := range ids {
@@ -104,28 +106,28 @@ func (d *DbBackup) walk(rc *req.Ctx, fn func(ctx context.Context, backupId uint6
// Delete 删除数据库备份任务 // Delete 删除数据库备份任务
// @router /api/dbs/:dbId/backups/:backupId [DELETE] // @router /api/dbs/:dbId/backups/:backupId [DELETE]
func (d *DbBackup) Delete(rc *req.Ctx) { 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") biz.ErrIsNilAppendErr(err, "删除数据库备份任务失败: %v")
} }
// Enable 启用数据库备份任务 // Enable 启用数据库备份任务
// @router /api/dbs/:dbId/backups/:backupId/enable [PUT] // @router /api/dbs/:dbId/backups/:backupId/enable [PUT]
func (d *DbBackup) Enable(rc *req.Ctx) { 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") biz.ErrIsNilAppendErr(err, "启用数据库备份任务失败: %v")
} }
// Disable 禁用数据库备份任务 // Disable 禁用数据库备份任务
// @router /api/dbs/:dbId/backups/:backupId/disable [PUT] // @router /api/dbs/:dbId/backups/:backupId/disable [PUT]
func (d *DbBackup) Disable(rc *req.Ctx) { 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") biz.ErrIsNilAppendErr(err, "禁用数据库备份任务失败: %v")
} }
// Start 禁用数据库备份任务 // Start 禁用数据库备份任务
// @router /api/dbs/:dbId/backups/:backupId/start [PUT] // @router /api/dbs/:dbId/backups/:backupId/start [PUT]
func (d *DbBackup) Start(rc *req.Ctx) { 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") biz.ErrIsNilAppendErr(err, "运行数据库备份任务失败: %v")
} }
@@ -133,10 +135,10 @@ func (d *DbBackup) Start(rc *req.Ctx) {
// @router /api/dbs/:dbId/db-names-without-backup [GET] // @router /api/dbs/:dbId/db-names-without-backup [GET]
func (d *DbBackup) GetDbNamesWithoutBackup(rc *req.Ctx) { func (d *DbBackup) GetDbNamesWithoutBackup(rc *req.Ctx) {
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId")) 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") biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
dbNames := strings.Fields(db.Database) 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") biz.ErrIsNilAppendErr(err, "获取未配置定时备份的数据库名称失败: %v")
rc.ResData = dbNamesWithoutBackup rc.ResData = dbNamesWithoutBackup
} }
@@ -146,13 +148,74 @@ func (d *DbBackup) GetDbNamesWithoutBackup(rc *req.Ctx) {
func (d *DbBackup) GetHistoryPageList(rc *req.Ctx) { func (d *DbBackup) GetHistoryPageList(rc *req.Ctx) {
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId")) dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
biz.IsTrue(dbId > 0, "无效的 dbId: %v", 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") biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
queryCond, page := ginx.BindQueryAndPage[*entity.DbBackupHistoryQuery](rc.GinCtx, new(entity.DbBackupHistoryQuery)) backupHistoryCond, page := ginx.BindQueryAndPage[*entity.DbBackupHistoryQuery](rc.GinCtx, new(entity.DbBackupHistoryQuery))
queryCond.DbInstanceId = db.InstanceId backupHistoryCond.DbInstanceId = db.InstanceId
queryCond.InDbNames = strings.Fields(db.Database) backupHistoryCond.InDbNames = strings.Fields(db.Database)
res, err := d.DbBackupApp.GetHistoryPageList(queryCond, page, new([]vo.DbBackupHistory)) backupHistories := make([]*vo.DbBackupHistory, 0, page.PageSize)
res, err := d.backupApp.GetHistoryPageList(backupHistoryCond, page, &backupHistories)
biz.ErrIsNilAppendErr(err, "获取数据库备份历史失败: %v") 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 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")
}

View File

@@ -1,7 +1,6 @@
package api package api
import ( import (
"context"
"encoding/base64" "encoding/base64"
"mayfly-go/internal/db/api/form" "mayfly-go/internal/db/api/form"
"mayfly-go/internal/db/api/vo" "mayfly-go/internal/db/api/vo"
@@ -15,11 +14,10 @@ import (
"strings" "strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid"
) )
type DataSyncTask struct { type DataSyncTask struct {
DataSyncTaskApp application.DataSyncTask DataSyncTaskApp application.DataSyncTask `inject:"DbDataSyncTaskApp"`
} }
func (d *DataSyncTask) Tasks(rc *req.Ctx) { func (d *DataSyncTask) Tasks(rc *req.Ctx) {
@@ -47,13 +45,6 @@ func (d *DataSyncTask) SaveTask(rc *req.Ctx) {
task.DataSql = sql task.DataSql = sql
form.DataSql = sql form.DataSql = sql
key := task.TaskKey
// 判断key为空就生成随机key
if key == "" {
key = uuid.New().String()
task.TaskKey = key
}
rc.ReqParam = form rc.ReqParam = form
biz.ErrIsNil(d.DataSyncTaskApp.Save(rc.MetaCtx, task)) 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) { func (d *DataSyncTask) ChangeStatus(rc *req.Ctx) {
form := &form.DataSyncTaskStatusForm{} form := &form.DataSyncTaskStatusForm{}
task := ginx.BindJsonAndCopyTo[*entity.DataSyncTask](rc.GinCtx, form, new(entity.DataSyncTask)) 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 { if task.Status == entity.DataSyncTaskStatusEnable {
task, err := d.DataSyncTaskApp.GetById(new(entity.DataSyncTask), task.Id) 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) { func (d *DataSyncTask) Run(rc *req.Ctx) {
taskId := getTaskId(rc.GinCtx) taskId := getTaskId(rc.GinCtx)
rc.ReqParam = taskId rc.ReqParam = taskId
d.DataSyncTaskApp.RunCronJob(taskId) _ = d.DataSyncTaskApp.RunCronJob(taskId)
} }
func (d *DataSyncTask) Stop(rc *req.Ctx) { func (d *DataSyncTask) Stop(rc *req.Ctx) {
@@ -99,7 +90,7 @@ func (d *DataSyncTask) Stop(rc *req.Ctx) {
task := new(entity.DataSyncTask) task := new(entity.DataSyncTask)
task.Id = taskId task.Id = taskId
task.RunningState = entity.DataSyncTaskRunStateStop task.RunningState = entity.DataSyncTaskRunStateStop
_ = d.DataSyncTaskApp.UpdateById(context.Background(), task) _ = d.DataSyncTaskApp.UpdateById(rc.MetaCtx, task)
} }
func (d *DataSyncTask) GetTask(rc *req.Ctx) { func (d *DataSyncTask) GetTask(rc *req.Ctx) {

View File

@@ -14,8 +14,8 @@ import (
) )
type DbRestore struct { type DbRestore struct {
DbRestoreApp *application.DbRestoreApp restoreApp *application.DbRestoreApp `inject:"DbRestoreApp"`
DbApp application.Db dbApp application.Db `inject:"DbApp"`
} }
// GetPageList 获取数据库恢复任务 // GetPageList 获取数据库恢复任务
@@ -23,14 +23,14 @@ type DbRestore struct {
func (d *DbRestore) GetPageList(rc *req.Ctx) { func (d *DbRestore) GetPageList(rc *req.Ctx) {
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId")) dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
biz.IsTrue(dbId > 0, "无效的 dbId: %v", 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") biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
var restores []vo.DbRestore 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.DbInstanceId = db.InstanceId
queryCond.InDbNames = strings.Fields(db.Database) 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") biz.ErrIsNilAppendErr(err, "获取数据库恢复任务失败: %v")
rc.ResData = res rc.ResData = res
} }
@@ -44,11 +44,12 @@ func (d *DbRestore) Create(rc *req.Ctx) {
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId")) dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
biz.IsTrue(dbId > 0, "无效的 dbId: %v", 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") biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
job := &entity.DbRestore{ job := &entity.DbRestore{
DbJobBaseImpl: entity.NewDbBJobBase(db.InstanceId, entity.DbJobTypeRestore), DbInstanceId: db.InstanceId,
DbName: restoreForm.DbName,
Enabled: true, Enabled: true,
Repeated: restoreForm.Repeated, Repeated: restoreForm.Repeated,
StartTime: restoreForm.StartTime, StartTime: restoreForm.StartTime,
@@ -58,8 +59,11 @@ func (d *DbRestore) Create(rc *req.Ctx) {
DbBackupHistoryId: restoreForm.DbBackupHistoryId, DbBackupHistoryId: restoreForm.DbBackupHistoryId,
DbBackupHistoryName: restoreForm.DbBackupHistoryName, DbBackupHistoryName: restoreForm.DbBackupHistoryName,
} }
job.DbName = restoreForm.DbName biz.ErrIsNilAppendErr(d.restoreApp.Create(rc.MetaCtx, job), "添加数据库恢复任务失败: %v")
biz.ErrIsNilAppendErr(d.DbRestoreApp.Create(rc.MetaCtx, job), "添加数据库恢复任务失败: %v") }
func (d *DbRestore) createWithBackupHistory(backupHistoryIds string) {
} }
// Update 保存数据库恢复任务 // Update 保存数据库恢复任务
@@ -73,7 +77,7 @@ func (d *DbRestore) Update(rc *req.Ctx) {
job.Id = restoreForm.Id job.Id = restoreForm.Id
job.StartTime = restoreForm.StartTime job.StartTime = restoreForm.StartTime
job.Interval = restoreForm.Interval 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 { 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 删除数据库恢复任务 // Delete 删除数据库恢复任务
// @router /api/dbs/:dbId/restores/:restoreId [DELETE] // @router /api/dbs/:dbId/restores/:restoreId [DELETE]
func (d *DbRestore) Delete(rc *req.Ctx) { 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") biz.ErrIsNilAppendErr(err, "删除数据库恢复任务失败: %v")
} }
// Enable 启用数据库恢复任务 // Enable 启用数据库恢复任务
// @router /api/dbs/:dbId/restores/:restoreId/enable [PUT] // @router /api/dbs/:dbId/restores/:restoreId/enable [PUT]
func (d *DbRestore) Enable(rc *req.Ctx) { 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") biz.ErrIsNilAppendErr(err, "启用数据库恢复任务失败: %v")
} }
// Disable 禁用数据库恢复任务 // Disable 禁用数据库恢复任务
// @router /api/dbs/:dbId/restores/:restoreId/disable [PUT] // @router /api/dbs/:dbId/restores/:restoreId/disable [PUT]
func (d *DbRestore) Disable(rc *req.Ctx) { 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") biz.ErrIsNilAppendErr(err, "禁用数据库恢复任务失败: %v")
} }
@@ -120,21 +124,21 @@ func (d *DbRestore) Disable(rc *req.Ctx) {
// @router /api/dbs/:dbId/db-names-without-backup [GET] // @router /api/dbs/:dbId/db-names-without-backup [GET]
func (d *DbRestore) GetDbNamesWithoutRestore(rc *req.Ctx) { func (d *DbRestore) GetDbNamesWithoutRestore(rc *req.Ctx) {
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId")) 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") biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
dbNames := strings.Fields(db.Database) 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") biz.ErrIsNilAppendErr(err, "获取未配置定时备份的数据库名称失败: %v")
rc.ResData = dbNamesWithoutRestore rc.ResData = dbNamesWithoutRestore
} }
// 获取数据库备份历史 // GetHistoryPageList 获取数据库备份历史
// @router /api/dbs/:dbId/restores/:restoreId/histories [GET] // @router /api/dbs/:dbId/restores/:restoreId/histories [GET]
func (d *DbRestore) GetHistoryPageList(rc *req.Ctx) { func (d *DbRestore) GetHistoryPageList(rc *req.Ctx) {
queryCond := &entity.DbRestoreHistoryQuery{ queryCond := &entity.DbRestoreHistoryQuery{
DbRestoreId: uint64(ginx.PathParamInt(rc.GinCtx, "restoreId")), 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") biz.ErrIsNilAppendErr(err, "获取数据库备份历史失败: %v")
rc.ResData = res rc.ResData = res
} }

View File

@@ -10,7 +10,7 @@ import (
) )
type DbSql struct { type DbSql struct {
DbSqlApp application.DbSql DbSqlApp application.DbSql `inject:""`
} }
// @router /api/db/:dbId/sql [post] // @router /api/db/:dbId/sql [post]

View File

@@ -9,7 +9,7 @@ import (
) )
type DbSqlExec struct { type DbSqlExec struct {
DbSqlExecApp application.DbSqlExec DbSqlExecApp application.DbSqlExec `inject:""`
} }
func (d *DbSqlExec) DbSqlExecs(rc *req.Ctx) { func (d *DbSqlExec) DbSqlExecs(rc *req.Ctx) {

View File

@@ -23,3 +23,11 @@ type DbSqlExecForm struct {
Sql string `binding:"required" json:"sql"` // 执行sql Sql string `binding:"required" json:"sql"` // 执行sql
Remark string `json:"remark"` // 执行备注 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"` // 是否复制数据
}

View File

@@ -5,9 +5,9 @@ type InstanceForm struct {
Name string `binding:"required" json:"name"` Name string `binding:"required" json:"name"`
Type string `binding:"required" json:"type"` // 类型mysql oracle等 Type string `binding:"required" json:"type"` // 类型mysql oracle等
Host string `binding:"required" json:"host"` Host string `binding:"required" json:"host"`
Port int `binding:"required" json:"port"` Port int `json:"port"`
Sid string `json:"sid"` Sid string `json:"sid"`
Username string `binding:"required" json:"username"` Username string `json:"username"`
Password string `json:"password"` Password string `json:"password"`
Params string `json:"params"` Params string `json:"params"`
Remark string `json:"remark"` Remark string `json:"remark"`

View File

@@ -16,8 +16,8 @@ import (
) )
type Instance struct { type Instance struct {
InstanceApp application.Instance InstanceApp application.Instance `inject:"DbInstanceApp"`
DbApp application.Db DbApp application.Db `inject:""`
} }
// Instances 获取数据库实例信息 // Instances 获取数据库实例信息

View File

@@ -2,6 +2,7 @@ package vo
import ( import (
"encoding/json" "encoding/json"
"mayfly-go/internal/db/domain/entity"
"mayfly-go/pkg/utils/timex" "mayfly-go/pkg/utils/timex"
"time" "time"
) )
@@ -15,8 +16,9 @@ type DbBackup struct {
Interval time.Duration `json:"-"` // 间隔时间 Interval time.Duration `json:"-"` // 间隔时间
IntervalDay uint64 `json:"intervalDay" gorm:"-"` // 间隔天数 IntervalDay uint64 `json:"intervalDay" gorm:"-"` // 间隔天数
Enabled bool `json:"enabled"` // 是否启用 Enabled bool `json:"enabled"` // 是否启用
EnabledDesc string `json:"enabledDesc"` // 启用状态描述
LastTime timex.NullTime `json:"lastTime"` // 最近一次执行时间 LastTime timex.NullTime `json:"lastTime"` // 最近一次执行时间
LastStatus string `json:"lastStatus"` // 最近一次执行状态 LastStatus entity.DbJobStatus `json:"lastStatus"` // 最近一次执行状态
LastResult string `json:"lastResult"` // 最近一次执行结果 LastResult string `json:"lastResult"` // 最近一次执行结果
DbInstanceId uint64 `json:"dbInstanceId"` // 数据库实例ID DbInstanceId uint64 `json:"dbInstanceId"` // 数据库实例ID
Name string `json:"name"` // 备份任务名称 Name string `json:"name"` // 备份任务名称
@@ -25,6 +27,13 @@ type DbBackup struct {
func (backup *DbBackup) MarshalJSON() ([]byte, error) { func (backup *DbBackup) MarshalJSON() ([]byte, error) {
type dbBackup DbBackup type dbBackup DbBackup
backup.IntervalDay = uint64(backup.Interval / time.Hour / 24) 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)) return json.Marshal((*dbBackup)(backup))
} }
@@ -35,4 +44,8 @@ type DbBackupHistory struct {
CreateTime time.Time `json:"createTime"` CreateTime time.Time `json:"createTime"`
DbName string `json:"dbName"` // 数据库名称 DbName string `json:"dbName"` // 数据库名称
Name string `json:"name"` // 备份历史名称 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:"-"` // 最近一次恢复结果
} }

View File

@@ -14,6 +14,7 @@ type DbRestore struct {
Interval time.Duration `json:"-"` // 间隔时间 Interval time.Duration `json:"-"` // 间隔时间
IntervalDay uint64 `json:"intervalDay" gorm:"-"` // 间隔天数 IntervalDay uint64 `json:"intervalDay" gorm:"-"` // 间隔天数
Enabled bool `json:"enabled"` // 是否启用 Enabled bool `json:"enabled"` // 是否启用
EnabledDesc string `json:"enabledDesc"` // 启用状态描述
LastTime timex.NullTime `json:"lastTime"` // 最近一次执行时间 LastTime timex.NullTime `json:"lastTime"` // 最近一次执行时间
LastStatus string `json:"lastStatus"` // 最近一次执行状态 LastStatus string `json:"lastStatus"` // 最近一次执行状态
LastResult string `json:"lastResult"` // 最近一次执行结果 LastResult string `json:"lastResult"` // 最近一次执行结果
@@ -27,6 +28,13 @@ type DbRestore struct {
func (restore *DbRestore) MarshalJSON() ([]byte, error) { func (restore *DbRestore) MarshalJSON() ([]byte, error) {
type dbBackup DbRestore type dbBackup DbRestore
restore.IntervalDay = uint64(restore.Interval / time.Hour / 24) 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)) return json.Marshal((*dbBackup)(restore))
} }

View File

@@ -2,91 +2,50 @@ package application
import ( import (
"fmt" "fmt"
"mayfly-go/internal/db/domain/repository"
"mayfly-go/internal/db/infrastructure/persistence" "mayfly-go/internal/db/infrastructure/persistence"
tagapp "mayfly-go/internal/tag/application" "mayfly-go/pkg/ioc"
"sync" "sync"
) )
var ( func InitIoc() {
instanceApp Instance persistence.Init()
dbApp Db
dbSqlExecApp DbSqlExec ioc.Register(new(instanceAppImpl), ioc.WithComponentName("DbInstanceApp"))
dbSqlApp DbSql ioc.Register(new(dbAppImpl), ioc.WithComponentName("DbApp"))
dbBackupApp *DbBackupApp ioc.Register(new(dbSqlExecAppImpl), ioc.WithComponentName("DbSqlExecApp"))
dbRestoreApp *DbRestoreApp ioc.Register(new(dbSqlAppImpl), ioc.WithComponentName("DbSqlApp"))
dbBinlogApp *DbBinlogApp ioc.Register(new(dataSyncAppImpl), ioc.WithComponentName("DbDataSyncTaskApp"))
dataSyncApp DataSyncTask
) 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() { func Init() {
sync.OnceFunc(func() { sync.OnceFunc(func() {
repositories := &repository.Repositories{ if err := GetDbBackupApp().Init(); err != nil {
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 {
panic(fmt.Sprintf("初始化 dbBackupApp 失败: %v", err)) panic(fmt.Sprintf("初始化 dbBackupApp 失败: %v", err))
} }
dbRestoreApp, err = newDbRestoreApp(repositories, dbApp, scheduler) if err := GetDbRestoreApp().Init(); err != nil {
if err != nil {
panic(fmt.Sprintf("初始化 dbRestoreApp 失败: %v", err)) panic(fmt.Sprintf("初始化 dbRestoreApp 失败: %v", err))
} }
dbBinlogApp, err = newDbBinlogApp(repositories, dbApp, scheduler) GetDataSyncTaskApp().InitCronJob()
if err != nil {
panic(fmt.Sprintf("初始化 dbBinlogApp 失败: %v", err))
}
dataSyncApp.InitCronJob()
})() })()
} }
func GetInstanceApp() Instance {
return instanceApp
}
func GetDbApp() Db {
return dbApp
}
func GetDbSqlApp() DbSql {
return dbSqlApp
}
func GetDbSqlExecApp() DbSqlExec {
return dbSqlExecApp
}
func GetDbBackupApp() *DbBackupApp { func GetDbBackupApp() *DbBackupApp {
return dbBackupApp return ioc.Get[*DbBackupApp]("DbBackupApp")
} }
func GetDbRestoreApp() *DbRestoreApp { func GetDbRestoreApp() *DbRestoreApp {
return dbRestoreApp return ioc.Get[*DbRestoreApp]("DbRestoreApp")
} }
func GetDbBinlogApp() *DbBinlogApp { func GetDbBinlogApp() *DbBinlogApp {
return dbBinlogApp return ioc.Get[*DbBinlogApp]("DbBinlogApp")
} }
func GetDataSyncTaskApp() DataSyncTask { func GetDataSyncTaskApp() DataSyncTask {
return dataSyncApp return ioc.Get[DataSyncTask]("DbDataSyncTaskApp")
} }

View File

@@ -40,22 +40,17 @@ type Db interface {
GetDbConnByInstanceId(instanceId uint64) (*dbi.DbConn, error) 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 { type dbAppImpl struct {
base.AppImpl[*entity.Db, repository.Db] base.AppImpl[*entity.Db, repository.Db]
dbSqlRepo repository.DbSql dbSqlRepo repository.DbSql `inject:"DbSqlRepo"`
dbInstanceApp Instance dbInstanceApp Instance `inject:"DbInstanceApp"`
tagApp tagapp.TagTree 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) _, delDb, _ := collx.ArrayCompare(newDbs, oldDbs)
for _, v := range delDb { // 先简单关闭可能存在的旧库连接可能改了关联标签导致DbConn.Info.TagPath与修改后的标签不一致、导致操作权限校验出错
for _, v := range oldDbs {
// 关闭数据库连接 // 关闭数据库连接
dbm.CloseDb(dbEntity.Id, v) dbm.CloseDb(dbEntity.Id, v)
}
for _, v := range delDb {
// 删除该库关联的所有sql记录 // 删除该库关联的所有sql记录
d.dbSqlRepo.DeleteByCond(ctx, &entity.DbSql{DbId: dbId, Db: v}) 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 checkDb := dbName
// 兼容pgsql/dm db/schema模式 // 兼容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, "/") ss := strings.Split(dbName, "/")
if len(ss) > 1 { if len(ss) > 1 {
checkDb = ss[0] checkDb = ss[0]

View File

@@ -3,36 +3,36 @@ package application
import ( import (
"context" "context"
"encoding/binary" "encoding/binary"
"github.com/google/uuid" "errors"
"fmt"
"gorm.io/gorm"
"mayfly-go/internal/db/domain/entity" "mayfly-go/internal/db/domain/entity"
"mayfly-go/internal/db/domain/repository" "mayfly-go/internal/db/domain/repository"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/model" "mayfly-go/pkg/model"
"sync"
"github.com/google/uuid"
) )
func newDbBackupApp(repositories *repository.Repositories, dbApp Db, scheduler *dbScheduler) (*DbBackupApp, error) { type DbBackupApp struct {
var jobs []*entity.DbBackup scheduler *dbScheduler `inject:"DbScheduler"`
if err := repositories.Backup.ListToDo(&jobs); err != nil { backupRepo repository.DbBackup `inject:"DbBackupRepo"`
return nil, err backupHistoryRepo repository.DbBackupHistory `inject:"DbBackupHistoryRepo"`
} restoreRepo repository.DbRestore `inject:"DbRestoreRepo"`
if err := scheduler.AddJob(context.Background(), false, entity.DbJobTypeBackup, jobs); err != nil { dbApp Db `inject:"DbApp"`
return nil, err mutex sync.Mutex
}
app := &DbBackupApp{
backupRepo: repositories.Backup,
instanceRepo: repositories.Instance,
backupHistoryRepo: repositories.BackupHistory,
dbApp: dbApp,
scheduler: scheduler,
}
return app, nil
} }
type DbBackupApp struct { func (app *DbBackupApp) Init() error {
backupRepo repository.DbBackup var jobs []*entity.DbBackup
instanceRepo repository.Instance if err := app.backupRepo.ListToDo(&jobs); err != nil {
backupHistoryRepo repository.DbBackupHistory return err
dbApp Db }
scheduler *dbScheduler if err := app.scheduler.AddJob(context.Background(), jobs); err != nil {
return err
}
return nil
} }
func (app *DbBackupApp) Close() { func (app *DbBackupApp) Close() {
@@ -40,32 +40,111 @@ func (app *DbBackupApp) Close() {
} }
func (app *DbBackupApp) Create(ctx context.Context, jobs []*entity.DbBackup) error { 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 { 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 { func (app *DbBackupApp) Delete(ctx context.Context, jobId uint64) error {
// todo: 删除数据库备份历史文件 // 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 { 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 { 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 { func (app *DbBackupApp) StartNow(ctx context.Context, jobId uint64) error {
return app.scheduler.StartJobNow(ctx, entity.DbJobTypeBackup, jobId) 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 分页获取数据库备份任务 // 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...) return app.backupRepo.GetPageList(condition, pageParam, toEntity, orderBy...)
} }
@@ -76,7 +155,11 @@ func (app *DbBackupApp) GetDbNamesWithoutBackup(instanceId uint64, dbNames []str
// GetHistoryPageList 分页获取数据库备份历史 // GetHistoryPageList 分页获取数据库备份历史
func (app *DbBackupApp) GetHistoryPageList(condition *entity.DbBackupHistoryQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) { 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) { func NewIncUUID() (uuid.UUID, error) {
@@ -99,3 +182,41 @@ func NewIncUUID() (uuid.UUID, error) {
return uid, nil 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)
}

View File

@@ -11,32 +11,24 @@ import (
) )
type DbBinlogApp struct { type DbBinlogApp struct {
binlogRepo repository.DbBinlog scheduler *dbScheduler `inject:"DbScheduler"`
binlogHistoryRepo repository.DbBinlogHistory binlogRepo repository.DbBinlog `inject:"DbBinlogRepo"`
backupRepo repository.DbBackup backupRepo repository.DbBackup `inject:"DbBackupRepo"`
backupHistoryRepo repository.DbBackupHistory
dbApp Db
context context.Context context context.Context
cancel context.CancelFunc cancel context.CancelFunc
waitGroup sync.WaitGroup waitGroup sync.WaitGroup
scheduler *dbScheduler
} }
func newDbBinlogApp(repositories *repository.Repositories, dbApp Db, scheduler *dbScheduler) (*DbBinlogApp, error) { func newDbBinlogApp() *DbBinlogApp {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
svc := &DbBinlogApp{ svc := &DbBinlogApp{
binlogRepo: repositories.Binlog,
binlogHistoryRepo: repositories.BinlogHistory,
backupRepo: repositories.Backup,
backupHistoryRepo: repositories.BackupHistory,
dbApp: dbApp,
scheduler: scheduler,
context: ctx, context: ctx,
cancel: cancel, cancel: cancel,
} }
svc.waitGroup.Add(1) svc.waitGroup.Add(1)
go svc.run() go svc.run()
return svc, nil return svc
} }
func (app *DbBinlogApp) run() { func (app *DbBinlogApp) run() {
@@ -54,7 +46,7 @@ func (app *DbBinlogApp) run() {
if app.closed() { if app.closed() {
break 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()) logx.Error("DbBinlogApp: 添加 BINLOG 同步任务失败: ", err.Error())
} }
timex.SleepWithContext(app.context, entity.BinlogDownloadInterval) timex.SleepWithContext(app.context, entity.BinlogDownloadInterval)

View File

@@ -14,7 +14,12 @@ import (
"mayfly-go/pkg/logx" "mayfly-go/pkg/logx"
"mayfly-go/pkg/model" "mayfly-go/pkg/model"
"mayfly-go/pkg/scheduler" "mayfly-go/pkg/scheduler"
"regexp"
"strconv"
"strings"
"time" "time"
"github.com/google/uuid"
) )
type DataSyncTask interface { 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) 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 { type dataSyncAppImpl struct {
base.AppImpl[*entity.DataSyncTask, repository.DataSyncTask] 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) { 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 { func (app *dataSyncAppImpl) Save(ctx context.Context, taskEntity *entity.DataSyncTask) error {
var err error var err error
if taskEntity.Id == 0 { if taskEntity.Id == 0 {
// 新建时生成key
taskEntity.TaskKey = uuid.New().String()
err = app.Insert(ctx, taskEntity) err = app.Insert(ctx, taskEntity)
} else { } else {
err = app.UpdateById(ctx, taskEntity) err = app.UpdateById(ctx, taskEntity)
} }
if err != nil { if err != nil {
return err return err
} }
app.AddCronJob(taskEntity) task, err := app.GetById(new(entity.DataSyncTask), taskEntity.Id)
if err != nil {
return err
}
app.AddCronJob(task)
return nil return nil
} }
@@ -85,8 +100,10 @@ func (app *dataSyncAppImpl) AddCronJob(taskEntity *entity.DataSyncTask) {
// 根据状态添加新的任务 // 根据状态添加新的任务
if taskEntity.Status == entity.DataSyncTaskStatusEnable { if taskEntity.Status == entity.DataSyncTaskStatusEnable {
taskId := taskEntity.Id
scheduler.AddFunByKey(key, taskEntity.TaskCron, func() { 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()) logx.Errorf("定时执行数据同步任务失败: %s", err.Error())
} }
}) })
@@ -126,7 +143,23 @@ func (app *dataSyncAppImpl) RunCronJob(id uint64) error {
updSql := "" updSql := ""
orderSql := "" orderSql := ""
if task.UpdFieldVal != "0" && task.UpdFieldVal != "" && task.UpdField != "" { if task.UpdFieldVal != "0" && task.UpdFieldVal != "" && task.UpdField != "" {
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) 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 " orderSql = "order by " + task.UpdField + " asc "
} }
// 组装查询sql // 组装查询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 { if err != nil {
return syncLog, errorx.NewBiz("连接源数据库失败: %s", err.Error()) 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 { if err != nil {
return syncLog, errorx.NewBiz("连接目标数据库失败: %s", err.Error()) return syncLog, errorx.NewBiz("连接目标数据库失败: %s", err.Error())
} }
@@ -197,8 +230,8 @@ func (app *dataSyncAppImpl) doDataSync(sql string, task *entity.DataSyncTask) (*
// 遍历columns 取task.UpdField的字段类型 // 遍历columns 取task.UpdField的字段类型
updFieldType = dbi.DataTypeString updFieldType = dbi.DataTypeString
for _, column := range columns { for _, column := range columns {
if column.Name == task.UpdField { if strings.EqualFold(strings.ToLower(column.Name), strings.ToLower(task.UpdField)) {
updFieldType = srcDialect.GetDataType(column.Type) updFieldType = srcDialect.GetDataConverter().GetDataType(column.Type)
break break
} }
} }
@@ -207,7 +240,7 @@ func (app *dataSyncAppImpl) doDataSync(sql string, task *entity.DataSyncTask) (*
total++ total++
result = append(result, row) result = append(result, row)
if total%batchSize == 0 { 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 return err
} }
@@ -229,7 +262,7 @@ func (app *dataSyncAppImpl) doDataSync(sql string, task *entity.DataSyncTask) (*
// 处理剩余的数据 // 处理剩余的数据
if len(result) > 0 { 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() targetDbTx.Rollback()
return syncLog, err return syncLog, err
} }
@@ -249,10 +282,16 @@ func (app *dataSyncAppImpl) doDataSync(sql string, task *entity.DataSyncTask) (*
return syncLog, nil 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 { 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 {
var data = make([]map[string]any, 0)
// 遍历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 { for _, record := range srcRes {
var rowData = make(map[string]any) var rowData = make(map[string]any)
// 遍历字段映射, target字段的值为src字段取值 // 遍历字段映射, target字段的值为src字段取值
@@ -265,18 +304,23 @@ func (app *dataSyncAppImpl) srcData2TargetDb(srcRes []map[string]any, fieldMap [
data = append(data, rowData) 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]) task.UpdFieldVal = srcDialect.GetDataConverter().FormatData(updFieldVal, updFieldType)
updFieldVal = srcDialect.FormatStrData(updFieldVal, updFieldType)
task.UpdFieldVal = updFieldVal
// 获取目标库字段数组 // 获取目标库字段数组
targetWrapColumns := make([]string, 0) targetWrapColumns := make([]string, 0)
// 获取源库字段数组 // 获取源库字段数组
srcColumns := make([]string, 0) srcColumns := make([]string, 0)
srcFieldTypes := make(map[string]dbi.DataType)
for _, item := range fieldMap { for _, item := range fieldMap {
targetField := item["target"] targetField := item["target"]
srcField := item["target"] srcField := item["target"]
srcFieldTypes[srcField] = srcDialect.GetDataConverter().GetDataType(srcColumnTypes[item["src"]])
targetWrapColumns = append(targetWrapColumns, targetDbConn.Info.Type.QuoteIdentifier(targetField)) targetWrapColumns = append(targetWrapColumns, targetDbConn.Info.Type.QuoteIdentifier(targetField))
srcColumns = append(srcColumns, srcField) srcColumns = append(srcColumns, srcField)
} }
@@ -286,7 +330,9 @@ func (app *dataSyncAppImpl) srcData2TargetDb(srcRes []map[string]any, fieldMap [
for _, record := range data { for _, record := range data {
rawValue := make([]any, 0) rawValue := make([]any, 0)
for _, column := range srcColumns { 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) values = append(values, rawValue)
} }
@@ -328,7 +374,7 @@ func (app *dataSyncAppImpl) endRunning(taskEntity *entity.DataSyncTask, log *ent
} }
func (app *dataSyncAppImpl) saveLog(log *entity.DataSyncLog) { func (app *dataSyncAppImpl) saveLog(log *entity.DataSyncLog) {
app.dataSyncLogRepo.Save(context.Background(), log) app.dbDataSyncLogRepo.Save(context.Background(), log)
} }
func (app *dataSyncAppImpl) InitCronJob() { 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) { 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...)
} }

View File

@@ -2,71 +2,131 @@ package application
import ( import (
"context" "context"
"errors"
"mayfly-go/internal/db/domain/entity" "mayfly-go/internal/db/domain/entity"
"mayfly-go/internal/db/domain/repository" "mayfly-go/internal/db/domain/repository"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/model" "mayfly-go/pkg/model"
"sync"
) )
func newDbRestoreApp(repositories *repository.Repositories, dbApp Db, scheduler *dbScheduler) (*DbRestoreApp, error) { type DbRestoreApp struct {
var jobs []*entity.DbRestore scheduler *dbScheduler `inject:"DbScheduler"`
if err := repositories.Restore.ListToDo(&jobs); err != nil { restoreRepo repository.DbRestore `inject:"DbRestoreRepo"`
return nil, err restoreHistoryRepo repository.DbRestoreHistory `inject:"DbRestoreHistoryRepo"`
} mutex sync.Mutex
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 { func (app *DbRestoreApp) Init() error {
restoreRepo repository.DbRestore var jobs []*entity.DbRestore
instanceRepo repository.Instance if err := app.restoreRepo.ListToDo(&jobs); err != nil {
backupHistoryRepo repository.DbBackupHistory return err
restoreHistoryRepo repository.DbRestoreHistory }
binlogHistoryRepo repository.DbBinlogHistory if err := app.scheduler.AddJob(context.Background(), jobs); err != nil {
dbApp Db return err
scheduler *dbScheduler }
return nil
} }
func (app *DbRestoreApp) Close() { func (app *DbRestoreApp) Close() {
app.scheduler.Close() app.scheduler.Close()
} }
func (app *DbRestoreApp) Create(ctx context.Context, job *entity.DbRestore) error { func (app *DbRestoreApp) Create(ctx context.Context, jobs any) error {
return app.scheduler.AddJob(ctx, true /* 保存到数据库 */, entity.DbJobTypeRestore, job) 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 { 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 { func (app *DbRestoreApp) Delete(ctx context.Context, jobId uint64) error {
// todo: 删除数据库恢复历史文件 // 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 { 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 { 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 分页获取数据库恢复任务 // 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...) 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 获取未配置定时恢复的数据库名称 // GetDbNamesWithoutRestore 获取未配置定时恢复的数据库名称
func (app *DbRestoreApp) GetDbNamesWithoutRestore(instanceId uint64, dbNames []string) ([]string, error) { func (app *DbRestoreApp) GetDbNamesWithoutRestore(instanceId uint64, dbNames []string) ([]string, error) {
return app.restoreRepo.GetDbNamesWithoutRestore(instanceId, dbNames) return app.restoreRepo.GetDbNamesWithoutRestore(instanceId, dbNames)

View File

@@ -4,10 +4,10 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"gorm.io/gorm"
"mayfly-go/internal/db/dbm/dbi" "mayfly-go/internal/db/dbm/dbi"
"mayfly-go/internal/db/domain/entity" "mayfly-go/internal/db/domain/entity"
"mayfly-go/internal/db/domain/repository" "mayfly-go/internal/db/domain/repository"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/runner" "mayfly-go/pkg/runner"
"reflect" "reflect"
"sync" "sync"
@@ -21,58 +21,34 @@ const (
type dbScheduler struct { type dbScheduler struct {
mutex sync.Mutex mutex sync.Mutex
runner *runner.Runner[entity.DbJob] runner *runner.Runner[entity.DbJob]
dbApp Db dbApp Db `inject:"DbApp"`
backupRepo repository.DbBackup backupRepo repository.DbBackup `inject:"DbBackupRepo"`
backupHistoryRepo repository.DbBackupHistory backupHistoryRepo repository.DbBackupHistory `inject:"DbBackupHistoryRepo"`
restoreRepo repository.DbRestore restoreRepo repository.DbRestore `inject:"DbRestoreRepo"`
restoreHistoryRepo repository.DbRestoreHistory restoreHistoryRepo repository.DbRestoreHistory `inject:"DbRestoreHistoryRepo"`
binlogRepo repository.DbBinlog binlogRepo repository.DbBinlog `inject:"DbBinlogRepo"`
binlogHistoryRepo repository.DbBinlogHistory binlogHistoryRepo repository.DbBinlogHistory `inject:"DbBinlogHistoryRepo"`
binlogTimes map[uint64]time.Time
} }
func newDbScheduler(repositories *repository.Repositories) (*dbScheduler, error) { func newDbScheduler() *dbScheduler {
scheduler := &dbScheduler{ scheduler := &dbScheduler{}
dbApp: dbApp,
backupRepo: repositories.Backup,
backupHistoryRepo: repositories.BackupHistory,
restoreRepo: repositories.Restore,
restoreHistoryRepo: repositories.RestoreHistory,
binlogRepo: repositories.Binlog,
binlogHistoryRepo: repositories.BinlogHistory,
}
scheduler.runner = runner.NewRunner[entity.DbJob](maxRunning, scheduler.runJob, scheduler.runner = runner.NewRunner[entity.DbJob](maxRunning, scheduler.runJob,
runner.WithScheduleJob[entity.DbJob](scheduler.scheduleJob), runner.WithScheduleJob[entity.DbJob](scheduler.scheduleJob),
runner.WithRunnableJob[entity.DbJob](scheduler.runnableJob), 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) { func (s *dbScheduler) scheduleJob(job entity.DbJob) (time.Time, error) {
return job.Schedule() 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 { func (s *dbScheduler) UpdateJob(ctx context.Context, job entity.DbJob) error {
s.mutex.Lock() s.mutex.Lock()
defer s.mutex.Unlock() defer s.mutex.Unlock()
if err := s.repo(job.GetJobType()).UpdateById(ctx, job); err != nil { _ = s.runner.Update(ctx, job)
return err
}
_ = s.runner.UpdateOrAdd(ctx, job)
return nil return nil
} }
@@ -80,28 +56,20 @@ func (s *dbScheduler) Close() {
s.runner.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() s.mutex.Lock()
defer s.mutex.Unlock() defer s.mutex.Unlock()
if saving {
if err := s.repo(jobType).AddJob(ctx, jobs); err != nil {
return err
}
}
reflectValue := reflect.ValueOf(jobs) reflectValue := reflect.ValueOf(jobs)
switch reflectValue.Kind() { switch reflectValue.Kind() {
case reflect.Array, reflect.Slice: case reflect.Array, reflect.Slice:
reflectLen := reflectValue.Len() reflectLen := reflectValue.Len()
for i := 0; i < reflectLen; i++ { for i := 0; i < reflectLen; i++ {
job := reflectValue.Index(i).Interface().(entity.DbJob) job := reflectValue.Index(i).Interface().(entity.DbJob)
job.SetJobType(jobType)
_ = s.runner.Add(ctx, job) _ = s.runner.Add(ctx, job)
} }
default: default:
job := jobs.(entity.DbJob) job := jobs.(entity.DbJob)
job.SetJobType(jobType)
_ = s.runner.Add(ctx, job) _ = s.runner.Add(ctx, job)
} }
return nil return nil
@@ -112,29 +80,16 @@ func (s *dbScheduler) RemoveJob(ctx context.Context, jobType entity.DbJobType, j
s.mutex.Lock() s.mutex.Lock()
defer s.mutex.Unlock() 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 return err
} }
_ = s.runner.Remove(ctx, entity.FormatJobKey(jobType, jobId))
return nil 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() s.mutex.Lock()
defer s.mutex.Unlock() 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) _ = s.runner.Add(ctx, job)
return nil return nil
} }
@@ -143,37 +98,19 @@ func (s *dbScheduler) DisableJob(ctx context.Context, jobType entity.DbJobType,
s.mutex.Lock() s.mutex.Lock()
defer s.mutex.Unlock() defer s.mutex.Unlock()
repo := s.repo(jobType) _ = s.runner.Remove(ctx, entity.FormatJobKey(jobType, jobId))
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())
return nil 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() s.mutex.Lock()
defer s.mutex.Unlock() 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) _ = s.runner.StartNow(ctx, job)
return nil 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() id, err := NewIncUUID()
if err != nil { if err != nil {
return err return err
@@ -185,19 +122,14 @@ func (s *dbScheduler) backupMysql(ctx context.Context, job entity.DbJob) error {
DbInstanceId: backup.DbInstanceId, DbInstanceId: backup.DbInstanceId,
DbName: backup.DbName, 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) binlogInfo, err := dbProgram.Backup(ctx, history)
if err != nil { if err != nil {
return err return err
} }
now := time.Now() now := time.Now()
name := backup.Name name := backup.DbName
if len(name) == 0 { if len(backup.Name) > 0 {
name = backup.DbName name = fmt.Sprintf("%s-%s", backup.DbName, backup.Name)
} }
history.Name = fmt.Sprintf("%s[%s]", name, now.Format(time.DateTime)) history.Name = fmt.Sprintf("%s[%s]", name, now.Format(time.DateTime))
history.CreateTime = now history.CreateTime = now
@@ -211,43 +143,59 @@ func (s *dbScheduler) backupMysql(ctx context.Context, job entity.DbJob) error {
return nil 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) restore := job.(*entity.DbRestore)
conn, err := s.dbApp.GetDbConnByInstanceId(restore.DbInstanceId)
if err != nil {
return err
}
dbProgram := conn.GetDialect().GetDbProgram()
if restore.PointInTime.Valid { if restore.PointInTime.Valid {
latestBinlogSequence, earliestBackupSequence := int64(-1), int64(-1) //if enabled, err := dbProgram.CheckBinlogEnabled(ctx); err != nil {
binlogHistory, ok, err := s.binlogHistoryRepo.GetLatestHistory(restore.DbInstanceId) // return err
if err != nil { //} else if !enabled {
return err // return errors.New("数据库未启用 BINLOG")
} //}
if ok { //if enabled, err := dbProgram.CheckBinlogRowFormat(ctx); err != nil {
latestBinlogSequence = binlogHistory.Sequence // return err
} else { //} else if !enabled {
backupHistory, ok, err := s.backupHistoryRepo.GetEarliestHistory(restore.DbInstanceId) // return errors.New("数据库未启用 BINLOG 行模式")
if err != nil { //}
return err //
} //latestBinlogSequence, earliestBackupSequence := int64(-1), int64(-1)
if !ok { //binlogHistory, ok, err := s.binlogHistoryRepo.GetLatestHistory(restore.DbInstanceId)
return nil //if err != nil {
} // return err
earliestBackupSequence = backupHistory.BinlogSequence //}
} //if ok {
binlogFiles, err := dbProgram.FetchBinlogs(ctx, true, earliestBackupSequence, latestBinlogSequence) // latestBinlogSequence = binlogHistory.Sequence
if err != nil { //} else {
return err // backupHistory, ok, err := s.backupHistoryRepo.GetEarliestHistory(restore.DbInstanceId)
} // if err != nil {
if err := s.binlogHistoryRepo.InsertWithBinlogFiles(ctx, restore.DbInstanceId, binlogFiles); 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 return err
} }
if err := s.restorePointInTime(ctx, dbProgram, restore); err != nil { if err := s.restorePointInTime(ctx, dbProgram, restore); err != nil {
return err return err
} }
} else { } 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 return err
} }
} }
@@ -262,76 +210,108 @@ func (s *dbScheduler) restoreMysql(ctx context.Context, job entity.DbJob) error
return nil return nil
} }
func (s *dbScheduler) runJob(ctx context.Context, job entity.DbJob) { //func (s *dbScheduler) updateLastStatus(ctx context.Context, job entity.DbJob) error {
job.SetLastStatus(entity.DbJobRunning, nil) // switch typ := job.GetJobType(); typ {
if err := s.repo(job.GetJobType()).UpdateLastStatus(ctx, job); err != nil { // case entity.DbJobTypeBackup:
logx.Errorf("failed to update job status: %v", err) // return s.backupRepo.UpdateLastStatus(ctx, job)
return // 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 { switch typ := job.GetJobType(); typ {
case entity.DbJobTypeBackup: case entity.DbJobTypeBackup:
errRun = s.backupMysql(ctx, job) return s.backupRepo.UpdateById(ctx, job)
case entity.DbJobTypeRestore: case entity.DbJobTypeRestore:
errRun = s.restoreMysql(ctx, job) return s.restoreRepo.UpdateById(ctx, job)
case entity.DbJobTypeBinlog: case entity.DbJobTypeBinlog:
errRun = s.fetchBinlogMysql(ctx, job) return s.binlogRepo.UpdateById(ctx, job)
default: default:
errRun = errors.New(fmt.Sprintf("无效的数据库任务类型: %v", typ)) return fmt.Errorf("无效的数据库任务类型: %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
} }
} }
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 maxCountByInstanceId = 4
const maxCountByDbName = 1 const maxCountByDbName = 1
var countByInstanceId, countByDbName int var countByInstanceId, countByDbName int
jobBase := job.GetJobBase()
for item, ok := next(); ok; item, ok = next() { for item, ok := next(); ok; item, ok = next() {
itemBase := item.GetJobBase() if job.GetInstanceId() == item.GetInstanceId() {
if jobBase.DbInstanceId == itemBase.DbInstanceId {
countByInstanceId++ countByInstanceId++
if countByInstanceId >= maxCountByInstanceId { if countByInstanceId >= maxCountByInstanceId {
return false return false, nil
} }
if relatedToBinlog(job.GetJobType()) { if relatedToBinlog(job.GetJobType()) {
// todo: 恢复数据库前触发 BINLOG 同步BINLOG 同步完成后才能恢复数据库 // todo: 恢复数据库前触发 BINLOG 同步BINLOG 同步完成后才能恢复数据库
if relatedToBinlog(item.GetJobType()) { if relatedToBinlog(item.GetJobType()) {
return false return false, nil
} }
} }
if job.GetDbName() == item.GetDbName() { if job.GetDbName() == item.GetDbName() {
countByDbName++ countByDbName++
if countByDbName >= maxCountByDbName { if countByDbName >= maxCountByDbName {
return false return false, nil
} }
} }
} }
} }
return true return true, nil
} }
func relatedToBinlog(typ entity.DbJobType) bool { func relatedToBinlog(typ entity.DbJobType) bool {
return typ == entity.DbJobTypeRestore || typ == entity.DbJobTypeBinlog 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) binlogHistory, err := s.binlogHistoryRepo.GetHistoryByTime(job.DbInstanceId, job.PointInTime.Time)
if err != nil { if err != nil {
return err 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 { if err != nil {
return err return err
} }
@@ -360,22 +340,63 @@ func (s *dbScheduler) restorePointInTime(ctx context.Context, program dbi.DbProg
TargetPosition: target.Position, TargetPosition: target.Position,
TargetTime: job.PointInTime.Time, 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 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 { func (s *dbScheduler) restoreBackupHistory(ctx context.Context, program dbi.DbProgram, backupHistory *entity.DbBackupHistory) (retErr error) {
backupHistory := &entity.DbBackupHistory{} ok, err := s.backupHistoryRepo.UpdateRestoring(true, backupHistory.Id)
if err := s.backupHistoryRepo.GetById(backupHistory, job.DbBackupHistoryId); err != nil { if err != nil {
return err 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) return program.RestoreBackupHistory(ctx, backupHistory.DbName, backupHistory.DbBackupId, backupHistory.Uuid)
} }
func (s *dbScheduler) fetchBinlogMysql(ctx context.Context, backup entity.DbJob) error { func (s *dbScheduler) fetchBinlog(ctx context.Context, dbProgram dbi.DbProgram, instanceId uint64, downloadLatestBinlogFile bool) error {
instanceId := backup.GetJobBase().DbInstanceId 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) latestBinlogSequence, earliestBackupSequence := int64(-1), int64(-1)
binlogHistory, ok, err := s.binlogHistoryRepo.GetLatestHistory(instanceId) binlogHistory, ok, err := s.binlogHistoryRepo.GetLatestHistory(instanceId)
if err != nil { if err != nil {
@@ -393,14 +414,9 @@ func (s *dbScheduler) fetchBinlogMysql(ctx context.Context, backup entity.DbJob)
} }
earliestBackupSequence = backupHistory.BinlogSequence earliestBackupSequence = backupHistory.BinlogSequence
} }
conn, err := s.dbApp.GetDbConnByInstanceId(instanceId) binlogFiles, err := dbProgram.FetchBinlogs(ctx, downloadLatestBinlogFile, earliestBackupSequence, latestBinlogSequence)
if err != nil { if err != nil {
return err return err
} }
dbProgram := conn.GetDialect().GetDbProgram() return s.binlogHistoryRepo.InsertWithBinlogFiles(ctx, instanceId, binlogFiles)
binlogFiles, err := dbProgram.FetchBinlogs(ctx, false, earliestBackupSequence, latestBinlogSequence)
if err == nil {
err = s.binlogHistoryRepo.InsertWithBinlogFiles(ctx, instanceId, binlogFiles)
}
return nil
} }

View File

@@ -14,8 +14,7 @@ type dbSqlAppImpl struct {
base.AppImpl[*entity.DbSql, repository.DbSql] base.AppImpl[*entity.DbSql, repository.DbSql]
} }
func newDbSqlApp(dbSqlRepo repository.DbSql) DbSql { // 注入DbSqlRepo
app := new(dbSqlAppImpl) func (d *dbSqlAppImpl) InjectDbSqlRepo(repo repository.DbSql) {
app.Repo = dbSqlRepo d.Repo = repo
return app
} }

View File

@@ -9,6 +9,7 @@ import (
"mayfly-go/internal/db/domain/repository" "mayfly-go/internal/db/domain/repository"
"mayfly-go/pkg/contextx" "mayfly-go/pkg/contextx"
"mayfly-go/pkg/errorx" "mayfly-go/pkg/errorx"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/model" "mayfly-go/pkg/model"
"mayfly-go/pkg/utils/jsonx" "mayfly-go/pkg/utils/jsonx"
"strconv" "strconv"
@@ -56,14 +57,8 @@ type DbSqlExec interface {
GetPageList(condition *entity.DbSqlExecQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) 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 { type dbSqlExecAppImpl struct {
dbSqlExecRepo repository.DbSqlExec dbSqlExecRepo repository.DbSqlExec `inject:"DbSqlExecRepo"`
} }
func createSqlExecRecord(ctx context.Context, execSqlReq *DbSqlExecReq) *entity.DbSqlExec { func createSqlExecRecord(ctx context.Context, execSqlReq *DbSqlExecReq) *entity.DbSqlExec {
@@ -93,11 +88,21 @@ func (d *dbSqlExecAppImpl) Exec(ctx context.Context, execSqlReq *DbSqlExecReq) (
// 如果配置为0则不校验分页参数 // 如果配置为0则不校验分页参数
maxCount := config.GetDbQueryMaxCount() maxCount := config.GetDbQueryMaxCount()
if maxCount != 0 { if maxCount != 0 {
if !strings.Contains(lowerSql, "limit") {
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("请完善分页信息后执行") return nil, errorx.NewBiz("请完善分页信息后执行")
} }
} }
} }
}
var execErr error var execErr error
if isSelect || strings.HasPrefix(lowerSql, "show") { if isSelect || strings.HasPrefix(lowerSql, "show") {
execRes, execErr = doRead(ctx, execSqlReq) execRes, execErr = doRead(ctx, execSqlReq)
@@ -165,7 +170,9 @@ func doSelect(ctx context.Context, selectStmt *sqlparser.Select, execSqlReq *DbS
len(strings.Split(selectExprsStr, ",")) > 1 { len(strings.Split(selectExprsStr, ",")) > 1 {
// 如果配置为0则不校验分页参数 // 如果配置为0则不校验分页参数
maxCount := config.GetDbQueryMaxCount() 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 limit := selectStmt.Limit
if limit == nil { if limit == nil {
return nil, errorx.NewBiz("请完善分页信息后执行") return nil, errorx.NewBiz("请完善分页信息后执行")
@@ -204,6 +211,9 @@ func doUpdate(ctx context.Context, update *sqlparser.Update, execSqlReq *DbSqlEx
tableStr := sqlparser.String(update.TableExprs) tableStr := sqlparser.String(update.TableExprs)
// 可能使用别名,故空格切割 // 可能使用别名,故空格切割
tableName := strings.Split(tableStr, " ")[0] tableName := strings.Split(tableStr, " ")[0]
if strings.Contains(tableName, ".") {
tableName = strings.Split(tableName, ".")[1]
}
where := sqlparser.String(update.Where) where := sqlparser.String(update.Where)
if len(where) == 0 { if len(where) == 0 {
return nil, errorx.NewBiz("SQL[%s]未执行. 请完善 where 条件后再执行", execSqlReq.Sql) 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 updateColumnsAndPrimaryKey := strings.Join(updateColumns, ",") + "," + primaryKey
// 查询要更新字段数据的旧值,以及主键值 // 查询要更新字段数据的旧值,以及主键值
selectSql := fmt.Sprintf("SELECT %s FROM %s %s LIMIT 200", updateColumnsAndPrimaryKey, tableStr, where) selectSql := fmt.Sprintf("SELECT %s FROM %s %s", updateColumnsAndPrimaryKey, tableStr, where)
_, res, err := dbConn.QueryContext(ctx, selectSql)
if err == nil { // WalkQuery查出最多200条数据
dbSqlExec.OldValue = jsonx.ToStr(res) maxRec := 200
} else { nowRec := 0
dbSqlExec.OldValue = err.Error() 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.Table = tableName
dbSqlExec.Type = entity.DbSqlExecTypeUpdate dbSqlExec.Type = entity.DbSqlExecTypeUpdate

View File

@@ -30,16 +30,15 @@ type Instance interface {
GetDatabases(entity *entity.DbInstance) ([]string, error) GetDatabases(entity *entity.DbInstance) ([]string, error)
} }
func newInstanceApp(instanceRepo repository.Instance) Instance {
app := new(instanceAppImpl)
app.Repo = instanceRepo
return app
}
type instanceAppImpl struct { type instanceAppImpl struct {
base.AppImpl[*entity.DbInstance, repository.Instance] base.AppImpl[*entity.DbInstance, repository.Instance]
} }
// 注入DbInstanceRepo
func (app *instanceAppImpl) InjectDbInstanceRepo(repo repository.Instance) {
app.Repo = repo
}
// GetPageList 分页获取数据库实例 // GetPageList 分页获取数据库实例
func (app *instanceAppImpl) GetPageList(condition *entity.InstanceQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) { 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...) 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) err := app.GetBy(oldInstance)
if instanceEntity.Id == 0 { if instanceEntity.Id == 0 {
if instanceEntity.Password == "" {
if instanceEntity.Type != string(dbi.DbTypeSqlite) && instanceEntity.Password == "" {
return errorx.NewBiz("密码不能为空") return errorx.NewBiz("密码不能为空")
} }
if err == nil { if err == nil {
return errorx.NewBiz("该数据库实例已存在") return errorx.NewBiz("该数据库实例已存在")
} }

View File

@@ -77,6 +77,11 @@ func (d *DbConn) WalkQueryRows(ctx context.Context, querySql string, walkFn Walk
return walkQueryRows(ctx, d.db, querySql, walkFn, args...) 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 // 执行 update, insert, delete建表等sql
// 返回影响条数和错误 // 返回影响条数和错误
func (d *DbConn) Exec(sql string, args ...any) (int64, error) { 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) 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() { func (d *DbConn) Close() {
if d.db != nil { if d.db != nil {
@@ -163,7 +173,12 @@ func walkQueryRows(ctx context.Context, db *sql.DB, selectSql string, walkFn Wal
// 这里表示一行所有列的值,用[]byte表示 // 这里表示一行所有列的值,用[]byte表示
values := make([][]byte, lenCols) values := make([][]byte, lenCols)
for k, colType := range colTypes { 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引用values把数据填充到[]byte里
scans[k] = &values[k] 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) rowData := make(map[string]any, lenCols)
// 把values中的数据复制到row中 // 把values中的数据复制到row中
for i, v := range values { 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 { if err = walkFn(rowData, cols); err != nil {
logx.Error("游标遍历查询结果集出错,退出遍历: %s", err.Error()) logx.Error("游标遍历查询结果集出错,退出遍历: %s", err.Error())

View File

@@ -8,6 +8,9 @@ import (
) )
type DbProgram interface { 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) Backup(ctx context.Context, backupHistory *entity.DbBackupHistory) (*entity.BinlogInfo, error)
FetchBinlogs(ctx context.Context, downloadLatestBinlogFile bool, earliestBackupSequence, latestBinlogSequence int64) ([]*entity.BinlogFile, 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 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) GetBinlogEventPositionAtOrAfterTime(ctx context.Context, binlogName string, targetTime time.Time) (position int64, parseErr error)
} }

View File

@@ -14,8 +14,11 @@ const (
DbTypeMysql DbType = "mysql" DbTypeMysql DbType = "mysql"
DbTypeMariadb DbType = "mariadb" DbTypeMariadb DbType = "mariadb"
DbTypePostgres DbType = "postgres" DbTypePostgres DbType = "postgres"
DbTypeGauss DbType = "gauss"
DbTypeDM DbType = "dm" DbTypeDM DbType = "dm"
DbTypeOracle DbType = "oracle" DbTypeOracle DbType = "oracle"
DbTypeSqlite DbType = "sqlite"
DbTypeMssql DbType = "mssql"
) )
func ToDbType(dbType string) DbType { func ToDbType(dbType string) DbType {
@@ -41,20 +44,33 @@ func (dbType DbType) QuoteIdentifier(name string) string {
switch dbType { switch dbType {
case DbTypeMysql, DbTypeMariadb: case DbTypeMysql, DbTypeMariadb:
return quoteIdentifier(name, "`") return quoteIdentifier(name, "`")
case DbTypePostgres: case DbTypePostgres, DbTypeGauss:
return quoteIdentifier(name, `"`) return quoteIdentifier(name, `"`)
case DbTypeMssql:
return fmt.Sprintf("[%s]", name)
default: default:
return quoteIdentifier(name, `"`) 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 { func (dbType DbType) QuoteLiteral(literal string) string {
switch dbType { switch dbType {
case DbTypeMysql, DbTypeMariadb: case DbTypeMysql, DbTypeMariadb:
literal = strings.ReplaceAll(literal, `\`, `\\`) literal = strings.ReplaceAll(literal, `\`, `\\`)
literal = strings.ReplaceAll(literal, `'`, `''`) literal = strings.ReplaceAll(literal, `'`, `''`)
return "'" + literal + "'" return "'" + literal + "'"
case DbTypePostgres: case DbTypePostgres, DbTypeGauss:
return pq.QuoteLiteral(literal) return pq.QuoteLiteral(literal)
default: default:
return pq.QuoteLiteral(literal) return pq.QuoteLiteral(literal)
@@ -65,7 +81,7 @@ func (dbType DbType) MetaDbName() string {
switch dbType { switch dbType {
case DbTypeMysql, DbTypeMariadb: case DbTypeMysql, DbTypeMariadb:
return "" return ""
case DbTypePostgres: case DbTypePostgres, DbTypeGauss:
return "postgres" return "postgres"
case DbTypeDM: case DbTypeDM:
return "" return ""
@@ -78,7 +94,7 @@ func (dbType DbType) Dialect() sqlparser.Dialect {
switch dbType { switch dbType {
case DbTypeMysql, DbTypeMariadb: case DbTypeMysql, DbTypeMariadb:
return sqlparser.MysqlDialect{} return sqlparser.MysqlDialect{}
case DbTypePostgres: case DbTypePostgres, DbTypeGauss:
return sqlparser.PostgresDialect{} return sqlparser.PostgresDialect{}
default: default:
return sqlparser.PostgresDialect{} return sqlparser.PostgresDialect{}
@@ -93,6 +109,11 @@ func quoteIdentifier(name, quoter string) string {
return quoter + strings.Replace(name, quoter, quoter+quoter, -1) + quoter 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 { func (dbType DbType) StmtSetForeignKeyChecks(check bool) string {
switch dbType { switch dbType {
case DbTypeMysql, DbTypeMariadb: case DbTypeMysql, DbTypeMariadb:
@@ -101,7 +122,7 @@ func (dbType DbType) StmtSetForeignKeyChecks(check bool) string {
} else { } else {
return "SET FOREIGN_KEY_CHECKS = 0;\n" return "SET FOREIGN_KEY_CHECKS = 0;\n"
} }
case DbTypePostgres: case DbTypePostgres, DbTypeGauss:
// not currently supported postgres // not currently supported postgres
return "" return ""
default: default:
@@ -113,7 +134,7 @@ func (dbType DbType) StmtUseDatabase(dbName string) string {
switch dbType { switch dbType {
case DbTypeMysql, DbTypeMariadb: case DbTypeMysql, DbTypeMariadb:
return fmt.Sprintf("USE %s;\n", dbType.QuoteIdentifier(dbName)) return fmt.Sprintf("USE %s;\n", dbType.QuoteIdentifier(dbName))
case DbTypePostgres: case DbTypePostgres, DbTypeGauss:
// not currently supported postgres // not currently supported postgres
return "" return ""
default: default:

View File

@@ -42,7 +42,8 @@ type Column struct {
ColumnName string `json:"columnName"` // 列名 ColumnName string `json:"columnName"` // 列名
ColumnType string `json:"columnType"` // 列类型 ColumnType string `json:"columnType"` // 列类型
ColumnComment string `json:"columnComment"` // 列备注 ColumnComment string `json:"columnComment"` // 列备注
ColumnKey string `json:"columnKey"` // 是否为主键逐渐的话值钱为PRI IsPrimaryKey bool `json:"isPrimaryKey"` // 是否为主键
IsIdentity bool `json:"isIdentity"` // 是否自增
ColumnDefault string `json:"columnDefault"` // 默认值 ColumnDefault string `json:"columnDefault"` // 默认值
Nullable string `json:"nullable"` // 是否可为null Nullable string `json:"nullable"` // 是否可为null
NumScale string `json:"numScale"` // 小数点 NumScale string `json:"numScale"` // 小数点
@@ -56,12 +57,33 @@ type Index struct {
IndexType string `json:"indexType"` // 索引类型 IndexType string `json:"indexType"` // 索引类型
IndexComment string `json:"indexComment"` // 备注 IndexComment string `json:"indexComment"` // 备注
SeqInIndex int `json:"seqInIndex"` 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 { type Dialect interface {
// 获取数据库服务实例信息 // 获取数据库服务实例信息
GetDbServer() (*DbServer, error) GetDbServer() (*DbServer, error)
@@ -75,7 +97,7 @@ type Dialect interface {
GetColumns(tableNames ...string) ([]Column, error) GetColumns(tableNames ...string) ([]Column, error)
// 获取表主键字段名,没有主键标识则默认第一个字段 // 获取表主键字段名,没有主键标识则默认第一个字段
GetPrimaryKey(tablename string) (string, error) GetPrimaryKey(tableName string) (string, error)
// 获取表索引信息 // 获取表索引信息
GetTableIndex(tableName string) ([]Index, error) GetTableIndex(tableName string) ([]Index, error)
@@ -83,9 +105,6 @@ type Dialect interface {
// 获取建表ddl // 获取建表ddl
GetTableDDL(tableName string) (string, error) GetTableDDL(tableName string) (string, error)
// WalkTableRecord 遍历指定表的数据
WalkTableRecord(tableName string, walkFn WalkQueryRowsFunc) error
GetSchemas() ([]string, error) GetSchemas() ([]string, error)
// GetDbProgram 获取数据库程序模块,用于数据库备份与恢复 // GetDbProgram 获取数据库程序模块,用于数据库备份与恢复
@@ -94,9 +113,10 @@ type Dialect interface {
// 批量保存数据 // 批量保存数据
BatchInsert(tx *sql.Tx, tableName string, columns []string, values [][]any) (int64, error) 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操作 ------------------------- // ------------------------- 元数据sql操作 -------------------------

View File

@@ -2,6 +2,20 @@ package dbi
import "database/sql" 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等 // 数据库元信息获取如获取sql.DB、Dialect等
type Meta interface { type Meta interface {
// 根据数据库信息获取sql.DB // 根据数据库信息获取sql.DB

View File

@@ -36,7 +36,7 @@ ORDER BY a.object_name
select select
a.index_name as INDEX_NAME, a.index_name as INDEX_NAME,
a.index_type as INDEX_TYPE, 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, indexdef(b.object_id,1) as INDEX_DEF,
c.column_name as COLUMN_NAME, c.column_name as COLUMN_NAME,
c.column_position as SEQ_IN_INDEX, c.column_position as SEQ_IN_INDEX,
@@ -64,22 +64,26 @@ select a.table_name
b.comments as COLUMN_COMMENT, b.comments as COLUMN_COMMENT,
a.data_default as COLUMN_DEFAULT, a.data_default as COLUMN_DEFAULT,
a.data_scale as NUM_SCALE, 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 from all_tab_columns a
left join user_col_comments b 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 on b.owner = (SELECT SF_GET_SCHEMA_NAME_BY_ID(CURRENT_SCHID))
a.column_name = b.column_name and b.table_name = a.table_name
left join (select b.owner, b.table_name, a.name COL_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, from SYS.SYSCOLUMNS a,
all_tables b, SYS.all_tables b,
sys.sysobjects c, SYS.SYSOBJECTS c
sys.sysobjects d
where a.INFO2 & 0x01 = 0x01 where a.INFO2 & 0x01 = 0x01
and a.id=c.id and d.type$ = 'SCH' and d.id = c.schid and a.ID = c.ID
and b.owner = (SELECT SF_GET_SCHEMA_NAME_BY_ID(CURRENT_SCHID)) and c.NAME = b.TABLE_NAME) t
and c.schid = ( select id from sys.sysobjects where type$ = 'SCH' and name = (SELECT SF_GET_SCHEMA_NAME_BY_ID(CURRENT_SCHID))) on t.table_name = a.table_name and t.owner = a.owner
and c.name = b.table_name) t left join (select uc.OWNER, uic.column_name, uic.table_name, uc.constraint_type
on t.table_name = a.table_name 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)) where a.owner = (SELECT SF_GET_SCHEMA_NAME_BY_ID(CURRENT_SCHID))
and a.table_name in (%s) and a.table_name in (%s)
order by a.table_name, a.column_id order by a.table_name,
a.column_id

View 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;

View File

@@ -30,7 +30,7 @@ SELECT
index_name indexName, index_name indexName,
column_name columnName, column_name columnName,
index_type indexType, index_type indexType,
non_unique nonUnique, IF(non_unique, 0, 1) isUnique,
SEQ_IN_INDEX seqInIndex, SEQ_IN_INDEX seqInIndex,
INDEX_COMMENT indexComment INDEX_COMMENT indexComment
FROM FROM
@@ -46,24 +46,25 @@ ORDER BY
SEQ_IN_INDEX asc SEQ_IN_INDEX asc
--------------------------------------- ---------------------------------------
--MYSQL_COLUMN_MA 列信息元数据 --MYSQL_COLUMN_MA 列信息元数据
SELECT SELECT table_name tableName,
table_name tableName,
column_name columnName, column_name columnName,
column_type columnType, column_type columnType,
column_default columnDefault, column_default columnDefault,
column_comment columnComment, column_comment columnComment,
column_key columnKey, CASE
extra extra, 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, is_nullable nullable,
NUMERIC_SCALE numScale NUMERIC_SCALE numScale
from FROM information_schema.COLUMNS
information_schema.columns WHERE table_schema = (SELECT DATABASE())
WHERE AND table_name IN (%s)
table_schema = ( ORDER BY table_name,
SELECT
database ()
)
AND table_name in (%s)
ORDER BY
tableName,
ordinal_position ordinal_position

View File

@@ -21,9 +21,9 @@ ORDER BY a.TABLE_NAME
SELECT ai.INDEX_NAME AS INDEX_NAME, SELECT ai.INDEX_NAME AS INDEX_NAME,
ai.INDEX_TYPE AS INDEX_TYPE, ai.INDEX_TYPE AS INDEX_TYPE,
CASE CASE
WHEN ai.uniqueness = 'UNIQUE' THEN 'NO' WHEN ai.uniqueness = 'UNIQUE' THEN 1
ELSE 'YES' ELSE 0
END AS NON_UNIQUE, END AS IS_UNIQUE,
(SELECT LISTAGG(column_name, ', ') WITHIN GROUP (ORDER BY column_position) (SELECT LISTAGG(column_name, ', ') WITHIN GROUP (ORDER BY column_position)
FROM ALL_IND_COLUMNS aic FROM ALL_IND_COLUMNS aic
WHERE aic.INDEX_NAME = ai.INDEX_NAME WHERE aic.INDEX_NAME = ai.INDEX_NAME
@@ -53,9 +53,8 @@ SELECT a.TABLE_NAME as TABLE_NAME,
b.COMMENTS as COLUMN_COMMENT, b.COMMENTS as COLUMN_COMMENT,
a.DATA_DEFAULT as COLUMN_DEFAULT, a.DATA_DEFAULT as COLUMN_DEFAULT,
a.DATA_SCALE as NUM_SCALE, a.DATA_SCALE as NUM_SCALE,
CASE CASE WHEN d.pri IS NOT NULL THEN 1 ELSE 0 END as IS_PRIMARY_KEY,
WHEN d.pri IS NOT NULL THEN 'PRI' CASE WHEN a.IDENTITY_COLUMN = 'YES' THEN 1 ELSE 0 END as IS_IDENTITY
END as COLUMN_KEY
FROM all_tab_columns a FROM all_tab_columns a
LEFT JOIN all_col_comments b 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 on a.OWNER = b.OWNER AND a.TABLE_NAME = b.TABLE_NAME AND a.COLUMN_NAME = b.COLUMN_NAME

View File

@@ -35,7 +35,7 @@ order by c.relname
SELECT SELECT
indexname AS "indexName", indexname AS "indexName",
'BTREE' AS "IndexType", '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", obj_description(b.oid, 'pg_class') AS "indexComment",
indexdef AS "indexDef", indexdef AS "indexDef",
c.attname AS "columnName", c.attname AS "columnName",
@@ -47,18 +47,21 @@ WHERE a.schemaname = (select current_schema())
AND a.tablename = '%s'; AND a.tablename = '%s';
--------------------------------------- ---------------------------------------
--PGSQL_COLUMN_MA 表列信息 --PGSQL_COLUMN_MA 表列信息
SELECT SELECT a.table_name AS "tableName",
table_name AS "tableName", a.column_name AS "columnName",
column_name AS "columnName", a.is_nullable AS "nullable",
is_nullable AS "nullable",
case when character_maximum_length > 0 then concat(udt_name, '(',character_maximum_length,')') else udt_name end AS "columnType", case when character_maximum_length > 0 then concat(udt_name, '(',character_maximum_length,')') else udt_name end AS "columnType",
column_default as "columnDefault", a.column_default as "columnDefault",
numeric_scale AS "numScale", a.numeric_scale AS "numScale",
case when column_default like 'nextval%%' then 'PRI' else '' end "columnKey", case when a.column_default like 'nextval%%' then 1 else 0 end "isIdentity",
col_description((table_schema || '.' || table_name)::regclass, ordinal_position) AS "columnComment" case when b.column_name is not null then 1 else 0 end "isPrimaryKey",
FROM information_schema.columns col_description((a.table_schema || '.' || a.table_name)::regclass, a.ordinal_position) AS "columnComment"
WHERE table_schema = (select current_schema()) and table_name in (%s) FROM information_schema.columns a
order by table_name, ordinal_position 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函数 --PGSQL_TABLE_DDL_FUNC 表ddl函数
CREATE OR REPLACE FUNCTION showcreatetable(namespace character varying, tablename character varying) CREATE OR REPLACE FUNCTION showcreatetable(namespace character varying, tablename character varying)

View 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

View File

@@ -4,10 +4,12 @@ import (
"fmt" "fmt"
"mayfly-go/internal/common/consts" "mayfly-go/internal/common/consts"
"mayfly-go/internal/db/dbm/dbi" "mayfly-go/internal/db/dbm/dbi"
"mayfly-go/internal/db/dbm/dm" _ "mayfly-go/internal/db/dbm/dm"
"mayfly-go/internal/db/dbm/mysql" _ "mayfly-go/internal/db/dbm/mssql"
"mayfly-go/internal/db/dbm/oracle" _ "mayfly-go/internal/db/dbm/mysql"
"mayfly-go/internal/db/dbm/postgres" _ "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/internal/machine/mcm"
"mayfly-go/pkg/cache" "mayfly-go/pkg/cache"
"mayfly-go/pkg/logx" "mayfly-go/pkg/logx"
@@ -38,21 +40,6 @@ func init() {
var mutex sync.Mutex 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进行连接并缓存 // 从缓存中获取数据库连接信息若缓存中不存在则会使用回调函数获取dbInfo进行连接并缓存
func GetDbConn(dbId uint64, database string, getDbInfo func() (*dbi.DbInfo, error)) (*dbi.DbConn, error) { func GetDbConn(dbId uint64, database string, getDbInfo func() (*dbi.DbInfo, error)) (*dbi.DbConn, error) {
connId := dbi.GetDbConnId(dbId, database) connId := dbi.GetDbConnId(dbId, database)
@@ -89,7 +76,7 @@ func GetDbConn(dbId uint64, database string, getDbInfo func() (*dbi.DbInfo, erro
// 使用指定dbInfo信息进行连接 // 使用指定dbInfo信息进行连接
func Conn(di *dbi.DbInfo) (*dbi.DbConn, error) { func Conn(di *dbi.DbInfo) (*dbi.DbConn, error) {
return di.Conn(getDbMetaByType(di.Type)) return di.Conn(dbi.GetMeta(di.Type))
} }
// 根据实例id获取连接 // 根据实例id获取连接

View File

@@ -1,17 +1,20 @@
package dm package dm
import ( import (
"context"
"database/sql" "database/sql"
"fmt" "fmt"
"mayfly-go/internal/db/dbm/dbi" "mayfly-go/internal/db/dbm/dbi"
"mayfly-go/pkg/errorx" "mayfly-go/pkg/errorx"
"mayfly-go/pkg/logx" "mayfly-go/pkg/logx"
"mayfly-go/pkg/utils/anyx" "mayfly-go/pkg/utils/anyx"
"mayfly-go/pkg/utils/collx"
"mayfly-go/pkg/utils/stringx"
"regexp" "regexp"
"strings" "strings"
"time" "time"
"github.com/kanzihuang/vitess/go/vt/sqlparser"
_ "gitee.com/chunanyong/dm" _ "gitee.com/chunanyong/dm"
) )
@@ -67,7 +70,7 @@ func (dd *DMDialect) GetTables() ([]dbi.Table, error) {
tables := make([]dbi.Table, 0) tables := make([]dbi.Table, 0)
for _, re := range res { for _, re := range res {
tables = append(tables, dbi.Table{ tables = append(tables, dbi.Table{
TableName: re["TABLE_NAME"].(string), TableName: anyx.ConvString(re["TABLE_NAME"]),
TableComment: anyx.ConvString(re["TABLE_COMMENT"]), TableComment: anyx.ConvString(re["TABLE_COMMENT"]),
CreateTime: anyx.ConvString(re["CREATE_TIME"]), CreateTime: anyx.ConvString(re["CREATE_TIME"]),
TableRows: anyx.ConvInt(re["TABLE_ROWS"]), 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) { func (dd *DMDialect) GetColumns(tableNames ...string) ([]dbi.Column, error) {
tableName := "" dbType := dd.dc.Info.Type
for i := 0; i < len(tableNames); i++ { tableName := strings.Join(collx.ArrayMap[string, string](tableNames, func(val string) string {
if i != 0 { return fmt.Sprintf("'%s'", dbType.RemoveQuote(val))
tableName = tableName + ", " }), ",")
}
tableName = tableName + "'" + tableNames[i] + "'"
}
_, res, err := dd.dc.Query(fmt.Sprintf(dbi.GetLocalSql(DM_META_FILE, DM_COLUMN_MA_KEY), tableName)) _, res, err := dd.dc.Query(fmt.Sprintf(dbi.GetLocalSql(DM_META_FILE, DM_COLUMN_MA_KEY), tableName))
if err != nil { if err != nil {
@@ -96,12 +96,13 @@ func (dd *DMDialect) GetColumns(tableNames ...string) ([]dbi.Column, error) {
columns := make([]dbi.Column, 0) columns := make([]dbi.Column, 0)
for _, re := range res { for _, re := range res {
columns = append(columns, dbi.Column{ columns = append(columns, dbi.Column{
TableName: re["TABLE_NAME"].(string), TableName: anyx.ConvString(re["TABLE_NAME"]),
ColumnName: re["COLUMN_NAME"].(string), ColumnName: anyx.ConvString(re["COLUMN_NAME"]),
ColumnType: anyx.ConvString(re["COLUMN_TYPE"]), ColumnType: anyx.ConvString(re["COLUMN_TYPE"]),
ColumnComment: anyx.ConvString(re["COLUMN_COMMENT"]), ColumnComment: anyx.ConvString(re["COLUMN_COMMENT"]),
Nullable: anyx.ConvString(re["NULLABLE"]), 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"]), ColumnDefault: anyx.ConvString(re["COLUMN_DEFAULT"]),
NumScale: anyx.ConvString(re["NUM_SCALE"]), NumScale: anyx.ConvString(re["NUM_SCALE"]),
}) })
@@ -118,7 +119,7 @@ func (dd *DMDialect) GetPrimaryKey(tablename string) (string, error) {
return "", errorx.NewBiz("[%s] 表不存在", tablename) return "", errorx.NewBiz("[%s] 表不存在", tablename)
} }
for _, v := range columns { for _, v := range columns {
if v.ColumnKey == "PRI" { if v.IsPrimaryKey {
return v.ColumnName, nil return v.ColumnName, nil
} }
} }
@@ -136,11 +137,11 @@ func (dd *DMDialect) GetTableIndex(tableName string) ([]dbi.Index, error) {
indexs := make([]dbi.Index, 0) indexs := make([]dbi.Index, 0)
for _, re := range res { for _, re := range res {
indexs = append(indexs, dbi.Index{ indexs = append(indexs, dbi.Index{
IndexName: re["INDEX_NAME"].(string), IndexName: anyx.ConvString(re["INDEX_NAME"]),
ColumnName: anyx.ConvString(re["COLUMN_NAME"]), ColumnName: anyx.ConvString(re["COLUMN_NAME"]),
IndexType: anyx.ConvString(re["INDEX_TYPE"]), IndexType: anyx.ConvString(re["INDEX_TYPE"]),
IndexComment: anyx.ConvString(re["INDEX_COMMENT"]), 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"]), SeqInIndex: anyx.ConvInt(re["SEQ_IN_INDEX"]),
}) })
} }
@@ -232,10 +233,6 @@ func (dd *DMDialect) GetTableDDL(tableName string) (string, error) {
return builder.String(), nil 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 // 获取DM当前连接的库可访问的schemaNames
func (dd *DMDialect) GetSchemas() ([]string, error) { func (dd *DMDialect) GetSchemas() ([]string, error) {
sql := dbi.GetLocalSql(DM_META_FILE, DM_DB_SCHEMAS) sql := dbi.GetLocalSql(DM_META_FILE, DM_DB_SCHEMAS)
@@ -255,24 +252,16 @@ func (dd *DMDialect) GetDbProgram() dbi.DbProgram {
panic("implement me") panic("implement me")
} }
func (dd *DMDialect) GetDataType(dbColumnType string) dbi.DataType { var (
if regexp.MustCompile(`(?i)int|double|float|number|decimal|byte|bit`).MatchString(dbColumnType) { // 数字类型
return dbi.DataTypeNumber numberRegexp = regexp.MustCompile(`(?i)int|double|float|number|decimal|byte|bit`)
}
// 日期时间类型 // 日期时间类型
if regexp.MustCompile(`(?i)datetime|timestamp`).MatchString(dbColumnType) { datetimeRegexp = regexp.MustCompile(`(?i)datetime|timestamp`)
return dbi.DataTypeDateTime
}
// 日期类型 // 日期类型
if regexp.MustCompile(`(?i)date`).MatchString(dbColumnType) { dateRegexp = regexp.MustCompile(`(?i)date`)
return dbi.DataTypeDate
}
// 时间类型 // 时间类型
if regexp.MustCompile(`(?i)time`).MatchString(dbColumnType) { timeRegexp = regexp.MustCompile(`(?i)time`)
return dbi.DataTypeTime )
}
return dbi.DataTypeString
}
func (dd *DMDialect) BatchInsert(tx *sql.Tx, tableName string, columns []string, values [][]any) (int64, error) { func (dd *DMDialect) BatchInsert(tx *sql.Tx, tableName string, columns []string, values [][]any) (int64, error) {
// 执行批量insert sql // 执行批量insert sql
@@ -299,17 +288,86 @@ func (dd *DMDialect) BatchInsert(tx *sql.Tx, tableName string, columns []string,
return int64(effRows), nil 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 { switch dataType {
case dbi.DataTypeDateTime: // "2024-01-02T22:08:22.275697+08:00" 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) return res.Format(time.DateTime)
case dbi.DataTypeDate: // "2024-01-02T00:00:00+08:00" 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) return res.Format(time.DateOnly)
case dbi.DataTypeTime: // "0000-01-01T22:08:22.275688+08:00" 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 res.Format(time.TimeOnly)
} }
return str
}
func (dd *DataConverter) ParseData(dbColumnValue any, dataType dbi.DataType) any {
return dbColumnValue 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
}

View File

@@ -4,20 +4,12 @@ import (
"database/sql" "database/sql"
"fmt" "fmt"
"mayfly-go/internal/db/dbm/dbi" "mayfly-go/internal/db/dbm/dbi"
"net/url"
"strings" "strings"
"sync"
) )
var ( func init() {
meta dbi.Meta dbi.Register(dbi.DbTypeDM, new(DmMeta))
once sync.Once
)
func GetMeta() dbi.Meta {
once.Do(func() {
meta = new(DmMeta)
})
return meta
} }
type DmMeta struct { type DmMeta struct {
@@ -31,10 +23,12 @@ func (md *DmMeta) GetSqlDb(d *dbi.DbInfo) (*sql.DB, error) {
// dm database可以使用db/schema表示方便连接指定schema, 若不存在schema则使用默认schema // dm database可以使用db/schema表示方便连接指定schema, 若不存在schema则使用默认schema
ss := strings.Split(db, "/") ss := strings.Split(db, "/")
if len(ss) > 1 { 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 { } else {
dbParam = db dbParam = db + "?escapeProcess=true"
} }
} else {
dbParam = "?escapeProcess=true"
} }
err := d.IfUseSshTunnelChangeIpPort() err := d.IfUseSshTunnelChangeIpPort()
@@ -42,7 +36,7 @@ func (md *DmMeta) GetSqlDb(d *dbi.DbInfo) (*sql.DB, error) {
return nil, err 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) return sql.Open(driverName, dsn)
} }

View 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