mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-23 01:20:25 +08:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
252fc553f2 | ||
|
|
ac2ceed3f9 | ||
|
|
3f828cc5b0 | ||
|
|
fc1b9ef35d | ||
|
|
d0b71a1c40 | ||
|
|
a743a6a05a | ||
|
|
0e6b9713ce | ||
|
|
b9afbc764d | ||
|
|
923e183a67 | ||
|
|
7e9a381641 | ||
|
|
bed95254d0 | ||
|
|
e4d13f3377 | ||
|
|
d530365ef9 | ||
|
|
070d4ea104 | ||
|
|
3fc86f0fae | ||
|
|
3b77ab2727 | ||
|
|
76cb991282 | ||
|
|
9efd20f1b9 | ||
|
|
de5b9e46d3 | ||
|
|
f27d3d200f | ||
|
|
f4a64b96a9 | ||
|
|
9a59749763 | ||
|
|
b017b902f8 | ||
|
|
7c53353c60 |
@@ -5,7 +5,7 @@ WORKDIR /mayfly
|
|||||||
|
|
||||||
COPY mayfly_go_web .
|
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
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
### 介绍
|
### 介绍
|
||||||
|
|
||||||
web 版 **linux(终端[终端回放] 文件 脚本 进程 计划任务)、数据库(mysql postgres oracle 达梦 高斯)、redis(单机 哨兵 集群)、mongo 统一管理操作平台**
|
web 版 **linux(终端[终端回放] 文件 脚本 进程 计划任务)、数据库(mysql postgres oracle 达梦 高斯 sqlite)、redis(单机 哨兵 集群)、mongo 统一管理操作平台**
|
||||||
|
|
||||||
### 开发语言与主要框架
|
### 开发语言与主要框架
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2021 lyt-Top
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
"countup.js": "^2.7.0",
|
"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
@@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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();
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
155
mayfly_go_web/src/views/ops/db/DbBackupHistoryList.vue
Normal file
155
mayfly_go_web/src/views/ops/db/DbBackupHistoryList.vue
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
<template>
|
||||||
|
<div class="db-backup-history">
|
||||||
|
<page-table
|
||||||
|
height="100%"
|
||||||
|
ref="pageTableRef"
|
||||||
|
:page-api="dbApi.getDbBackupHistories"
|
||||||
|
:show-selection="true"
|
||||||
|
v-model:selection-data="state.selectedData"
|
||||||
|
:searchItems="searchItems"
|
||||||
|
:before-query-fn="beforeQueryFn"
|
||||||
|
v-model:query-form="query"
|
||||||
|
:columns="columns"
|
||||||
|
>
|
||||||
|
<template #dbSelect>
|
||||||
|
<el-select v-model="query.dbName" placeholder="请选择数据库" style="width: 200px" filterable clearable>
|
||||||
|
<el-option v-for="item in props.dbNames" :key="item" :label="`${item}`" :value="item"> </el-option>
|
||||||
|
</el-select>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #tableHeader>
|
||||||
|
<el-button type="primary" icon="back" @click="restoreDbBackupHistory(null)">立即恢复</el-button>
|
||||||
|
<el-button type="danger" icon="delete" @click="deleteDbBackupHistory(null)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #action="{ data }">
|
||||||
|
<div>
|
||||||
|
<el-button @click="restoreDbBackupHistory(data)" type="primary" link>立即恢复</el-button>
|
||||||
|
<el-button @click="deleteDbBackupHistory(data)" type="danger" link>删除</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</page-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { toRefs, reactive, Ref, ref } from 'vue';
|
||||||
|
import { dbApi } from './api';
|
||||||
|
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||||
|
import { TableColumn } from '@/components/pagetable';
|
||||||
|
import { SearchItem } from '@/components/SearchForm';
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
|
||||||
|
const pageTableRef: Ref<any> = ref(null);
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
dbId: {
|
||||||
|
type: [Number],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
dbNames: {
|
||||||
|
type: [Array<String>],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchItems = [SearchItem.slot('dbName', '数据库名称', 'dbSelect')];
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
TableColumn.new('dbName', '数据库名称'),
|
||||||
|
TableColumn.new('name', '备份名称'),
|
||||||
|
TableColumn.new('createTime', '创建时间').isTime(),
|
||||||
|
TableColumn.new('lastResult', '恢复结果'),
|
||||||
|
TableColumn.new('lastTime', '恢复时间').isTime(),
|
||||||
|
TableColumn.new('action', '操作').isSlot().setMinWidth(160).fixedRight(),
|
||||||
|
];
|
||||||
|
|
||||||
|
const emptyQuery = {
|
||||||
|
dbId: 0,
|
||||||
|
dbName: '',
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
data: [],
|
||||||
|
total: 0,
|
||||||
|
query: emptyQuery,
|
||||||
|
/**
|
||||||
|
* 选中的数据
|
||||||
|
*/
|
||||||
|
selectedData: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { query } = toRefs(state);
|
||||||
|
|
||||||
|
const beforeQueryFn = (query: any) => {
|
||||||
|
query.dbId = props.dbId;
|
||||||
|
return query;
|
||||||
|
};
|
||||||
|
|
||||||
|
const search = async () => {
|
||||||
|
await pageTableRef.value.search();
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteDbBackupHistory = async (data: any) => {
|
||||||
|
let backupHistoryId: string;
|
||||||
|
if (data) {
|
||||||
|
backupHistoryId = data.id;
|
||||||
|
} else if (state.selectedData.length > 0) {
|
||||||
|
backupHistoryId = state.selectedData.map((x: any) => x.id).join(' ');
|
||||||
|
} else {
|
||||||
|
ElMessage.error('请选择需要删除的数据库备份历史');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await ElMessageBox.confirm(`确定删除 “数据库备份历史” 吗?`, '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
});
|
||||||
|
await dbApi.deleteDbBackupHistory.request({ dbId: props.dbId, backupHistoryId: backupHistoryId });
|
||||||
|
await search();
|
||||||
|
ElMessage.success('删除成功');
|
||||||
|
};
|
||||||
|
|
||||||
|
const restoreDbBackupHistory = async (data: any) => {
|
||||||
|
let backupHistoryId: string;
|
||||||
|
if (data) {
|
||||||
|
backupHistoryId = data.id;
|
||||||
|
} else if (state.selectedData.length > 0) {
|
||||||
|
const pluralDbNames: string[] = [];
|
||||||
|
const dbNames: Map<string, boolean> = new Map();
|
||||||
|
state.selectedData.forEach((item: any) => {
|
||||||
|
if (!dbNames.has(item.dbName)) {
|
||||||
|
dbNames.set(item.dbName, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!dbNames.get(item.dbName)) {
|
||||||
|
dbNames.set(item.dbName, true);
|
||||||
|
pluralDbNames.push(item.dbName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (pluralDbNames.length > 0) {
|
||||||
|
ElMessage.error('多次选择相同数据库:' + pluralDbNames.join(', '));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
backupHistoryId = state.selectedData.map((x: any) => x.id).join(' ');
|
||||||
|
} else {
|
||||||
|
ElMessage.error('请选择需要恢复的数据库备份历史');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await ElMessageBox.confirm(`确定从 “数据库备份历史” 中恢复数据库吗?`, '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
});
|
||||||
|
|
||||||
|
await dbApi.restoreDbBackupHistory.request({
|
||||||
|
dbId: props.dbId,
|
||||||
|
backupHistoryId: backupHistoryId,
|
||||||
|
});
|
||||||
|
await search();
|
||||||
|
ElMessage.success('成功创建数据库恢复任务');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss"></style>
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
<el-button type="primary" icon="plus" @click="createDbBackup()">添加</el-button>
|
<el-button type="primary" icon="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>
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,19 +364,20 @@ 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);
|
||||||
});
|
});
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前操作的数据库信息
|
* 获取当前操作的数据库信息
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 要实时响应,故需要用索引改变数据才生效
|
// 要实时响应,故需要用索引改变数据才生效
|
||||||
|
|||||||
@@ -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']);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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[] = [];
|
||||||
|
|||||||
17
mayfly_go_web/src/views/ops/db/dialect/gauss_dialect.ts
Normal file
17
mayfly_go_web/src/views/ops/db/dialect/gauss_dialect.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { PostgresqlDialect } from '@/views/ops/db/dialect/postgres_dialect';
|
||||||
|
import { DialectInfo } from '@/views/ops/db/dialect/index';
|
||||||
|
|
||||||
|
let gsDialectInfo: DialectInfo;
|
||||||
|
export class GaussDialect extends PostgresqlDialect {
|
||||||
|
getInfo(): DialectInfo {
|
||||||
|
if (gsDialectInfo) {
|
||||||
|
return gsDialectInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
gsDialectInfo = {} as DialectInfo;
|
||||||
|
Object.assign(gsDialectInfo, super.getInfo());
|
||||||
|
gsDialectInfo.icon = 'iconfont icon-gauss';
|
||||||
|
gsDialectInfo.name = 'GaussDB';
|
||||||
|
return gsDialectInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,9 @@ import { PostgresqlDialect } from './postgres_dialect';
|
|||||||
import { DMDialect } from '@/views/ops/db/dialect/dm_dialect';
|
import { 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());
|
||||||
|
})();
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
405
mayfly_go_web/src/views/ops/db/dialect/mssql_dialect.ts
Normal file
405
mayfly_go_web/src/views/ops/db/dialect/mssql_dialect.ts
Normal file
@@ -0,0 +1,405 @@
|
|||||||
|
import { DbInst } from '../db';
|
||||||
|
import { commonCustomKeywords, DataType, DbDialect, DialectInfo, EditorCompletion, EditorCompletionItem, IndexDefinition, RowDefinition } from './index';
|
||||||
|
import { language as sqlLanguage } from 'monaco-editor/esm/vs/basic-languages/sql/sql.js';
|
||||||
|
|
||||||
|
export { MSSQL_TYPE_LIST, MssqlDialect };
|
||||||
|
|
||||||
|
// 参考官方文档:https://docs.microsoft.com/zh-cn/sql/t-sql/data-types/data-types-transact-sql?view=sql-server-ver15
|
||||||
|
const MSSQL_TYPE_LIST = [
|
||||||
|
//精确数字
|
||||||
|
'bigint',
|
||||||
|
'numeric',
|
||||||
|
'bit',
|
||||||
|
'smallint',
|
||||||
|
'decimal',
|
||||||
|
'smallmoney',
|
||||||
|
'int',
|
||||||
|
'tinyint',
|
||||||
|
'money',
|
||||||
|
// 近似数字
|
||||||
|
'float',
|
||||||
|
'real',
|
||||||
|
// 日期和时间
|
||||||
|
'date',
|
||||||
|
'datetimeoffset',
|
||||||
|
'datetime2',
|
||||||
|
'smalldatetime',
|
||||||
|
'datetime',
|
||||||
|
'time',
|
||||||
|
// 字符串
|
||||||
|
'char',
|
||||||
|
'varchar',
|
||||||
|
'text',
|
||||||
|
'nchar',
|
||||||
|
'nvarchar',
|
||||||
|
'ntext',
|
||||||
|
'binary',
|
||||||
|
'varbinary',
|
||||||
|
|
||||||
|
// 其他
|
||||||
|
'cursor',
|
||||||
|
'rowversion',
|
||||||
|
'hierarchyid',
|
||||||
|
'uniqueidentifier',
|
||||||
|
'sql_variant',
|
||||||
|
'xml',
|
||||||
|
'table',
|
||||||
|
// 空间几何类型 参照 https://learn.microsoft.com/zh-cn/sql/t-sql/spatial-geometry/spatial-types-geometry-transact-sql?view=sql-server-ver15
|
||||||
|
'geometry',
|
||||||
|
// 空间地理类型 参照 https://learn.microsoft.com/zh-cn/sql/t-sql/spatial-geography/spatial-types-geography?view=sql-server-ver15
|
||||||
|
'geography',
|
||||||
|
];
|
||||||
|
// 函数参考官方文档 https://learn.microsoft.com/zh-cn/sql/t-sql/functions/functions?view=sql-server-ver15
|
||||||
|
|
||||||
|
let mssqlDialectInfo: DialectInfo;
|
||||||
|
|
||||||
|
const customKeywords: EditorCompletionItem[] = [
|
||||||
|
{
|
||||||
|
label: 'select top ',
|
||||||
|
description: 'keyword',
|
||||||
|
insertText: 'select top 100 * from',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'select page ',
|
||||||
|
description: 'keyword',
|
||||||
|
insertText: 'SELECT *, 0 AS _ORDER_F_ FROM table_name \n ORDER BY _ORDER_F_ \n OFFSET 0 ROWS FETCH NEXT 25 ROWS ONLY;',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const fixedLengthTypes = [
|
||||||
|
'int',
|
||||||
|
'bigint',
|
||||||
|
'smallint',
|
||||||
|
'tinyint',
|
||||||
|
'float',
|
||||||
|
'real',
|
||||||
|
'datetime',
|
||||||
|
'smalldatetime',
|
||||||
|
'date',
|
||||||
|
'time',
|
||||||
|
'datetime2',
|
||||||
|
'datetimeoffset',
|
||||||
|
'bit',
|
||||||
|
'uniqueidentifier',
|
||||||
|
'geometry',
|
||||||
|
'geography',
|
||||||
|
];
|
||||||
|
|
||||||
|
class MssqlDialect implements DbDialect {
|
||||||
|
getInfo(): DialectInfo {
|
||||||
|
if (mssqlDialectInfo) {
|
||||||
|
return mssqlDialectInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { keywords, operators, builtinVariables, builtinFunctions } = sqlLanguage;
|
||||||
|
let functions = builtinFunctions.map((a: string): EditorCompletionItem => ({ label: a, insertText: `${a}()`, description: 'func' }));
|
||||||
|
|
||||||
|
let excludeKeywords = new Set(operators);
|
||||||
|
let editorCompletions: EditorCompletion = {
|
||||||
|
keywords: keywords
|
||||||
|
.filter((a: string) => !excludeKeywords.has(a)) // 移除已存在的operator、function
|
||||||
|
.map((a: string): EditorCompletionItem => ({ label: a, description: 'keyword' }))
|
||||||
|
.concat(customKeywords)
|
||||||
|
.concat(commonCustomKeywords.map((a): EditorCompletionItem => ({ label: a, description: 'keyword' }))),
|
||||||
|
operators: operators.map((a: string): EditorCompletionItem => ({ label: a, description: 'operator' })),
|
||||||
|
functions,
|
||||||
|
variables: builtinVariables.map((a: string): EditorCompletionItem => ({ label: a, description: 'var' })),
|
||||||
|
};
|
||||||
|
|
||||||
|
mssqlDialectInfo = {
|
||||||
|
name: 'MSSQL',
|
||||||
|
icon: 'iconfont icon-MSSQLNATIVE',
|
||||||
|
defaultPort: 1433,
|
||||||
|
formatSqlDialect: 'transactsql',
|
||||||
|
columnTypes: MSSQL_TYPE_LIST.map((a) => ({ udtName: a, dataType: a, desc: '', space: '' })),
|
||||||
|
editorCompletions,
|
||||||
|
};
|
||||||
|
return mssqlDialectInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
|
||||||
|
let schema = db.split('/')[1];
|
||||||
|
return `SELECT *, 0 AS _MAY_ORDER_F_ FROM ${this.quoteIdentifier(schema)}.${this.quoteIdentifier(table)} ${condition ? 'WHERE ' + condition : ''} ${
|
||||||
|
orderBy ? orderBy + ', _MAY_ORDER_F_' : 'order by _MAY_ORDER_F_'
|
||||||
|
} ${this.getPageSql(pageNum, limit)};`.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
getPageSql(pageNum: number, limit: number) {
|
||||||
|
return ` offset ${(pageNum - 1) * limit} rows fetch next ${limit} rows only`.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
getDefaultRows(): RowDefinition[] {
|
||||||
|
return [
|
||||||
|
{ name: 'id', type: 'bigint', length: '', numScale: '', value: '', notNull: true, pri: true, auto_increment: true, remark: '主键ID' },
|
||||||
|
{ name: 'creator_id', type: 'bigint', length: '20', numScale: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '创建人id' },
|
||||||
|
{
|
||||||
|
name: 'creator',
|
||||||
|
type: 'varchar',
|
||||||
|
length: '100',
|
||||||
|
numScale: '',
|
||||||
|
value: '',
|
||||||
|
notNull: true,
|
||||||
|
pri: false,
|
||||||
|
auto_increment: false,
|
||||||
|
remark: '创建人姓名',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'create_time',
|
||||||
|
type: 'datetime2',
|
||||||
|
length: '',
|
||||||
|
numScale: '',
|
||||||
|
value: 'CURRENT_TIMESTAMP',
|
||||||
|
notNull: true,
|
||||||
|
pri: false,
|
||||||
|
auto_increment: false,
|
||||||
|
remark: '创建时间',
|
||||||
|
},
|
||||||
|
{ name: 'updator_id', type: 'bigint', length: '20', numScale: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '修改人id' },
|
||||||
|
{
|
||||||
|
name: 'updator',
|
||||||
|
type: 'varchar',
|
||||||
|
length: '100',
|
||||||
|
numScale: '',
|
||||||
|
value: '',
|
||||||
|
notNull: true,
|
||||||
|
pri: false,
|
||||||
|
auto_increment: false,
|
||||||
|
remark: '修改人姓名',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'update_time',
|
||||||
|
type: 'datetime2',
|
||||||
|
length: '',
|
||||||
|
numScale: '',
|
||||||
|
value: 'CURRENT_TIMESTAMP',
|
||||||
|
notNull: true,
|
||||||
|
pri: false,
|
||||||
|
auto_increment: false,
|
||||||
|
remark: '修改时间',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
getDefaultIndex(): IndexDefinition {
|
||||||
|
return {
|
||||||
|
indexName: '',
|
||||||
|
columnNames: [],
|
||||||
|
unique: false,
|
||||||
|
indexType: 'NONCLUSTERED',
|
||||||
|
indexComment: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
quoteIdentifier = (name: string) => {
|
||||||
|
return `[${name}]`;
|
||||||
|
};
|
||||||
|
|
||||||
|
genColumnBasicSql(cl: any): string {
|
||||||
|
let val = cl.value ? (cl.value === 'CURRENT_TIMESTAMP' ? cl.value : `'${cl.value}'`) : '';
|
||||||
|
let defVal = val ? `DEFAULT ${val}` : '';
|
||||||
|
// mssql哪些字段允许有长度
|
||||||
|
let length = '';
|
||||||
|
if (!fixedLengthTypes.includes(cl.type)) {
|
||||||
|
length = cl.length ? `(${cl.length})` : '';
|
||||||
|
}
|
||||||
|
return ` ${this.quoteIdentifier(cl.name)} ${cl.type}${length} ${cl.auto_increment ? 'IDENTITY(1,1)' : ''} ${defVal} ${cl.notNull ? 'NOT NULL' : 'NULL'} `;
|
||||||
|
}
|
||||||
|
getCreateTableSql(data: any): string {
|
||||||
|
let schema = data.db.split('/')[1];
|
||||||
|
|
||||||
|
// 创建表结构
|
||||||
|
let pks = [] as string[];
|
||||||
|
let fields: string[] = [];
|
||||||
|
let fieldComments: string[] = [];
|
||||||
|
data.fields.res.forEach((item: any) => {
|
||||||
|
item.name && fields.push(this.genColumnBasicSql(item));
|
||||||
|
item.remark &&
|
||||||
|
fieldComments.push(
|
||||||
|
`EXECUTE sp_addextendedproperty N'MS_Description', N'${item.remark}', N'SCHEMA', N'${schema}', N'TABLE', N'${data.tableName}', N'COLUMN', N'${item.name}'`
|
||||||
|
);
|
||||||
|
if (item.pri) {
|
||||||
|
pks.push(`${this.quoteIdentifier(item.name)}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let baseTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(data.tableName)}`;
|
||||||
|
|
||||||
|
// 建表语句
|
||||||
|
let createTable = `CREATE TABLE ${baseTable}
|
||||||
|
( ${fields.join(',')}
|
||||||
|
${pks.length > 0 ? `, PRIMARY KEY CLUSTERED (${pks.join(',')})` : ''}
|
||||||
|
);`;
|
||||||
|
|
||||||
|
let createIndexSql = this.getCreateIndexSql(data);
|
||||||
|
|
||||||
|
// 表注释
|
||||||
|
if (data.tableComment) {
|
||||||
|
createTable += ` EXECUTE sp_addextendedproperty N'MS_Description', N'${data.tableComment}', N'SCHEMA', N'${schema}', N'TABLE', N'${data.tableName}';`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return createTable + createIndexSql + fieldComments.join(';');
|
||||||
|
}
|
||||||
|
|
||||||
|
getCreateIndexSql(data: any): string {
|
||||||
|
// CREATE UNIQUE NONCLUSTERED INDEX [aaa]
|
||||||
|
// ON [dbo].[无标题] (
|
||||||
|
// [id],
|
||||||
|
// [name]
|
||||||
|
// )
|
||||||
|
let schema = data.db.split('/')[1];
|
||||||
|
let baseTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(data.tableName)}`;
|
||||||
|
|
||||||
|
let indexComment = [] as string[];
|
||||||
|
|
||||||
|
// 创建索引
|
||||||
|
let sql: string[] = [];
|
||||||
|
data.indexs.res.forEach((a: any) => {
|
||||||
|
let columnNames = a.columnNames.map((b: string) => `${this.quoteIdentifier(b)}`);
|
||||||
|
sql.push(` CREATE ${a.unique ? 'UNIQUE' : ''} NONCLUSTERED INDEX ${this.quoteIdentifier(a.indexName)} on ${baseTable} (${columnNames.join(',')})`);
|
||||||
|
if (a.indexComment) {
|
||||||
|
indexComment.push(
|
||||||
|
`EXECUTE sp_addextendedproperty N'MS_Description', N'${a.indexComment}', N'SCHEMA', N'${schema}', N'TABLE', N'${data.tableName}', N'INDEX', N'${a.indexName}'`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return sql.join(';') + ';' + indexComment.join(';');
|
||||||
|
}
|
||||||
|
|
||||||
|
getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
|
||||||
|
// sql执行顺序
|
||||||
|
// 1. 删除字段
|
||||||
|
// 2. 添加字段
|
||||||
|
// 3. 修改字段名字
|
||||||
|
// 4. 修改字段类型
|
||||||
|
// 5. 修改字段注释
|
||||||
|
// 6. 添加字段注释
|
||||||
|
|
||||||
|
let schema = tableData.db.split('/')[1];
|
||||||
|
let baseTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableName)}`;
|
||||||
|
|
||||||
|
let delSql = '';
|
||||||
|
let addArr = [] as string[];
|
||||||
|
let renameArr = [] as string[];
|
||||||
|
let updArr = [] as string[];
|
||||||
|
let changeCommentArr = [] as string[];
|
||||||
|
let addCommentArr = [] as string[];
|
||||||
|
|
||||||
|
if (changeData.del.length > 0) {
|
||||||
|
delSql = `ALTER TABLE ${baseTable} DROP ${changeData.del.map((a) => 'COLUMN ' + this.quoteIdentifier(a.name)).join(',')};`;
|
||||||
|
}
|
||||||
|
if (changeData.add.length > 0) {
|
||||||
|
changeData.add.forEach((a) => {
|
||||||
|
addArr.push(` ALTER TABLE ${baseTable} ADD COLUMN ${this.genColumnBasicSql(a)}`);
|
||||||
|
if (a.remark) {
|
||||||
|
addCommentArr.push(
|
||||||
|
`EXECUTE sp_addextendedproperty N'MS_Description', N'${a.remark}', N'SCHEMA', N'${schema}', N'TABLE', N'${tableName}', N'COLUMN', N'${a.name}'`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changeData.upd.length > 0) {
|
||||||
|
changeData.upd.forEach((a) => {
|
||||||
|
if (a.oldName && a.name !== a.oldName) {
|
||||||
|
renameArr.push(` EXEC sp_rename '${baseTable}.${this.quoteIdentifier(a.oldName)}', '${a.name}', 'COLUMN' `);
|
||||||
|
} else {
|
||||||
|
updArr.push(` ALTER TABLE ${baseTable} ALTER COLUMN ${this.genColumnBasicSql(a)} `);
|
||||||
|
}
|
||||||
|
if (a.remark) {
|
||||||
|
changeCommentArr.push(`IF ((SELECT COUNT(*) FROM fn_listextendedproperty('MS_Description',
|
||||||
|
'SCHEMA', N'${schema}',
|
||||||
|
'TABLE', N'${tableName}',
|
||||||
|
'COLUMN', N'${a.name}')) > 0)
|
||||||
|
EXEC sp_updateextendedproperty
|
||||||
|
'MS_Description', N'${a.remark}',
|
||||||
|
'SCHEMA', N'${schema}',
|
||||||
|
'TABLE', N'${tableName}',
|
||||||
|
'COLUMN', N'${a.name}'
|
||||||
|
ELSE
|
||||||
|
EXEC sp_addextendedproperty
|
||||||
|
'MS_Description', N'${a.remark}',
|
||||||
|
'SCHEMA', N'${schema}',
|
||||||
|
'TABLE', N'${tableName}',
|
||||||
|
'COLUMN',N'${a.name}'`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
delSql +
|
||||||
|
(addArr.length > 0 ? addArr.join(';') + ';' : '') +
|
||||||
|
(renameArr.length > 0 ? renameArr.join(';') + ';' : '') +
|
||||||
|
(updArr.length > 0 ? updArr.join(';') + ';' : '') +
|
||||||
|
(changeCommentArr.length > 0 ? changeCommentArr.join(';') + ';' : '') +
|
||||||
|
(addCommentArr.length > 0 ? addCommentArr.join(';') + ';' : '')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
|
||||||
|
let schema = tableData.db.split('/')[1];
|
||||||
|
let baseTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableName)}`;
|
||||||
|
|
||||||
|
let dropArr = [] as string[];
|
||||||
|
let addArr = [] as string[];
|
||||||
|
let commentArr = [] as string[];
|
||||||
|
|
||||||
|
const pushDrop = (a: any) => {
|
||||||
|
dropArr.push(` DROP INDEX ${this.quoteIdentifier(a.indexName)} ON ${baseTable} `);
|
||||||
|
};
|
||||||
|
const pushAdd = (a: any) => {
|
||||||
|
addArr.push(
|
||||||
|
` CREATE ${a.unique ? 'UNIQUE' : ''} NONCLUSTERED INDEX ${this.quoteIdentifier(a.indexName)} ON ${baseTable} (${a.columnNames.map((b: string) => this.quoteIdentifier(b)).join(',')}) `
|
||||||
|
);
|
||||||
|
if (a.indexComment) {
|
||||||
|
commentArr.push(
|
||||||
|
` EXEC sp_addextendedproperty N'MS_Description', N'${a.indexComment}', N'SCHEMA', N'${schema}', N'TABLE', N'${tableName}', N'INDEX', N'${a.indexName}' `
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (changeData.upd.length > 0) {
|
||||||
|
changeData.upd.forEach((a) => {
|
||||||
|
pushDrop(a);
|
||||||
|
pushAdd(a);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changeData.del.length > 0) {
|
||||||
|
changeData.del.forEach((a) => {
|
||||||
|
pushDrop(a);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changeData.add.length > 0) {
|
||||||
|
changeData.add.forEach((a) => pushAdd(a));
|
||||||
|
}
|
||||||
|
let dropSql = dropArr.join(';');
|
||||||
|
let addSql = addArr.join(';');
|
||||||
|
let commentSql = commentArr.join(';');
|
||||||
|
return dropSql + ';' + addSql + ';' + commentSql + ';';
|
||||||
|
}
|
||||||
|
|
||||||
|
getDataType(columnType: string): DataType {
|
||||||
|
if (DbInst.isNumber(columnType)) {
|
||||||
|
return DataType.Number;
|
||||||
|
}
|
||||||
|
// 日期时间类型
|
||||||
|
if (/datetime|timestamp/gi.test(columnType)) {
|
||||||
|
return DataType.DateTime;
|
||||||
|
}
|
||||||
|
// 日期类型
|
||||||
|
if (/date/gi.test(columnType)) {
|
||||||
|
return DataType.Date;
|
||||||
|
}
|
||||||
|
// 时间类型
|
||||||
|
if (/time/gi.test(columnType)) {
|
||||||
|
return DataType.Time;
|
||||||
|
}
|
||||||
|
return DataType.String;
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
|
||||||
|
wrapStrValue(columnType: string, value: string): string {
|
||||||
|
return `'${value}'`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { language as mysqlLanguage } from 'monaco-editor/esm/vs/basic-languages/
|
|||||||
|
|
||||||
export { MYSQL_TYPE_LIST, MysqlDialect };
|
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`
|
||||||
|
|||||||
@@ -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[] = [];
|
||||||
|
|||||||
@@ -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[] = [];
|
||||||
|
|||||||
338
mayfly_go_web/src/views/ops/db/dialect/sqlite_dialect.ts
Normal file
338
mayfly_go_web/src/views/ops/db/dialect/sqlite_dialect.ts
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
import {
|
||||||
|
commonCustomKeywords,
|
||||||
|
DataType,
|
||||||
|
DbDialect,
|
||||||
|
DialectInfo,
|
||||||
|
EditorCompletion,
|
||||||
|
EditorCompletionItem,
|
||||||
|
IndexDefinition,
|
||||||
|
RowDefinition,
|
||||||
|
sqlColumnType,
|
||||||
|
} from './index';
|
||||||
|
import { DbInst } from '@/views/ops/db/db';
|
||||||
|
import { language as sqlLanguage } from 'monaco-editor/esm/vs/basic-languages/sql/sql.js';
|
||||||
|
|
||||||
|
export { SqliteDialect };
|
||||||
|
|
||||||
|
// 参考官方文档:https://www.sqlite.org/datatype3.html
|
||||||
|
const SQLITE_TYPE_LIST: sqlColumnType[] = [
|
||||||
|
// INTEGER
|
||||||
|
{ udtName: 'int', dataType: 'int', desc: '', space: '', range: '' },
|
||||||
|
{ udtName: 'integer', dataType: 'integer', desc: '', space: '', range: '' },
|
||||||
|
{ udtName: 'tinyint', dataType: 'tinyint', desc: '', space: '', range: '' },
|
||||||
|
{ udtName: 'smallint', dataType: 'smallint', desc: '', space: '', range: '' },
|
||||||
|
{ udtName: 'mediumint', dataType: 'mediumint', desc: '', space: '', range: '' },
|
||||||
|
{ udtName: 'bigint', dataType: 'bigint', desc: '', space: '', range: '' },
|
||||||
|
{ udtName: 'unsigned big int', dataType: 'unsigned big int', desc: '', space: '', range: '' },
|
||||||
|
{ udtName: 'int2', dataType: 'int2', desc: '', space: '', range: '' },
|
||||||
|
{ udtName: 'int8', dataType: 'int8', desc: '', space: '', range: '' },
|
||||||
|
// TEXT
|
||||||
|
{ udtName: 'character', dataType: 'character', desc: '', space: '', range: '' },
|
||||||
|
{ udtName: 'varchar', dataType: 'varchar', desc: '', space: '', range: '' },
|
||||||
|
{ udtName: 'varying character', dataType: 'varying character', desc: '', space: '', range: '' },
|
||||||
|
{ udtName: 'nchar', dataType: 'nchar', desc: '', space: '', range: '' },
|
||||||
|
{ udtName: 'native character', dataType: 'native character', desc: '', space: '', range: '' },
|
||||||
|
{ udtName: 'nvarchar', dataType: 'nvarchar', desc: '', space: '', range: '' },
|
||||||
|
{ udtName: 'text', dataType: 'text', desc: '', space: '', range: '' },
|
||||||
|
{ udtName: 'clob', dataType: 'clob', desc: '', space: '', range: '' },
|
||||||
|
// blob
|
||||||
|
{ udtName: 'blob', dataType: 'blob', desc: '', space: '', range: '' },
|
||||||
|
{ udtName: 'no datatype specified', dataType: 'no datatype specified', desc: '', space: '', range: '' },
|
||||||
|
// REAL
|
||||||
|
{ udtName: 'real', dataType: 'real', desc: '', space: '', range: '' },
|
||||||
|
{ udtName: 'double', dataType: 'double', desc: '', space: '', range: '' },
|
||||||
|
{ udtName: 'double precision', dataType: 'double precision', desc: '', space: '', range: '' },
|
||||||
|
{ udtName: 'float', dataType: 'float', desc: '', space: '', range: '' },
|
||||||
|
// NUMERIC
|
||||||
|
{ udtName: 'numeric', dataType: 'numeric', desc: '', space: '', range: '' },
|
||||||
|
{ udtName: 'decimal', dataType: 'decimal', desc: '', space: '', range: '' },
|
||||||
|
{ udtName: 'boolean', dataType: 'boolean', desc: '', space: '', range: '' },
|
||||||
|
{ udtName: 'date', dataType: 'date', desc: '', space: '', range: '' },
|
||||||
|
{ udtName: 'datetime', dataType: 'datetime', desc: '', space: '', range: '' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const addCustomKeywords = ['PRAGMA', 'database_list', 'sqlite_master'];
|
||||||
|
|
||||||
|
// 参考官方文档:https://www.sqlite.org/lang_corefunc.html
|
||||||
|
const functions: EditorCompletionItem[] = [
|
||||||
|
// 字符函数
|
||||||
|
{ label: 'abs', insertText: 'abs(X)', description: '返回给定数值的绝对值' },
|
||||||
|
{ label: 'changes', insertText: 'changes()', description: '返回最近增删改影响的行数' },
|
||||||
|
{ label: 'coalesce', insertText: 'coalesce(X,Y,...)', description: '返回第一个不为空的值' },
|
||||||
|
{ label: 'hex', insertText: 'hex(X)', description: '返回给定字符的hex值' },
|
||||||
|
{ label: 'ifnull', insertText: 'ifnull(X,Y)', description: '返回第一个不为空的值' },
|
||||||
|
{ label: 'iif', insertText: 'iif(X,Y,Z)', description: '如果x为真则返回y,否则返回z' },
|
||||||
|
{ label: 'instr', insertText: 'instr(X,Y)', description: '返回字符y在x的第n个位置' },
|
||||||
|
{ label: 'length', insertText: 'length(X)', description: '返回给定字符的长度' },
|
||||||
|
{ label: 'load_extension', insertText: 'load_extension(X[,Y])', description: '加载扩展块' },
|
||||||
|
{ label: 'lower', insertText: 'lower(X)', description: '返回小写字符' },
|
||||||
|
{ label: 'ltrim', insertText: 'ltrim(X[,Y])', description: '左trim' },
|
||||||
|
{ label: 'nullif', insertText: 'nullif(X,Y)', description: '比较两值相等则返回null,否则返回第一个值' },
|
||||||
|
{ label: 'printf', insertText: "printf('%s',...)", description: '字符串格式化拼接,如%s %d' },
|
||||||
|
{ label: 'quote', insertText: 'quote(X)', description: '把字符串用引号包起来' },
|
||||||
|
{ label: 'random', insertText: 'random()', description: '生成随机数' },
|
||||||
|
{ label: 'randomblob', insertText: 'randomblob(N)', description: '生成一个包含N个随机字节的BLOB' },
|
||||||
|
{ label: 'replace', insertText: 'replace(X,Y,Z)', description: '替换字符串' },
|
||||||
|
{ label: 'round', insertText: 'round(X[,Y])', description: '将数值四舍五入到指定的小数位数' },
|
||||||
|
{ label: 'rtrim', insertText: 'rtrim(X[,Y])', description: '右trim' },
|
||||||
|
{ label: 'sign', insertText: 'sign(X)', description: '返回数字符号 1正 -1负 0零 null' },
|
||||||
|
{ label: 'soundex', insertText: 'soundex(X)', description: '返回字符串X的soundex编码字符串' },
|
||||||
|
{ label: 'sqlite_compileoption_get', insertText: 'sqlite_compileoption_get(N)', description: '获取指定编译选项的值' },
|
||||||
|
{ label: 'sqlite_compileoption_used', insertText: 'sqlite_compileoption_used(X)', description: '检查SQLite编译时是否使用了指定的编译选项' },
|
||||||
|
{ label: 'sqlite_source_id', insertText: 'sqlite_source_id()', description: '获取sqlite源代码标识符' },
|
||||||
|
{ label: 'sqlite_version', insertText: 'sqlite_version()', description: '获取sqlite版本' },
|
||||||
|
{ label: 'substr', insertText: 'substr(X,Y[,Z])', description: '截取字符串' },
|
||||||
|
{ label: 'substring', insertText: 'substring(X,Y[,Z])', description: '截取字符串' },
|
||||||
|
{ label: 'trim', insertText: 'trim(X[,Y])', description: '去除给定字符串前后的字符,默认空格' },
|
||||||
|
{ label: 'typeof', insertText: 'typeof(X)', description: '返回X的基本类型:null,integer,real,text,blob' },
|
||||||
|
{ label: 'unicode', insertText: 'unicode(X)', description: '返回与字符串X的第一个字符相对应的数字unicode代码点' },
|
||||||
|
{ label: 'unlikely', insertText: 'unlikely(X)', description: '返回大写字符' },
|
||||||
|
{ label: 'upper', insertText: 'upper(X)', description: '返回由0x00的N个字节组成的BLOB' },
|
||||||
|
{ label: 'zeroblob', insertText: 'zeroblob(N)', description: '返回分组中的平均值' },
|
||||||
|
{ label: 'avg', insertText: 'avg(X)', description: '返回总条数' },
|
||||||
|
{ label: 'count', insertText: 'count(*)', description: '返回分组中用给定非空字符串连接的值' },
|
||||||
|
{ label: 'group_concat', insertText: 'group_concat(X[,Y])', description: '返回分组中最大值' },
|
||||||
|
{ label: 'max', insertText: 'max(X)', description: '返回分组中最小值' },
|
||||||
|
{ label: 'min', insertText: 'min(X)', description: '返回分组中非空值的总和。' },
|
||||||
|
{ label: 'sum', insertText: 'sum(X)', description: '返回分组中非空值的总和。' },
|
||||||
|
{ label: 'total', insertText: 'total(X)', description: '返回YYYY-MM-DD格式的字符串' },
|
||||||
|
{ label: 'date', insertText: 'date(time-value[, modifier, ...])', description: '返回HH:MM:SS格式的字符串' },
|
||||||
|
{ label: 'time', insertText: 'time(time-value[, modifier, ...])', description: '将日期和时间字符串转换为特定的日期和时间格式' },
|
||||||
|
{ label: 'datetime', insertText: 'datetime(time-value[, modifier, ...])', description: '计算日期和时间的儒略日数' },
|
||||||
|
{ label: 'julianday', insertText: 'julianday(time-value[, modifier, ...])', description: '将日期和时间格式化为指定的字符串' },
|
||||||
|
];
|
||||||
|
|
||||||
|
let sqliteDialectInfo: DialectInfo;
|
||||||
|
class SqliteDialect implements DbDialect {
|
||||||
|
getInfo(): DialectInfo {
|
||||||
|
if (sqliteDialectInfo) {
|
||||||
|
return sqliteDialectInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { keywords, operators, builtinVariables } = sqlLanguage;
|
||||||
|
|
||||||
|
let editorCompletions: EditorCompletion = {
|
||||||
|
keywords: keywords
|
||||||
|
.filter((a: string) => addCustomKeywords.indexOf(a) === -1)
|
||||||
|
.map((a: string): EditorCompletionItem => ({ label: a, description: 'keyword' }))
|
||||||
|
.concat(commonCustomKeywords.map((a): EditorCompletionItem => ({ label: a, description: 'keyword' })))
|
||||||
|
.concat(addCustomKeywords.map((a): EditorCompletionItem => ({ label: a, description: 'keyword' }))),
|
||||||
|
operators: operators.map((a: string): EditorCompletionItem => ({ label: a, description: 'operator' })),
|
||||||
|
functions,
|
||||||
|
variables: builtinVariables.map((a: string): EditorCompletionItem => ({ label: a, description: 'var' })),
|
||||||
|
};
|
||||||
|
|
||||||
|
sqliteDialectInfo = {
|
||||||
|
name: 'Sqlite',
|
||||||
|
icon: 'iconfont icon-sqlite',
|
||||||
|
defaultPort: 0,
|
||||||
|
formatSqlDialect: 'sql',
|
||||||
|
columnTypes: SQLITE_TYPE_LIST.sort((a, b) => a.udtName.localeCompare(b.udtName)),
|
||||||
|
editorCompletions,
|
||||||
|
};
|
||||||
|
return sqliteDialectInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
|
||||||
|
return `SELECT * FROM ${this.quoteIdentifier(table)} ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''} ${this.getPageSql(
|
||||||
|
pageNum,
|
||||||
|
limit
|
||||||
|
)};`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPageSql(pageNum: number, limit: number) {
|
||||||
|
return ` LIMIT ${(pageNum - 1) * limit}, ${limit}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDefaultRows(): RowDefinition[] {
|
||||||
|
return [
|
||||||
|
{ name: 'id', type: 'integer', length: '', numScale: '', value: '', notNull: true, pri: true, auto_increment: true, remark: '主键ID' },
|
||||||
|
{ name: 'creator_id', type: 'bigint', length: '20', numScale: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '创建人id' },
|
||||||
|
{
|
||||||
|
name: 'creator',
|
||||||
|
type: 'varchar',
|
||||||
|
length: '100',
|
||||||
|
numScale: '',
|
||||||
|
value: '',
|
||||||
|
notNull: true,
|
||||||
|
pri: false,
|
||||||
|
auto_increment: false,
|
||||||
|
remark: '创建人姓名',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'create_time',
|
||||||
|
type: 'datetime',
|
||||||
|
length: '',
|
||||||
|
numScale: '',
|
||||||
|
value: 'CURRENT_TIMESTAMP',
|
||||||
|
notNull: true,
|
||||||
|
pri: false,
|
||||||
|
auto_increment: false,
|
||||||
|
remark: '创建时间',
|
||||||
|
},
|
||||||
|
{ name: 'updator_id', type: 'bigint', length: '20', numScale: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '修改人id' },
|
||||||
|
{ name: 'updator', type: 'varchar', length: '100', numScale: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '修改姓名' },
|
||||||
|
{
|
||||||
|
name: 'update_time',
|
||||||
|
type: 'datetime',
|
||||||
|
length: '',
|
||||||
|
numScale: '',
|
||||||
|
value: 'CURRENT_TIMESTAMP',
|
||||||
|
notNull: true,
|
||||||
|
pri: false,
|
||||||
|
auto_increment: false,
|
||||||
|
remark: '修改时间',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
getDefaultIndex(): IndexDefinition {
|
||||||
|
return {
|
||||||
|
indexName: '',
|
||||||
|
columnNames: [],
|
||||||
|
unique: false,
|
||||||
|
indexType: 'BTREE',
|
||||||
|
indexComment: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
quoteIdentifier = (name: string) => {
|
||||||
|
return `\"${name}\"`;
|
||||||
|
};
|
||||||
|
|
||||||
|
genColumnBasicSql(cl: any): string {
|
||||||
|
let val = cl.value ? (cl.value === 'CURRENT_TIMESTAMP' ? cl.value : `'${cl.value}'`) : '';
|
||||||
|
let defVal = val ? `DEFAULT ${val}` : '';
|
||||||
|
let length = cl.length ? `(${cl.length})` : '';
|
||||||
|
let nullAble = cl.notNull ? 'NOT NULL' : 'NULL';
|
||||||
|
if (cl.pri) {
|
||||||
|
return ` ${this.quoteIdentifier(cl.name)} ${cl.type}${length} PRIMARY KEY ${cl.auto_increment ? 'AUTOINCREMENT' : ''} ${nullAble} `;
|
||||||
|
}
|
||||||
|
return ` ${this.quoteIdentifier(cl.name)} ${cl.type}${length} ${nullAble} ${defVal} `;
|
||||||
|
}
|
||||||
|
getCreateTableSql(data: any): string {
|
||||||
|
// 创建表结构
|
||||||
|
let fields: string[] = [];
|
||||||
|
data.fields.res.forEach((item: any) => {
|
||||||
|
item.name && fields.push(this.genColumnBasicSql(item));
|
||||||
|
});
|
||||||
|
|
||||||
|
return `CREATE TABLE ${this.quoteIdentifier(data.db)}.${this.quoteIdentifier(data.tableName)}
|
||||||
|
( ${fields.join(',')} )`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCreateIndexSql(data: any): string {
|
||||||
|
// 创建索引
|
||||||
|
let sql = [] as string[];
|
||||||
|
data.indexs.res.forEach((a: any) => {
|
||||||
|
sql.push(
|
||||||
|
`CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${this.quoteIdentifier(data.db)}.${this.quoteIdentifier(a.indexName)} ON "${data.tableName}" (${a.columnNames.join(',')})`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return sql.join(';');
|
||||||
|
}
|
||||||
|
|
||||||
|
getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
|
||||||
|
// sqlite修改表结构需要先删除再创建
|
||||||
|
|
||||||
|
// 1.删除旧表索引 DROP INDEX "main"."aa";
|
||||||
|
let sql = [] as string[];
|
||||||
|
tableData.indexs.res.forEach((a: any) => {
|
||||||
|
a.indexName && sql.push(`DROP INDEX ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(a.indexName)}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2.重命名表,备份旧表 ALTER TABLE "main"."t_sys_resource" RENAME TO "_t_sys_resource_old_20240118162712"; new Date().getTime()
|
||||||
|
let oldTableName = `_${tableName}_old_${new Date().getTime()}`;
|
||||||
|
sql.push(`ALTER TABLE ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(tableName)} RENAME TO ${this.quoteIdentifier(oldTableName)}`);
|
||||||
|
|
||||||
|
// 3.创建新表
|
||||||
|
sql.push(this.getCreateTableSql(tableData));
|
||||||
|
|
||||||
|
// 4.复制数据 INSERT INTO "库名"."新表名" (${insertFields}) SELECT ${queryFields} FROM "库名"."旧表名";
|
||||||
|
// 查询的字段数据类型和数量应与插入的字段一致
|
||||||
|
// 判断哪些字段需要查询旧表,哪些字段需要插入新表
|
||||||
|
// 解析changeData,统计需要查询旧表的字段,统计需要插入新表的字段
|
||||||
|
let delFields = changeData.del.map((a) => a.name);
|
||||||
|
let addFields = changeData.add.map((a) => a.name);
|
||||||
|
|
||||||
|
let queryFields = [] as string[];
|
||||||
|
let insertFields = [] as string[];
|
||||||
|
tableData.fields.res.forEach((a: any) => {
|
||||||
|
// 新增、删除的字段不需要查询旧表,不需要插入新表
|
||||||
|
if (addFields.includes(a.name) || delFields.includes(a.name)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 修改的字段需要查询和插入,判断是否修改了字段名,如果修改了字段名,需要查询旧表原名,插入新表新名
|
||||||
|
// 其余未删除、未修改的字段,需要查询旧表,插入新表
|
||||||
|
queryFields.push(this.quoteIdentifier(a.name === a.oldName ? a.name : a.oldName));
|
||||||
|
insertFields.push(this.quoteIdentifier(a.name));
|
||||||
|
});
|
||||||
|
// 生成sql
|
||||||
|
sql.push(
|
||||||
|
`INSERT INTO ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(tableName)} (${insertFields.join(',')}) SELECT ${queryFields.join(
|
||||||
|
','
|
||||||
|
)} FROM ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(oldTableName)}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// 5.创建索引
|
||||||
|
tableData.indexs.res.forEach((a: any) => {
|
||||||
|
a.indexName &&
|
||||||
|
sql.push(
|
||||||
|
`CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(a.indexName)} ON "${tableName}" (${a.columnNames.join(',')})`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return sql.join(';') + ';';
|
||||||
|
}
|
||||||
|
|
||||||
|
getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
|
||||||
|
// sqlite创建索引需要先删除再创建
|
||||||
|
// CREATE INDEX "main"."aa1" ON "t_sys_resource" ( "ui_path" );
|
||||||
|
|
||||||
|
let sql = [] as string[];
|
||||||
|
|
||||||
|
if (changeData.del.length > 0) {
|
||||||
|
changeData.del.forEach((a) => {
|
||||||
|
sql.push(`DROP INDEX ${this.quoteIdentifier(a.indexName)}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let indexData = [] as any[];
|
||||||
|
if (changeData.add.length > 0) {
|
||||||
|
indexData = indexData.concat(changeData.add);
|
||||||
|
}
|
||||||
|
if (changeData.upd.length > 0) {
|
||||||
|
indexData = indexData.concat(changeData.upd);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (indexData.length > 0) {
|
||||||
|
indexData.forEach((a) => {
|
||||||
|
sql.push(`CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${this.quoteIdentifier(a.indexName)} ON ${tableName} (${a.columnNames.join(',')})`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return sql.join(';');
|
||||||
|
}
|
||||||
|
|
||||||
|
getDataType(columnType: string): DataType {
|
||||||
|
if (DbInst.isNumber(columnType)) {
|
||||||
|
return DataType.Number;
|
||||||
|
}
|
||||||
|
// 日期时间类型
|
||||||
|
if (/datetime|timestamp/gi.test(columnType)) {
|
||||||
|
return DataType.DateTime;
|
||||||
|
}
|
||||||
|
// 日期类型
|
||||||
|
if (/date/gi.test(columnType)) {
|
||||||
|
return DataType.Date;
|
||||||
|
}
|
||||||
|
// 时间类型
|
||||||
|
if (/time/gi.test(columnType)) {
|
||||||
|
return DataType.Time;
|
||||||
|
}
|
||||||
|
return DataType.String;
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
|
||||||
|
wrapStrValue(columnType: string, value: string): string {
|
||||||
|
return `'${value}'`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,7 +41,15 @@
|
|||||||
<el-input v-model="state.keySeparator" placeholder="分割符" size="small" class="ml5" />
|
<el-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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
package initialize
|
|
||||||
|
|
||||||
import (
|
|
||||||
sysapp "mayfly-go/internal/sys/application"
|
|
||||||
"mayfly-go/pkg/req"
|
|
||||||
)
|
|
||||||
|
|
||||||
func InitSaveLogFunc() req.SaveLogFunc {
|
|
||||||
return sysapp.GetSyslogApp().SaveFromReq
|
|
||||||
}
|
|
||||||
@@ -1,25 +1,20 @@
|
|||||||
package initialize
|
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:""`
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 用户账号密码登录 **/
|
/** 用户账号密码登录 **/
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"))
|
||||||
}
|
}
|
||||||
|
|||||||
12
server/internal/auth/init/init.go
Normal file
12
server/internal/auth/init/init.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package init
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mayfly-go/initialize"
|
||||||
|
"mayfly-go/internal/auth/application"
|
||||||
|
"mayfly-go/internal/auth/router"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
initialize.AddInitIocFunc(application.InitIoc)
|
||||||
|
initialize.AddInitRouterFunc(router.Init)
|
||||||
|
}
|
||||||
@@ -2,30 +2,22 @@ package router
|
|||||||
|
|
||||||
import (
|
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")
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"mayfly-go/internal/common/consts"
|
|
||||||
dbapp "mayfly-go/internal/db/application"
|
|
||||||
machineapp "mayfly-go/internal/machine/application"
|
|
||||||
mongoapp "mayfly-go/internal/mongo/application"
|
|
||||||
redisapp "mayfly-go/internal/redis/application"
|
|
||||||
tagapp "mayfly-go/internal/tag/application"
|
|
||||||
"mayfly-go/pkg/req"
|
|
||||||
"mayfly-go/pkg/utils/collx"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Index struct {
|
|
||||||
TagApp tagapp.TagTree
|
|
||||||
MachineApp machineapp.Machine
|
|
||||||
DbApp dbapp.Db
|
|
||||||
RedisApp redisapp.Redis
|
|
||||||
MongoApp mongoapp.Mongo
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Index) Count(rc *req.Ctx) {
|
|
||||||
accountId := rc.GetLoginAccount().Id
|
|
||||||
|
|
||||||
mongoNum := len(i.TagApp.GetAccountResourceCodes(accountId, consts.TagResourceTypeMongo, ""))
|
|
||||||
machienNum := len(i.TagApp.GetAccountResourceCodes(accountId, consts.TagResourceTypeMachine, ""))
|
|
||||||
dbNum := len(i.TagApp.GetAccountResourceCodes(accountId, consts.TagResourceTypeDb, ""))
|
|
||||||
redisNum := len(i.TagApp.GetAccountResourceCodes(accountId, consts.TagResourceTypeRedis, ""))
|
|
||||||
|
|
||||||
rc.ResData = collx.M{
|
|
||||||
"mongoNum": mongoNum,
|
|
||||||
"machineNum": machienNum,
|
|
||||||
"dbNum": dbNum,
|
|
||||||
"redisNum": redisNum,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
10
server/internal/common/init/init.go
Normal file
10
server/internal/common/init/init.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package init
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mayfly-go/initialize"
|
||||||
|
"mayfly-go/internal/common/router"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
initialize.AddInitRouterFunc(router.Init)
|
||||||
|
}
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"mayfly-go/internal/common/api"
|
|
||||||
dbapp "mayfly-go/internal/db/application"
|
|
||||||
machineapp "mayfly-go/internal/machine/application"
|
|
||||||
mongoapp "mayfly-go/internal/mongo/application"
|
|
||||||
redisapp "mayfly-go/internal/redis/application"
|
|
||||||
tagapp "mayfly-go/internal/tag/application"
|
|
||||||
"mayfly-go/pkg/req"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
func InitIndexRouter(router *gin.RouterGroup) {
|
|
||||||
index := router.Group("common/index")
|
|
||||||
i := &api.Index{
|
|
||||||
TagApp: tagapp.GetTagTreeApp(),
|
|
||||||
MachineApp: machineapp.GetMachineApp(),
|
|
||||||
DbApp: dbapp.GetDbApp(),
|
|
||||||
RedisApp: redisapp.GetRedisApp(),
|
|
||||||
MongoApp: mongoapp.GetMongoApp(),
|
|
||||||
}
|
|
||||||
{
|
|
||||||
// 首页基本信息统计
|
|
||||||
req.NewGet("count", i.Count).Group(index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,5 +4,4 @@ import "github.com/gin-gonic/gin"
|
|||||||
|
|
||||||
func Init(router *gin.RouterGroup) {
|
func Init(router *gin.RouterGroup) {
|
||||||
InitCommonRouter(router)
|
InitCommonRouter(router)
|
||||||
InitIndexRouter(router)
|
|
||||||
}
|
}
|
||||||
|
|||||||
23
server/internal/db/api/dashbord.go
Normal file
23
server/internal/db/api/dashbord.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mayfly-go/internal/common/consts"
|
||||||
|
"mayfly-go/internal/db/application"
|
||||||
|
tagapp "mayfly-go/internal/tag/application"
|
||||||
|
"mayfly-go/pkg/req"
|
||||||
|
"mayfly-go/pkg/utils/collx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Dashbord struct {
|
||||||
|
TagTreeApp tagapp.TagTree `inject:""`
|
||||||
|
DbApp application.Db `inject:""`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Dashbord) Dashbord(rc *req.Ctx) {
|
||||||
|
accountId := rc.GetLoginAccount().Id
|
||||||
|
dbNum := len(m.TagTreeApp.GetAccountResourceCodes(accountId, consts.TagResourceTypeDb, ""))
|
||||||
|
|
||||||
|
rc.ResData = collx.M{
|
||||||
|
"dbNum": dbNum,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,11 +32,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Db struct {
|
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错误")
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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"` // 是否复制数据
|
||||||
|
}
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
@@ -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 获取数据库实例信息
|
||||||
|
|||||||
@@ -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:"-"` // 最近一次恢复结果
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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("该数据库实例已存在")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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操作 -------------------------
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
209
server/internal/db/dbm/dbi/metasql/mssql_meta.sql
Normal file
209
server/internal/db/dbm/dbi/metasql/mssql_meta.sql
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
--MSSQL_DBS 数据库名信息
|
||||||
|
SELECT name AS dbname
|
||||||
|
FROM sys.databases
|
||||||
|
WHERE owner_sid = SUSER_SID()
|
||||||
|
and name not in ('master', 'tempdb', 'model', 'msdb')
|
||||||
|
---------------------------------------
|
||||||
|
--MSSQL_TABLE_DETAIL 查询表名和表注释
|
||||||
|
SELECT t.name AS tableName,
|
||||||
|
ep.value AS tableComment
|
||||||
|
FROM sys.tables t
|
||||||
|
left OUTER JOIN sys.schemas ss on t.schema_id = ss.schema_id
|
||||||
|
LEFT OUTER JOIN
|
||||||
|
sys.extended_properties ep ON ep.major_id = t.object_id AND ep.minor_id = 0 AND ep.class = 1
|
||||||
|
WHERE ss.name = ?
|
||||||
|
and t.name = ?
|
||||||
|
---------------------------------------
|
||||||
|
--MSSQL_DB_SCHEMAS 数据库下所有schema
|
||||||
|
SELECT a.SCHEMA_NAME
|
||||||
|
FROM information_schema.schemata a
|
||||||
|
where a.catalog_name = DB_NAME()
|
||||||
|
and (a.SCHEMA_NAME in ('dbo', 'guest') or a.SCHEMA_NAME not like 'db_%')
|
||||||
|
and a.SCHEMA_NAME not in ('sys', 'INFORMATION_SCHEMA')
|
||||||
|
---------------------------------------
|
||||||
|
--MSSQL_TABLE_INFO 表详细信息
|
||||||
|
SELECT t.name AS tableName,
|
||||||
|
ss.name AS tableSchema,
|
||||||
|
c.value AS tableComment,
|
||||||
|
p.rows AS tableRows,
|
||||||
|
0 AS dataLength,
|
||||||
|
0 AS indexLength,
|
||||||
|
t.create_date AS createTime
|
||||||
|
FROM sys.tables t
|
||||||
|
left OUTER JOIN sys.schemas ss on t.schema_id = ss.schema_id
|
||||||
|
left OUTER JOIN sys.partitions p ON t.object_id = p.object_id AND p.index_id = 1
|
||||||
|
left OUTER JOIN sys.extended_properties c ON t.object_id = c.major_id AND c.minor_id = 0 AND c.class = 1
|
||||||
|
where ss.name = ?
|
||||||
|
ORDER BY t.name DESC;
|
||||||
|
---------------------------------------
|
||||||
|
--MSSQL_INDEX_INFO 索引信息
|
||||||
|
SELECT ind.name AS indexName,
|
||||||
|
col.name AS columnName,
|
||||||
|
CASE
|
||||||
|
WHEN ind.is_primary_key = 1 THEN 'CLUSTERED'
|
||||||
|
ELSE 'NON-CLUSTERED'
|
||||||
|
END AS indexType,
|
||||||
|
IIF(ind.is_unique = 'true', 1, 0) AS isUnique,
|
||||||
|
ic.key_ordinal AS seqInIndex,
|
||||||
|
idx.value AS indexComment
|
||||||
|
FROM sys.indexes ind
|
||||||
|
LEFT JOIN sys.tables t on t.object_id = ind.object_id
|
||||||
|
LEFT JOIN sys.schemas ss on t.schema_id = ss.schema_id
|
||||||
|
LEFT JOIN
|
||||||
|
sys.index_columns ic ON ind.object_id = ic.object_id AND ind.index_id = ic.index_id
|
||||||
|
LEFT JOIN
|
||||||
|
sys.columns col ON ind.object_id = col.object_id AND ic.column_id = col.column_id
|
||||||
|
LEFT JOIN
|
||||||
|
sys.extended_properties idx ON ind.object_id = idx.major_id AND ind.index_id = idx.minor_id AND idx.class = 7
|
||||||
|
WHERE ss.name = ?
|
||||||
|
and ind.name is not null
|
||||||
|
and t.name = ?
|
||||||
|
---------------------------------------
|
||||||
|
--MSSQL_COLUMN_MA 列信息元数据
|
||||||
|
SELECT t.name AS TABLE_NAME,
|
||||||
|
c.name AS COLUMN_NAME,
|
||||||
|
CASE
|
||||||
|
WHEN c.is_nullable = 1 THEN 'YES'
|
||||||
|
ELSE 'NO'
|
||||||
|
END AS NULLABLE,
|
||||||
|
tp.name +
|
||||||
|
CASE
|
||||||
|
WHEN tp.name IN ('char', 'varchar', 'nchar', 'nvarchar') THEN '(' + CASE
|
||||||
|
WHEN c.max_length = -1 THEN 'max'
|
||||||
|
ELSE CAST(c.max_length AS NVARCHAR(255)) END +
|
||||||
|
')'
|
||||||
|
WHEN tp.name IN ('numeric', 'decimal') THEN '(' + CAST(c.precision AS NVARCHAR(255)) + ',' +
|
||||||
|
CAST(c.scale AS NVARCHAR(255)) + ')'
|
||||||
|
ELSE ''
|
||||||
|
END AS COLUMN_TYPE,
|
||||||
|
ep.value AS COLUMN_COMMENT,
|
||||||
|
COLUMN_DEFAULT = CASE
|
||||||
|
WHEN c.default_object_id IS NOT NULL THEN object_definition(c.default_object_id)
|
||||||
|
ELSE ''
|
||||||
|
END,
|
||||||
|
c.scale AS NUM_SCALE,
|
||||||
|
IS_IDENTITY = COLUMNPROPERTY(c.object_id, c.name, 'IsIdentity'),
|
||||||
|
IS_PRIMARY_KEY = CASE
|
||||||
|
WHEN (SELECT COUNT(*)
|
||||||
|
FROM sys.index_columns ic
|
||||||
|
INNER JOIN sys.indexes i
|
||||||
|
ON ic.index_id = i.index_id AND ic.object_id = i.object_id
|
||||||
|
WHERE ic.object_id = c.object_id
|
||||||
|
AND ic.column_id = c.column_id
|
||||||
|
AND i.is_primary_key = 1) > 0 THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END
|
||||||
|
FROM sys.tables t
|
||||||
|
INNER JOIN sys.schemas ss on t.schema_id = ss.schema_id
|
||||||
|
INNER JOIN
|
||||||
|
sys.columns c ON t.object_id = c.object_id
|
||||||
|
INNER JOIN
|
||||||
|
sys.types tp ON c.system_type_id = tp.system_type_id AND c.user_type_id = tp.user_type_id
|
||||||
|
LEFT JOIN
|
||||||
|
sys.extended_properties ep ON t.object_id = ep.major_id AND c.column_id = ep.minor_id AND ep.class = 1
|
||||||
|
WHERE ss.name = ?
|
||||||
|
and t.name in (%s)
|
||||||
|
ORDER BY t.name, c.column_id
|
||||||
|
---------------------------------------
|
||||||
|
--MSSQL_TABLE_DDL 建表ddl
|
||||||
|
declare
|
||||||
|
@tabname varchar(50)
|
||||||
|
set @tabname= ? --表名
|
||||||
|
if ( object_id('tempdb.dbo.#t') is not null)
|
||||||
|
begin
|
||||||
|
DROP TABLE #t
|
||||||
|
end
|
||||||
|
select 'create table [' + so.name + '] (' + o.list + ')'
|
||||||
|
+ CASE
|
||||||
|
WHEN tc.Constraint_Name IS NULL THEN ''
|
||||||
|
ELSE 'ALTER TABLE ' + so.Name + ' ADD CONSTRAINT ' + tc.Constraint_Name + ' PRIMARY KEY ' +
|
||||||
|
' (' + LEFT(j.List, Len(j.List)-1) + ')' END
|
||||||
|
TABLE_DDL
|
||||||
|
into #t
|
||||||
|
from sysobjects so
|
||||||
|
cross apply
|
||||||
|
(SELECT
|
||||||
|
' \n ['+ column_name +'] ' +
|
||||||
|
data_type + case data_type
|
||||||
|
when 'sql_variant' then ''
|
||||||
|
when 'text' then ''
|
||||||
|
when 'ntext' then ''
|
||||||
|
when 'xml' then ''
|
||||||
|
when 'decimal' then '(' + cast (numeric_precision as varchar) + ', ' + cast (numeric_scale as varchar) + ')'
|
||||||
|
else coalesce ('('+ case when character_maximum_length = -1 then 'MAX' else cast (character_maximum_length as varchar) end +')', '') end + ' ' +
|
||||||
|
case when exists (
|
||||||
|
select id from syscolumns
|
||||||
|
where object_name(id)=so.name
|
||||||
|
and name = column_name
|
||||||
|
and columnproperty(id, name, 'IsIdentity') = 1
|
||||||
|
) then
|
||||||
|
'IDENTITY(' +
|
||||||
|
cast (ident_seed(so.name) as varchar) + ',' +
|
||||||
|
cast (ident_incr(so.name) as varchar) + ')'
|
||||||
|
else ''
|
||||||
|
end + ' ' +
|
||||||
|
(case when IS_NULLABLE = 'No' then 'NOT ' else '' end ) + 'NULL ' +
|
||||||
|
case when information_schema.columns.COLUMN_DEFAULT IS NOT NULL THEN 'DEFAULT '+ information_schema.columns.COLUMN_DEFAULT ELSE '' END + ', '
|
||||||
|
from information_schema.columns where table_name = so.name
|
||||||
|
order by ordinal_position
|
||||||
|
FOR XML PATH ('')) o (list)
|
||||||
|
left join
|
||||||
|
information_schema.table_constraints tc
|
||||||
|
on tc.Table_name = so.Name
|
||||||
|
AND tc.Constraint_Type = 'PRIMARY KEY'
|
||||||
|
cross apply
|
||||||
|
(select '[' + Column_Name + '], '
|
||||||
|
FROM information_schema.key_column_usage kcu
|
||||||
|
WHERE kcu.Constraint_Name = tc.Constraint_Name
|
||||||
|
ORDER BY
|
||||||
|
ORDINAL_POSITION
|
||||||
|
FOR XML PATH ('')) j (list)
|
||||||
|
where xtype = 'U'
|
||||||
|
AND name =@tabname
|
||||||
|
|
||||||
|
select (
|
||||||
|
case
|
||||||
|
when (select count(a.constraint_type)
|
||||||
|
from information_schema.table_constraints a
|
||||||
|
inner join information_schema.constraint_column_usage b
|
||||||
|
on a.constraint_name = b.constraint_name
|
||||||
|
where a.constraint_type = 'PRIMARY KEY'--主键
|
||||||
|
and a.table_name = @tabname) = 1 then replace(table_ddl
|
||||||
|
, ', )ALTER TABLE'
|
||||||
|
, ')' + CHAR (13)+'ALTER TABLE')
|
||||||
|
else SUBSTRING(table_ddl
|
||||||
|
, 1
|
||||||
|
, len(table_ddl) - 3) + ')' end
|
||||||
|
) as TableDDL
|
||||||
|
from #t
|
||||||
|
|
||||||
|
drop table #t
|
||||||
|
---------------------------------------
|
||||||
|
--MSSQL_TABLE_INDEX_DDL 建索引ddl
|
||||||
|
DECLARE
|
||||||
|
@TableName NVARCHAR(255)
|
||||||
|
SET @TableName = ?;
|
||||||
|
|
||||||
|
SELECT 'CREATE ' +
|
||||||
|
CASE
|
||||||
|
WHEN i.is_primary_key = 1 THEN 'CLUSTERED '
|
||||||
|
WHEN i.type_desc = 'HEAP' THEN ''
|
||||||
|
ELSE 'NONCLUSTERED '
|
||||||
|
END +
|
||||||
|
'INDEX ' + i.name + ' ON ' + t.name + ' (' +
|
||||||
|
STUFF((SELECT ',' + c.name +
|
||||||
|
CASE
|
||||||
|
WHEN ic.is_descending_key = 1 THEN ' DESC'
|
||||||
|
ELSE ' ASC'
|
||||||
|
END
|
||||||
|
FROM sys.index_columns ic
|
||||||
|
INNER JOIN
|
||||||
|
sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
|
||||||
|
WHERE ic.object_id = i.object_id
|
||||||
|
AND ic.index_id = i.index_id
|
||||||
|
ORDER BY ic.key_ordinal
|
||||||
|
FOR XML PATH (''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '') + ');' AS IndexDDL
|
||||||
|
FROM sys.tables t
|
||||||
|
INNER JOIN
|
||||||
|
sys.indexes i ON t.object_id = i.object_id
|
||||||
|
WHERE t.name = @TableName;
|
||||||
@@ -30,7 +30,7 @@ SELECT
|
|||||||
index_name indexName,
|
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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
21
server/internal/db/dbm/dbi/metasql/sqlite_meta.sql
Normal file
21
server/internal/db/dbm/dbi/metasql/sqlite_meta.sql
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
--SQLITE_TABLE_INFO 表详细信息
|
||||||
|
select tbl_name as tableName,
|
||||||
|
'' as tableComment,
|
||||||
|
'' as createTime,
|
||||||
|
0 as dataLength,
|
||||||
|
0 as indexLength,
|
||||||
|
0 as tableRows
|
||||||
|
FROM sqlite_master
|
||||||
|
WHERE type = 'table'
|
||||||
|
and name not like 'sqlite_%'
|
||||||
|
ORDER BY tbl_name
|
||||||
|
---------------------------------------
|
||||||
|
--SQLITE_INDEX_INFO 表索引信息
|
||||||
|
select name as indexName,
|
||||||
|
`sql` as indexSql,
|
||||||
|
'normal' as indexType,
|
||||||
|
'' as indexComment
|
||||||
|
FROM sqlite_master
|
||||||
|
WHERE type = 'index'
|
||||||
|
and tbl_name = '%s'
|
||||||
|
ORDER BY name
|
||||||
@@ -4,10 +4,12 @@ import (
|
|||||||
"fmt"
|
"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获取连接
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
425
server/internal/db/dbm/mssql/dialect.go
Normal file
425
server/internal/db/dbm/mssql/dialect.go
Normal file
@@ -0,0 +1,425 @@
|
|||||||
|
package mssql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"mayfly-go/internal/db/dbm/dbi"
|
||||||
|
"mayfly-go/pkg/errorx"
|
||||||
|
"mayfly-go/pkg/logx"
|
||||||
|
"mayfly-go/pkg/utils/anyx"
|
||||||
|
"mayfly-go/pkg/utils/collx"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MSSQL_META_FILE = "metasql/mssql_meta.sql"
|
||||||
|
MSSQL_DBS_KEY = "MSSQL_DBS"
|
||||||
|
MSSQL_DB_SCHEMAS_KEY = "MSSQL_DB_SCHEMAS"
|
||||||
|
MSSQL_TABLE_INFO_KEY = "MSSQL_TABLE_INFO"
|
||||||
|
MSSQL_INDEX_INFO_KEY = "MSSQL_INDEX_INFO"
|
||||||
|
MSSQL_COLUMN_MA_KEY = "MSSQL_COLUMN_MA"
|
||||||
|
MSSQL_TABLE_DETAIL_KEY = "MSSQL_TABLE_DETAIL"
|
||||||
|
MSSQL_TABLE_INDEX_DDL_KEY = "MSSQL_TABLE_INDEX_DDL"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MssqlDialect struct {
|
||||||
|
dc *dbi.DbConn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (md *MssqlDialect) GetDbServer() (*dbi.DbServer, error) {
|
||||||
|
_, res, err := md.dc.Query("SELECT @@VERSION as version")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ds := &dbi.DbServer{
|
||||||
|
Version: anyx.ConvString(res[0]["version"]),
|
||||||
|
}
|
||||||
|
return ds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (md *MssqlDialect) GetDbNames() ([]string, error) {
|
||||||
|
_, res, err := md.dc.Query(dbi.GetLocalSql(MSSQL_META_FILE, MSSQL_DBS_KEY))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
databases := make([]string, 0)
|
||||||
|
for _, re := range res {
|
||||||
|
databases = append(databases, anyx.ConvString(re["dbname"]))
|
||||||
|
}
|
||||||
|
|
||||||
|
return databases, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从连接信息中获取数据库和schema信息
|
||||||
|
func (md *MssqlDialect) currentSchema() string {
|
||||||
|
dbName := md.dc.Info.Database
|
||||||
|
schema := ""
|
||||||
|
arr := strings.Split(dbName, "/")
|
||||||
|
if len(arr) == 2 {
|
||||||
|
schema = arr[1]
|
||||||
|
}
|
||||||
|
return schema
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取表基础元信息, 如表名等
|
||||||
|
func (md *MssqlDialect) GetTables() ([]dbi.Table, error) {
|
||||||
|
_, res, err := md.dc.Query(dbi.GetLocalSql(MSSQL_META_FILE, MSSQL_TABLE_INFO_KEY), md.currentSchema())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tables := make([]dbi.Table, 0)
|
||||||
|
for _, re := range res {
|
||||||
|
tables = append(tables, dbi.Table{
|
||||||
|
TableName: anyx.ConvString(re["tableName"]),
|
||||||
|
TableComment: anyx.ConvString(re["tableComment"]),
|
||||||
|
CreateTime: anyx.ConvString(re["createTime"]),
|
||||||
|
TableRows: anyx.ConvInt(re["tableRows"]),
|
||||||
|
DataLength: anyx.ConvInt64(re["dataLength"]),
|
||||||
|
IndexLength: anyx.ConvInt64(re["indexLength"]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return tables, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取列元信息, 如列名等
|
||||||
|
func (md *MssqlDialect) GetColumns(tableNames ...string) ([]dbi.Column, error) {
|
||||||
|
dbType := md.dc.Info.Type
|
||||||
|
tableName := strings.Join(collx.ArrayMap[string, string](tableNames, func(val string) string {
|
||||||
|
return fmt.Sprintf("'%s'", dbType.RemoveQuote(val))
|
||||||
|
}), ",")
|
||||||
|
|
||||||
|
_, res, err := md.dc.Query(fmt.Sprintf(dbi.GetLocalSql(MSSQL_META_FILE, MSSQL_COLUMN_MA_KEY), tableName), md.currentSchema())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
columns := make([]dbi.Column, 0)
|
||||||
|
for _, re := range res {
|
||||||
|
columns = append(columns, dbi.Column{
|
||||||
|
TableName: anyx.ToString(re["TABLE_NAME"]),
|
||||||
|
ColumnName: anyx.ToString(re["COLUMN_NAME"]),
|
||||||
|
ColumnType: anyx.ToString(re["COLUMN_TYPE"]),
|
||||||
|
ColumnComment: anyx.ToString(re["COLUMN_COMMENT"]),
|
||||||
|
Nullable: anyx.ToString(re["NULLABLE"]),
|
||||||
|
IsPrimaryKey: anyx.ConvInt(re["IS_PRIMARY_KEY"]) == 1,
|
||||||
|
IsIdentity: anyx.ConvInt(re["IS_IDENTITY"]) == 1,
|
||||||
|
ColumnDefault: anyx.ToString(re["COLUMN_DEFAULT"]),
|
||||||
|
NumScale: anyx.ToString(re["NUM_SCALE"]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return columns, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取表主键字段名,不存在主键标识则默认第一个字段
|
||||||
|
func (md *MssqlDialect) GetPrimaryKey(tablename string) (string, error) {
|
||||||
|
columns, err := md.GetColumns(tablename)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(columns) == 0 {
|
||||||
|
return "", errorx.NewBiz("[%s] 表不存在", tablename)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range columns {
|
||||||
|
if v.IsPrimaryKey {
|
||||||
|
return v.ColumnName, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return columns[0].ColumnName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取表索引信息
|
||||||
|
func (md *MssqlDialect) GetTableIndex(tableName string) ([]dbi.Index, error) {
|
||||||
|
_, res, err := md.dc.Query(dbi.GetLocalSql(MSSQL_META_FILE, MSSQL_INDEX_INFO_KEY), md.currentSchema(), tableName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
indexs := make([]dbi.Index, 0)
|
||||||
|
for _, re := range res {
|
||||||
|
indexs = append(indexs, dbi.Index{
|
||||||
|
IndexName: anyx.ConvString(re["indexName"]),
|
||||||
|
ColumnName: anyx.ConvString(re["columnName"]),
|
||||||
|
IndexType: anyx.ConvString(re["indexType"]),
|
||||||
|
IndexComment: anyx.ConvString(re["indexComment"]),
|
||||||
|
IsUnique: anyx.ConvInt(re["isUnique"]) == 1,
|
||||||
|
SeqInIndex: anyx.ConvInt(re["seqInIndex"]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 把查询结果以索引名分组,索引字段以逗号连接
|
||||||
|
result := make([]dbi.Index, 0)
|
||||||
|
key := ""
|
||||||
|
for _, v := range indexs {
|
||||||
|
// 当前的索引名
|
||||||
|
in := v.IndexName
|
||||||
|
// 过滤掉主键索引,主键索引名为PK__开头的
|
||||||
|
if strings.HasPrefix(in, "PK__") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if key == in {
|
||||||
|
// 索引字段已根据名称和顺序排序,故取最后一个即可
|
||||||
|
i := len(result) - 1
|
||||||
|
// 同索引字段以逗号连接
|
||||||
|
result[i].ColumnName = result[i].ColumnName + "," + v.ColumnName
|
||||||
|
} else {
|
||||||
|
key = in
|
||||||
|
result = append(result, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (md MssqlDialect) CopyTableDDL(tableName string, newTableName string) (string, error) {
|
||||||
|
if newTableName == "" {
|
||||||
|
newTableName = tableName
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据列信息生成建表语句
|
||||||
|
var builder strings.Builder
|
||||||
|
var commentBuilder strings.Builder
|
||||||
|
|
||||||
|
// 查询表名和表注释, 设置表注释
|
||||||
|
_, res, err := md.dc.Query(dbi.GetLocalSql(MSSQL_META_FILE, MSSQL_TABLE_DETAIL_KEY), md.currentSchema(), tableName)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
tableComment := ""
|
||||||
|
if len(res) > 0 {
|
||||||
|
tableComment = anyx.ToString(res[0]["tableComment"])
|
||||||
|
if tableComment != "" {
|
||||||
|
// 注释转义单引号
|
||||||
|
tableComment = strings.ReplaceAll(tableComment, "'", "\\'")
|
||||||
|
commentBuilder.WriteString(fmt.Sprintf("\nEXEC sp_addextendedproperty N'MS_Description', N'%s', N'SCHEMA', N'%s', N'TABLE',N'%s';\n", tableComment, md.currentSchema(), newTableName))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
baseTable := fmt.Sprintf("%s.%s", md.dc.Info.Type.QuoteIdentifier(md.currentSchema()), md.dc.Info.Type.QuoteIdentifier(newTableName))
|
||||||
|
|
||||||
|
// 查询列信息
|
||||||
|
columns, err := md.GetColumns(tableName)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.WriteString(fmt.Sprintf("CREATE TABLE %s (\n", baseTable))
|
||||||
|
pks := make([]string, 0)
|
||||||
|
for i, v := range columns {
|
||||||
|
nullAble := "NULL"
|
||||||
|
if v.Nullable == "NO" {
|
||||||
|
nullAble = "NOT NULL"
|
||||||
|
}
|
||||||
|
builder.WriteString(fmt.Sprintf("\t[%s] %s %s", v.ColumnName, v.ColumnType, nullAble))
|
||||||
|
if v.IsIdentity {
|
||||||
|
builder.WriteString(" IDENTITY(1,11)")
|
||||||
|
}
|
||||||
|
if v.ColumnDefault != "" {
|
||||||
|
builder.WriteString(fmt.Sprintf(" DEFAULT %s", v.ColumnDefault))
|
||||||
|
}
|
||||||
|
if v.IsPrimaryKey {
|
||||||
|
pks = append(pks, fmt.Sprintf("[%s]", v.ColumnName))
|
||||||
|
}
|
||||||
|
if i < len(columns)-1 {
|
||||||
|
builder.WriteString(",")
|
||||||
|
}
|
||||||
|
builder.WriteString("\n")
|
||||||
|
}
|
||||||
|
// 设置主键
|
||||||
|
if len(pks) > 0 {
|
||||||
|
builder.WriteString(fmt.Sprintf("\tCONSTRAINT PK_%s PRIMARY KEY ( %s )", newTableName, strings.Join(pks, ",")))
|
||||||
|
}
|
||||||
|
builder.WriteString("\n);\n")
|
||||||
|
|
||||||
|
// 设置字段注释
|
||||||
|
for _, v := range columns {
|
||||||
|
if v.ColumnComment != "" {
|
||||||
|
// 注释转义单引号
|
||||||
|
v.ColumnComment = strings.ReplaceAll(v.ColumnComment, "'", "\\'")
|
||||||
|
commentBuilder.WriteString(fmt.Sprintf("\nEXEC sp_addextendedproperty N'MS_Description', N'%s', N'SCHEMA', N'%s', N'TABLE',N'%s', N'COLUMN', N'%s';\n", v.ColumnComment, md.currentSchema(), newTableName, v.ColumnName))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置索引
|
||||||
|
indexs, err := md.GetTableIndex(tableName)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
for _, v := range indexs {
|
||||||
|
builder.WriteString(fmt.Sprintf("\nCREATE NONCLUSTERED INDEX [%s] ON %s (%s);\n", v.IndexName, baseTable, v.ColumnName))
|
||||||
|
// 设置索引注释
|
||||||
|
if v.IndexComment != "" {
|
||||||
|
// 注释转义单引号
|
||||||
|
v.IndexComment = strings.ReplaceAll(v.IndexComment, "'", "\\'")
|
||||||
|
commentBuilder.WriteString(fmt.Sprintf("\nEXEC sp_addextendedproperty N'MS_Description', N'%s', N'SCHEMA', N'%s', N'TABLE',N'%s', N'INDEX', N'%s';\n", v.IndexComment, md.currentSchema(), newTableName, v.IndexName))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return builder.String() + commentBuilder.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取建表ddl
|
||||||
|
func (md *MssqlDialect) GetTableDDL(tableName string) (string, error) {
|
||||||
|
return md.CopyTableDDL(tableName, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (md *MssqlDialect) WalkTableRecord(tableName string, walkFn dbi.WalkQueryRowsFunc) error {
|
||||||
|
return md.dc.WalkQueryRows(context.Background(), fmt.Sprintf("SELECT * FROM %s", tableName), walkFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (md *MssqlDialect) GetSchemas() ([]string, error) {
|
||||||
|
_, res, err := md.dc.Query(dbi.GetLocalSql(MSSQL_META_FILE, MSSQL_DB_SCHEMAS_KEY))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
schemas := make([]string, 0)
|
||||||
|
for _, re := range res {
|
||||||
|
schemas = append(schemas, anyx.ConvString(re["SCHEMA_NAME"]))
|
||||||
|
}
|
||||||
|
return schemas, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDbProgram 获取数据库程序模块,用于数据库备份与恢复
|
||||||
|
func (md *MssqlDialect) GetDbProgram() dbi.DbProgram {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (md *MssqlDialect) BatchInsert(tx *sql.Tx, tableName string, columns []string, values [][]any) (int64, error) {
|
||||||
|
schema := md.currentSchema()
|
||||||
|
|
||||||
|
// 生成占位符字符串:如:(?,?)
|
||||||
|
// 重复字符串并用逗号连接
|
||||||
|
repeated := strings.Repeat("?,", len(columns))
|
||||||
|
// 去除最后一个逗号,占位符由括号包裹
|
||||||
|
placeholder := fmt.Sprintf("(%s)", strings.TrimSuffix(repeated, ","))
|
||||||
|
|
||||||
|
// 重复占位符字符串n遍
|
||||||
|
repeated = strings.Repeat(placeholder+",", len(values))
|
||||||
|
// 去除最后一个逗号
|
||||||
|
placeholder = strings.TrimSuffix(repeated, ",")
|
||||||
|
|
||||||
|
baseTable := fmt.Sprintf("%s.%s", md.dc.Info.Type.QuoteIdentifier(schema), md.dc.Info.Type.QuoteIdentifier(tableName))
|
||||||
|
|
||||||
|
sqlStr := fmt.Sprintf("insert into %s (%s) values %s", baseTable, strings.Join(columns, ","), placeholder)
|
||||||
|
// 执行批量insert sql
|
||||||
|
// 把二维数组转为一维数组
|
||||||
|
var args []any
|
||||||
|
for _, v := range values {
|
||||||
|
args = append(args, v...)
|
||||||
|
}
|
||||||
|
// 设置允许填充自增列之后,显示指定列名可以插入自增列
|
||||||
|
_, _ = md.dc.Exec(fmt.Sprintf("set identity_insert \"%s\" on", tableName))
|
||||||
|
|
||||||
|
exec, err := md.dc.TxExec(tx, sqlStr, args...)
|
||||||
|
|
||||||
|
_, _ = md.dc.Exec(fmt.Sprintf("set identity_insert \"%s\" off", tableName))
|
||||||
|
|
||||||
|
return exec, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (md *MssqlDialect) GetDataConverter() dbi.DataConverter {
|
||||||
|
return new(DataConverter)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// 数字类型
|
||||||
|
numberRegexp = regexp.MustCompile(`(?i)int|double|float|number|decimal|byte|bit`)
|
||||||
|
// 日期时间类型
|
||||||
|
datetimeRegexp = regexp.MustCompile(`(?i)datetime|timestamp`)
|
||||||
|
// 日期类型
|
||||||
|
dateRegexp = regexp.MustCompile(`(?i)date`)
|
||||||
|
// 时间类型
|
||||||
|
timeRegexp = regexp.MustCompile(`(?i)time`)
|
||||||
|
)
|
||||||
|
|
||||||
|
type DataConverter struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc *DataConverter) GetDataType(dbColumnType string) dbi.DataType {
|
||||||
|
if numberRegexp.MatchString(dbColumnType) {
|
||||||
|
return dbi.DataTypeNumber
|
||||||
|
}
|
||||||
|
// 日期时间类型
|
||||||
|
if datetimeRegexp.MatchString(dbColumnType) {
|
||||||
|
return dbi.DataTypeDateTime
|
||||||
|
}
|
||||||
|
// 日期类型
|
||||||
|
if dateRegexp.MatchString(dbColumnType) {
|
||||||
|
return dbi.DataTypeDate
|
||||||
|
}
|
||||||
|
// 时间类型
|
||||||
|
if timeRegexp.MatchString(dbColumnType) {
|
||||||
|
return dbi.DataTypeTime
|
||||||
|
}
|
||||||
|
return dbi.DataTypeString
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc *DataConverter) FormatData(dbColumnValue any, dataType dbi.DataType) string {
|
||||||
|
return anyx.ToString(dbColumnValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc *DataConverter) ParseData(dbColumnValue any, dataType dbi.DataType) any {
|
||||||
|
return dbColumnValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (md *MssqlDialect) CopyTable(copy *dbi.DbCopyTable) error {
|
||||||
|
|
||||||
|
schema := md.currentSchema()
|
||||||
|
|
||||||
|
// 生成新表名,为老表明+_copy_时间戳
|
||||||
|
newTableName := copy.TableName + "_copy_" + time.Now().Format("20060102150405")
|
||||||
|
|
||||||
|
// 复制建表语句
|
||||||
|
ddl, err := md.CopyTableDDL(copy.TableName, newTableName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行建表
|
||||||
|
_, err = md.dc.Exec(ddl)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// 复制数据
|
||||||
|
if copy.CopyData {
|
||||||
|
go func() {
|
||||||
|
// 查询所有的列
|
||||||
|
columns, err := md.GetColumns(copy.TableName)
|
||||||
|
if err != nil {
|
||||||
|
logx.Warnf("复制表[%s]数据失败: %s", copy.TableName, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 取出每列名, 需要显示指定列名插入数据
|
||||||
|
columnNames := make([]string, 0)
|
||||||
|
hasIdentity := false
|
||||||
|
for _, v := range columns {
|
||||||
|
columnNames = append(columnNames, fmt.Sprintf("[%s]", v.ColumnName))
|
||||||
|
if v.IsIdentity {
|
||||||
|
hasIdentity = true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
columnsSql := strings.Join(columnNames, ",")
|
||||||
|
|
||||||
|
// 复制数据
|
||||||
|
// 设置允许填充自增列之后,显示指定列名可以插入自增列
|
||||||
|
identityInsertOn := ""
|
||||||
|
identityInsertOff := ""
|
||||||
|
if hasIdentity {
|
||||||
|
identityInsertOn = fmt.Sprintf("SET IDENTITY_INSERT [%s].[%s] ON", schema, newTableName)
|
||||||
|
identityInsertOff = fmt.Sprintf("SET IDENTITY_INSERT [%s].[%s] OFF", schema, newTableName)
|
||||||
|
}
|
||||||
|
_, err = md.dc.Exec(fmt.Sprintf(" %s INSERT INTO [%s].[%s] (%s) SELECT * FROM [%s].[%s] %s", identityInsertOn, schema, newTableName, columnsSql, schema, copy.TableName, identityInsertOff))
|
||||||
|
if err != nil {
|
||||||
|
logx.Warnf("复制表[%s]数据失败: %s", copy.TableName, err.Error())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user