Merge branch 'master' into dev

This commit is contained in:
meilin.huang
2024-01-29 12:21:22 +08:00
43 changed files with 1495 additions and 240 deletions

File diff suppressed because one or more lines are too long

View File

@@ -74,6 +74,13 @@
"font_class": "sqlite",
"unicode": "e546",
"unicode_decimal": 58694
},
{
"icon_id": "29340317",
"name": "temp-mssql",
"font_class": "MSSQLNATIVE",
"unicode": "e600",
"unicode_decimal": 58880
}
]
}

View File

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

View File

@@ -156,27 +156,31 @@ const dbForm: any = ref(null);
const dbTypes = [
{
type: 'mysql',
label: 'mysql',
label: 'MySQL',
},
{
type: 'mariadb',
label: 'mariadb',
label: 'MariaDB',
},
{
type: 'postgres',
label: 'postgres',
label: 'PostgreSQL',
},
{
type: 'dm',
label: '达梦',
label: 'DM',
},
{
type: 'oracle',
label: 'oracle',
label: 'Oracle',
},
{
type: 'sqlite',
label: 'sqlite',
label: 'Sqlite',
},
{
type: 'mssql',
label: 'MSSQL',
},
];
@@ -196,17 +200,17 @@ const state = reactive({
remark: '',
sshTunnelMachineId: null as any,
},
subimtForm: {},
submitForm: {},
// 原密码
pwd: '',
// 原用户名
oldUserName: null,
});
const { dialogVisible, tabActiveName, form, subimtForm, pwd } = toRefs(state);
const { dialogVisible, tabActiveName, form, submitForm, pwd } = toRefs(state);
const { isFetching: saveBtnLoading, execute: saveInstanceExec } = dbApi.saveInstance.useApi(subimtForm);
const { isFetching: testConnBtnLoading, execute: testConnExec } = dbApi.testConn.useApi(subimtForm);
const { isFetching: saveBtnLoading, execute: saveInstanceExec } = dbApi.saveInstance.useApi(submitForm);
const { isFetching: testConnBtnLoading, execute: testConnExec } = dbApi.testConn.useApi(submitForm);
watch(props, (newValue: any) => {
state.dialogVisible = newValue.visible;
@@ -249,7 +253,7 @@ const testConn = async () => {
return false;
}
state.subimtForm = await getReqForm();
state.submitForm = await getReqForm();
await testConnExec();
ElMessage.success('连接成功');
});
@@ -270,7 +274,7 @@ const btnOk = async () => {
return false;
}
state.subimtForm = await getReqForm();
state.submitForm = await getReqForm();
await saveInstanceExec();
ElMessage.success('保存成功');
emit('val-change', state.form);

View File

@@ -165,8 +165,8 @@
</template>
<script lang="ts" setup>
import { defineAsyncComponent, onBeforeUnmount, onMounted, reactive, ref, toRefs } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { defineAsyncComponent, h, onBeforeUnmount, onMounted, reactive, ref, toRefs } from 'vue';
import { ElCheckbox, ElMessage, ElMessageBox } from 'element-plus';
import { formatByteSize } from '@/common/utils/format';
import { DbInst, registerDbCompletionItemProvider, TabInfo, TabType } from './db';
import { NodeType, TagTreeNode } from '../component/tag';
@@ -175,7 +175,7 @@ import { dbApi } from './api';
import { dispposeCompletionItemProvider } from '@/components/monaco/completionItemProvider';
import SvgIcon from '@/components/svgIcon/index.vue';
import { ContextmenuItem } from '@/components/contextmenu';
import { getDbDialect, schemaDbTypes} from './dialect/index'
import { getDbDialect, schemaDbTypes } from './dialect/index';
import { sleep } from '@/common/utils/loading';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { Pane, Splitpanes } from 'splitpanes';
@@ -229,21 +229,25 @@ const nodeClickChangeDb = (nodeData: TagTreeNode) => {
}
};
// tagpath 节点类型
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const dbInfoRes = await dbApi.dbs.request({ tagPath: parentNode.key });
const dbInfos = dbInfoRes.list;
if (!dbInfos) {
return [];
}
const ContextmenuItemRefresh = new ContextmenuItem('refresh', '刷新').withIcon('RefreshRight').withOnClick((data: any) => reloadNode(data.key));
// 防止过快加载会出现一闪而过,对眼睛不好
await sleep(100);
return dbInfos?.map((x: any) => {
x.tagPath = parentNode.key;
return new TagTreeNode(`${parentNode.key}.${x.id}`, x.name, NodeTypeDbInst).withParams(x);
});
});
// tagpath 节点类型
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath)
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const dbInfoRes = await dbApi.dbs.request({ tagPath: parentNode.key });
const dbInfos = dbInfoRes.list;
if (!dbInfos) {
return [];
}
// 防止过快加载会出现一闪而过,对眼睛不好
await sleep(100);
return dbInfos?.map((x: any) => {
x.tagPath = parentNode.key;
return new TagTreeNode(`${parentNode.key}.${x.id}`, x.name, NodeTypeDbInst).withParams(x);
});
})
.withContextMenuItems([ContextmenuItemRefresh]);
// 数据库实例节点类型
const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((parentNode: TagTreeNode) => {
@@ -266,7 +270,7 @@ const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((p
// 数据库节点
const NodeTypeDb = new NodeType(SqlExecNodeType.Db)
.withContextMenuItems([new ContextmenuItem('reloadTables', '刷新').withIcon('RefreshRight').withOnClick((data: any) => reloadNode(data.key))])
.withContextMenuItems([ContextmenuItemRefresh])
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const params = parentNode.params;
params.parentKey = parentNode.key;
@@ -288,17 +292,17 @@ const NodeTypeDb = new NodeType(SqlExecNodeType.Db)
.withNodeClickFunc(nodeClickChangeDb);
const NodeTypeTables = (params: any) => {
let tableKey = `${params.id}.${params.db}.table-menu`;
let sqlKey = getSqlMenuNodeKey(params.id, params.db);
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),
new TagTreeNode(`${params.id}.${params.db}.table-menu`, '表', NodeTypeTableMenu).withParams({ ...params, key: tableKey }).withIcon(TableIcon),
new TagTreeNode(sqlKey, 'SQL', NodeTypeSqlMenu).withParams({ ...params, key: sqlKey }).withIcon(SqlIcon),
];
};
// postgres schema模式
const NodeTypePostgresSchema = new NodeType(SqlExecNodeType.PgSchema)
.withContextMenuItems([new ContextmenuItem('reloadTables', '刷新').withIcon('RefreshRight').withOnClick((data: any) => reloadNode(data.key))])
.withContextMenuItems([ContextmenuItemRefresh])
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const params = parentNode.params;
params.parentKey = parentNode.key;
@@ -309,7 +313,7 @@ const NodeTypePostgresSchema = new NodeType(SqlExecNodeType.PgSchema)
// 数据库表菜单节点
const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
.withContextMenuItems([
new ContextmenuItem('reloadTables', '刷新').withIcon('RefreshRight').withOnClick((data: any) => reloadNode(data.key)),
ContextmenuItemRefresh,
new ContextmenuItem('createTable', '创建表').withIcon('Plus').withOnClick((data: any) => onEditTable(data)),
new ContextmenuItem('tablesOp', '表操作').withIcon('Setting').withOnClick((data: any) => {
const params = data.params;
@@ -676,12 +680,34 @@ const onDeleteTable = async (data: any) => {
const onCopyTable = async (data: any) => {
let { db, id, tableName, parentKey } = data.params;
// 执行sql
dbApi.copyTable.request({ id, db, tableName, copyData:true }).then(() => {
ElMessage.success('复制成功');
setTimeout(() => {
parentKey && reloadNode(parentKey);
}, 1000);
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);
});
}
},
});
};

View File

@@ -19,7 +19,7 @@ import { NodeType, TagTreeNode } from '@/views/ops/component/tag';
import { dbApi } from '@/views/ops/db/api';
import { sleep } from '@/common/utils/loading';
import SvgIcon from '@/components/svgIcon/index.vue';
import { getDbDialect, mysqlDbTypes} from '@/views/ops/db/dialect'
import { getDbDialect, noSchemaTypes } from '@/views/ops/db/dialect';
import TagTreeResourceSelect from '../../component/TagTreeResourceSelect.vue';
import { computed } from 'vue';
@@ -90,8 +90,8 @@ const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(asyn
});
/** mysql类型的数据库没有schema层 */
const mysqlType = (type: string) => {
return mysqlDbTypes.includes(type);
const noSchemaType = (type: string) => {
return noSchemaTypes.includes(type);
};
// 数据库实例节点类型
@@ -99,7 +99,7 @@ const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((p
const params = parentNode.params;
const dbs = params.database.split(' ')?.sort();
let fn: NodeType;
if (mysqlType(params.type)) {
if (noSchemaType(params.type)) {
fn = MysqlNodeTypes;
} else {
fn = PgNodeTypes;
@@ -117,7 +117,7 @@ const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((p
db: x,
})
.withIcon(DbIcon);
if (mysqlType(params.type)) {
if (noSchemaType(params.type)) {
tagTreeNode.isLeaf = true;
}
return tagTreeNode;

View File

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

View File

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

View File

@@ -715,9 +715,13 @@ const submitUpdateFields = async () => {
const db = state.db;
let res = '';
const dbDialect = getDbDialect(dbInst.type);
let schema = '';
let dbArr = db.split('/');
if (dbArr.length == 2) {
schema = dbInst.wrapName(dbArr[1]) + '.';
}
for (let updateRow of cellUpdateMap.values()) {
let sql = `UPDATE ${dbInst.wrapName(state.table)} SET `;
let sql = `UPDATE ${schema}${dbInst.wrapName(state.table)} SET `;
const rowData = updateRow.rowData;
// 主键列信息
const primaryKey = await dbInst.loadTableColumn(db, state.table);

View File

@@ -170,8 +170,8 @@
:page-sizes="pageSizes"
></el-pagination>
</el-row>
<div style="font-size: 12px; padding: 0 10px; color: #606266">
<span>{{ state.sql }}</span>
<div style="padding: 0 10px">
<span style="color: var(--el-color-info-light-3)" class="font10 el-text el-text--small is-truncated">{{ state.sql }}</span>
</div>
<el-dialog v-model="conditionDialog.visible" :title="conditionDialog.title" width="420px">
@@ -211,13 +211,14 @@
class="w100 mb5"
:prop="column.columnName"
:label="column.columnName"
:required="column.nullable != 'YES' && column.columnKey != 'PRI'"
:required="column.nullable != 'YES' && !column.isPrimaryKey && !column.isIdentity"
>
<ColumnFormItem
v-model="addDataDialog.data[`${column.columnName}`]"
:data-type="dbDialect.getDataType(column.columnType)"
:placeholder="`${column.columnType} ${column.columnComment}`"
:column-name="column.columnName"
:disabled="column.isIdentity"
/>
</el-form-item>
</el-form>
@@ -372,7 +373,7 @@ const selectData = async () => {
const countRes = await dbInst.runSql(db, dbInst.getDefaultCountSql(table, state.condition));
state.count = parseInt(countRes.res[0].count || countRes.res[0].COUNT || 0);
let sql = dbInst.getDefaultSelectSql(table, state.condition, state.orderBy, state.pageNum, state.pageSize);
let sql = dbInst.getDefaultSelectSql(db, table, state.condition, state.orderBy, state.pageNum, state.pageSize);
state.sql = sql;
if (state.count > 0) {
const colAndData: any = await dbInst.runSql(db, sql);
@@ -566,7 +567,13 @@ const addRow = async () => {
}
let columnNames = Object.keys(obj).join(',');
let values = Object.values(obj).join(',');
let sql = `INSERT INTO ${dbInst.wrapName(props.tableName)} (${columnNames}) VALUES (${values});`;
// 获取schema
let schema = '';
let arr = props.dbName?.split('/');
if (arr && arr.length > 1) {
schema = dbInst.wrapName(arr[1]) + '.';
}
let sql = `INSERT INTO ${schema}${dbInst.wrapName(props.tableName)} (${columnNames}) VALUES (${values});`;
dbInst.promptExeSql(props.dbName, sql, null, () => {
closeAddDataDialog();
onRefresh();

View File

@@ -56,7 +56,7 @@
v-else-if="item.prop === 'auto_increment'"
size="small"
v-model="scope.row.auto_increment"
:disabled="dbType === DbType.postgresql"
:disabled="disableEditIncr()"
/>
<el-input v-else-if="item.prop === 'remark'" size="small" v-model="scope.row.remark" />
@@ -99,9 +99,7 @@
<el-checkbox v-if="item.prop === 'unique'" size="small" v-model="scope.row.unique" @change="indexChanges(scope.row)">
</el-checkbox>
<el-select v-if="item.prop === 'indexType'" disabled size="small" v-model="scope.row.indexType">
<el-option v-for="typeValue in indexTypeList" :key="typeValue" :value="typeValue">{{ typeValue }}</el-option>
</el-select>
<el-input v-if="item.prop === 'indexType'" disabled size="small" v-model="scope.row.indexType" />
<el-input v-if="item.prop === 'indexComment'" size="small" v-model="scope.row.indexComment"> </el-input>
@@ -132,7 +130,7 @@
import { reactive, ref, toRefs, watch } from 'vue';
import { ElMessage } from 'element-plus';
import SqlExecBox from '../sqleditor/SqlExecBox';
import { DbDialect, DbType, getDbDialect, IndexDefinition, RowDefinition } from '../../dialect/index';
import { DbType, getDbDialect, IndexDefinition, RowDefinition } from '../../dialect/index';
const props = defineProps({
visible: {
@@ -172,7 +170,6 @@ const state = reactive({
btnloading: false,
activeName: '1',
columnTypeList: dbDialect.getInfo().columnTypes,
indexTypeList: ['BTREE', 'NORMAL'], // mysql索引类型详解 http://c.biancheng.net/view/7897.html
tableData: {
fields: {
colNames: [
@@ -268,7 +265,7 @@ const state = reactive({
},
});
const { dialogVisible, btnloading, activeName, indexTypeList, tableData } = toRefs(state);
const { dialogVisible, btnloading, activeName, tableData } = toRefs(state);
watch(props, async (newValue) => {
state.dialogVisible = newValue.visible;
@@ -408,7 +405,7 @@ const genSql = () => {
} else if (state.activeName === '2') {
// 修改索引
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);
}
}
};
@@ -418,28 +415,8 @@ const reset = () => {
formRef.value.resetFields();
state.tableData.tableName = '';
state.tableData.tableComment = '';
state.tableData.fields.res = [
{
name: '',
type: '',
value: '',
length: '',
numScale: '',
notNull: false,
pri: false,
auto_increment: false,
remark: '',
},
];
state.tableData.indexs.res = [
{
indexName: '',
columnNames: [],
unique: false,
indexType: 'BTREE',
indexComment: '',
},
];
state.tableData.fields.res = [];
state.tableData.indexs.res = [];
};
const indexChanges = (row: any) => {
@@ -460,6 +437,21 @@ const indexChanges = (row: any) => {
row.indexComment = `${tableData.value.tableName}表(${name.replaceAll('_', ',')})${commentSuffix}`;
};
const disableEditIncr = () => {
if (DbType.postgresql === props.dbType) {
return true;
}
// 如果是mssql则不能修改自增
if (props.data?.edit) {
if (DbType.mssql === props.dbType) {
return true;
}
}
return false;
};
watch(
() => props.data,
(newValue: any) => {
@@ -492,8 +484,8 @@ watch(
length,
numScale: a.numScale,
notNull: a.nullable !== 'YES',
pri: a.columnKey === 'PRI',
auto_increment: a.columnKey === 'PRI' /*a.extra?.indexOf('auto_increment') > -1*/,
pri: a.isPrimaryKey,
auto_increment: a.isIdentity /*a.extra?.indexOf('auto_increment') > -1*/,
remark: a.columnComment,
};
state.tableData.fields.res.push(data);
@@ -513,7 +505,7 @@ watch(
let data = {
indexName: a.indexName,
columnNames: a.columnName?.split(','),
unique: a.nonUnique === 0 || false,
unique: a.isUnique || false,
indexType: a.indexType,
indexComment: a.indexComment,
};

View File

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

View File

@@ -375,12 +375,12 @@ class DMDialect implements DbDialect {
return dmDialectInfo;
}
getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
return `SELECT * FROM "${table}" ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''} ${this.getPageSql(pageNum, limit)};`;
}
getPageSql(pageNum: number, limit: number) {
return ` OFFSET ${(pageNum - 1) * limit} LIMIT ${limit};`;
return ` OFFSET ${(pageNum - 1) * limit} LIMIT ${limit}`;
}
getDefaultRows(): RowDefinition[] {
@@ -620,7 +620,7 @@ class DMDialect implements DbDialect {
return dropPkSql + modifySql + dropSql + renameSql + addPkSql + commentSql;
}
getModifyIndexSql(tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
// 不能直接修改索引名或字段、需要先删后加
let dropIndexNames: string[] = [];
let addIndexs: any[] = [];

View File

@@ -4,6 +4,7 @@ import { DMDialect } from '@/views/ops/db/dialect/dm_dialect';
import { OracleDialect } from '@/views/ops/db/dialect/oracle_dialect';
import { MariadbDialect } from '@/views/ops/db/dialect/mariadb_dialect';
import { SqliteDialect } from '@/views/ops/db/dialect/sqlite_dialect';
import { MssqlDialect } from '@/views/ops/db/dialect/mssql_dialect';
export interface sqlColumnType {
udtName: string;
@@ -113,15 +114,16 @@ export const DbType = {
dm: 'dm', // 达梦
oracle: 'oracle',
sqlite: 'sqlite',
mssql: 'mssql', // ms sqlserver
};
// mysql兼容的数据库
export const mysqlDbTypes = [DbType.mysql, DbType.mariadb, DbType.sqlite];
export const noSchemaTypes = [DbType.mysql, DbType.mariadb, DbType.sqlite];
// 有schema层的数据库
export const schemaDbTypes = [DbType.postgresql, DbType.dm, DbType.oracle];
export const schemaDbTypes = [DbType.postgresql, DbType.dm, DbType.oracle, DbType.mssql];
export const editDbTypes = [...mysqlDbTypes, ...schemaDbTypes];
export const editDbTypes = [...noSchemaTypes, ...schemaDbTypes];
export const compatibleMysql = (dbType: string): boolean => {
switch (dbType) {
@@ -141,13 +143,14 @@ export interface DbDialect {
/**
* 获取默认查询sql
* @param db 数据库信息
* @param table 表名
* @param condition 条件
* @param orderBy 排序
* @param pageNum 页数
* @param limit 条数
*/
getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number): string;
getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number): string;
getPageSql(pageNum: number, limit: number): string;
@@ -183,10 +186,11 @@ export interface DbDialect {
/**
* 生成编辑索引sql
* @param tableData 表数据,包含表名、列数据、索引数据
* @param tableName 表名
* @param changeData 改变数据
*/
getModifyIndexSql(tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string;
getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string;
/** 通过数据库字段类型,返回基本数据类型 */
getDataType(columnType: string): DataType;
@@ -201,6 +205,7 @@ let postgresDialect = new PostgresqlDialect();
let dmDialect = new DMDialect();
let oracleDialect = new OracleDialect();
let sqliteDialect = new SqliteDialect();
let mssqlDialect = new MssqlDialect();
export const getDbDialect = (dbType: string | undefined): DbDialect => {
if (!dbType) {
@@ -219,6 +224,8 @@ export const getDbDialect = (dbType: string | undefined): DbDialect => {
return oracleDialect;
case DbType.sqlite:
return sqliteDialect;
case DbType.mssql:
return mssqlDialect;
default:
throw new Error('不支持的数据库');
}

View File

@@ -0,0 +1,404 @@
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 = {
icon: 'iconfont icon-MSSQLNATIVE',
defaultPort: 1433,
formatSqlDialect: 'transactsql',
columnTypes: MSSQL_TYPE_LIST.map((a) => ({ udtName: a, dataType: a, desc: '', space: '' })),
editorCompletions,
};
return mssqlDialectInfo;
}
getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
let schema = db.split('/')[1];
return `SELECT *, 0 AS _MAY_ORDER_F_ FROM ${this.quoteIdentifier(schema)}.${this.quoteIdentifier(table)} ${condition ? 'WHERE ' + condition : ''} ${
orderBy ? orderBy + ', _MAY_ORDER_F_' : 'order by _MAY_ORDER_F_'
} ${this.getPageSql(pageNum, limit)};`.toUpperCase();
}
getPageSql(pageNum: number, limit: number) {
return ` offset ${(pageNum - 1) * limit} rows fetch next ${limit} rows only`.toUpperCase();
}
getDefaultRows(): RowDefinition[] {
return [
{ name: 'id', type: 'bigint', length: '', numScale: '', value: '', notNull: true, pri: true, auto_increment: true, remark: '主键ID' },
{ name: 'creator_id', type: 'bigint', length: '20', numScale: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '创建人id' },
{
name: 'creator',
type: 'varchar',
length: '100',
numScale: '',
value: '',
notNull: true,
pri: false,
auto_increment: false,
remark: '创建人姓名',
},
{
name: 'create_time',
type: 'datetime2',
length: '',
numScale: '',
value: 'CURRENT_TIMESTAMP',
notNull: true,
pri: false,
auto_increment: false,
remark: '创建时间',
},
{ name: 'updator_id', type: 'bigint', length: '20', numScale: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '修改人id' },
{
name: 'updator',
type: 'varchar',
length: '100',
numScale: '',
value: '',
notNull: true,
pri: false,
auto_increment: false,
remark: '修改人姓名',
},
{
name: 'update_time',
type: 'datetime2',
length: '',
numScale: '',
value: 'CURRENT_TIMESTAMP',
notNull: true,
pri: false,
auto_increment: false,
remark: '修改时间',
},
];
}
getDefaultIndex(): IndexDefinition {
return {
indexName: '',
columnNames: [],
unique: false,
indexType: 'NONCLUSTERED',
indexComment: '',
};
}
quoteIdentifier = (name: string) => {
return `[${name}]`;
};
genColumnBasicSql(cl: any): string {
let val = cl.value ? (cl.value === 'CURRENT_TIMESTAMP' ? cl.value : `'${cl.value}'`) : '';
let defVal = val ? `DEFAULT ${val}` : '';
// mssql哪些字段允许有长度
let length = '';
if (!fixedLengthTypes.includes(cl.type)) {
length = cl.length ? `(${cl.length})` : '';
}
return ` ${this.quoteIdentifier(cl.name)} ${cl.type}${length} ${cl.auto_increment ? 'IDENTITY(1,1)' : ''} ${defVal} ${cl.notNull ? 'NOT NULL' : 'NULL'} `;
}
getCreateTableSql(data: any): string {
let schema = data.db.split('/')[1];
// 创建表结构
let pks = [] as string[];
let fields: string[] = [];
let fieldComments: string[] = [];
data.fields.res.forEach((item: any) => {
item.name && fields.push(this.genColumnBasicSql(item));
item.remark &&
fieldComments.push(
`EXECUTE sp_addextendedproperty N'MS_Description', N'${item.remark}', N'SCHEMA', N'${schema}', N'TABLE', N'${data.tableName}', N'COLUMN', N'${item.name}'`
);
if (item.pri) {
pks.push(`${this.quoteIdentifier(item.name)}`);
}
});
let baseTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(data.tableName)}`;
// 建表语句
let createTable = `CREATE TABLE ${baseTable}
( ${fields.join(',')}
${pks.length > 0 ? `, PRIMARY KEY CLUSTERED (${pks.join(',')})` : ''}
);`;
let createIndexSql = this.getCreateIndexSql(data);
// 表注释
if (data.tableComment) {
createTable += ` EXECUTE sp_addextendedproperty N'MS_Description', N'${data.tableComment}', N'SCHEMA', N'${schema}', N'TABLE', N'${data.tableName}';`;
}
return createTable + createIndexSql + fieldComments.join(';');
}
getCreateIndexSql(data: any): string {
// CREATE UNIQUE NONCLUSTERED INDEX [aaa]
// ON [dbo].[无标题] (
// [id],
// [name]
// )
let schema = data.db.split('/')[1];
let baseTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(data.tableName)}`;
let indexComment = [] as string[];
// 创建索引
let sql: string[] = [];
data.indexs.res.forEach((a: any) => {
let columnNames = a.columnNames.map((b: string) => `${this.quoteIdentifier(b)}`);
sql.push(` CREATE ${a.unique ? 'UNIQUE' : ''} NONCLUSTERED INDEX ${this.quoteIdentifier(a.indexName)} on ${baseTable} (${columnNames.join(',')})`);
if (a.indexComment) {
indexComment.push(
`EXECUTE sp_addextendedproperty N'MS_Description', N'${a.indexComment}', N'SCHEMA', N'${schema}', N'TABLE', N'${data.tableName}', N'INDEX', N'${a.indexName}'`
);
}
});
return sql.join(';') + ';' + indexComment.join(';');
}
getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
// sql执行顺序
// 1. 删除字段
// 2. 添加字段
// 3. 修改字段名字
// 4. 修改字段类型
// 5. 修改字段注释
// 6. 添加字段注释
let schema = tableData.db.split('/')[1];
let baseTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableName)}`;
let delSql = '';
let addArr = [] as string[];
let renameArr = [] as string[];
let updArr = [] as string[];
let changeCommentArr = [] as string[];
let addCommentArr = [] as string[];
if (changeData.del.length > 0) {
delSql = `ALTER TABLE ${baseTable} DROP ${changeData.del.map((a) => 'COLUMN ' + this.quoteIdentifier(a.name)).join(',')};`;
}
if (changeData.add.length > 0) {
changeData.add.forEach((a) => {
addArr.push(` ALTER TABLE ${baseTable} ADD COLUMN ${this.genColumnBasicSql(a)}`);
if (a.remark) {
addCommentArr.push(
`EXECUTE sp_addextendedproperty N'MS_Description', N'${a.remark}', N'SCHEMA', N'${schema}', N'TABLE', N'${tableName}', N'COLUMN', N'${a.name}'`
);
}
});
}
if (changeData.upd.length > 0) {
changeData.upd.forEach((a) => {
if (a.oldName && a.name !== a.oldName) {
renameArr.push(` EXEC sp_rename '${baseTable}.${this.quoteIdentifier(a.oldName)}', '${a.name}', 'COLUMN' `);
} else {
updArr.push(` ALTER TABLE ${baseTable} ALTER COLUMN ${this.genColumnBasicSql(a)} `);
}
if (a.remark) {
changeCommentArr.push(`IF ((SELECT COUNT(*) FROM fn_listextendedproperty('MS_Description',
'SCHEMA', N'${schema}',
'TABLE', N'${tableName}',
'COLUMN', N'${a.name}')) > 0)
EXEC sp_updateextendedproperty
'MS_Description', N'${a.remark}',
'SCHEMA', N'${schema}',
'TABLE', N'${tableName}',
'COLUMN', N'${a.name}'
ELSE
EXEC sp_addextendedproperty
'MS_Description', N'${a.remark}',
'SCHEMA', N'${schema}',
'TABLE', N'${tableName}',
'COLUMN',N'${a.name}'`);
}
});
}
return (
delSql +
(addArr.length > 0 ? addArr.join(';') + ';' : '') +
(renameArr.length > 0 ? renameArr.join(';') + ';' : '') +
(updArr.length > 0 ? updArr.join(';') + ';' : '') +
(changeCommentArr.length > 0 ? changeCommentArr.join(';') + ';' : '') +
(addCommentArr.length > 0 ? addCommentArr.join(';') + ';' : '')
);
}
getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
let schema = tableData.db.split('/')[1];
let baseTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableName)}`;
let dropArr = [] as string[];
let addArr = [] as string[];
let commentArr = [] as string[];
const pushDrop = (a: any) => {
dropArr.push(` DROP INDEX ${this.quoteIdentifier(a.indexName)} ON ${baseTable} `);
};
const pushAdd = (a: any) => {
addArr.push(
` CREATE ${a.unique ? 'UNIQUE' : ''} NONCLUSTERED INDEX ${this.quoteIdentifier(a.indexName)} ON ${baseTable} (${a.columnNames.map((b: string) => this.quoteIdentifier(b)).join(',')}) `
);
if (a.indexComment) {
commentArr.push(
` EXEC sp_addextendedproperty N'MS_Description', N'${a.indexComment}', N'SCHEMA', N'${schema}', N'TABLE', N'${tableName}', N'INDEX', N'${a.indexName}' `
);
}
};
if (changeData.upd.length > 0) {
changeData.upd.forEach((a) => {
pushDrop(a);
pushAdd(a);
});
}
if (changeData.del.length > 0) {
changeData.del.forEach((a) => {
pushDrop(a);
});
}
if (changeData.add.length > 0) {
changeData.add.forEach((a) => pushAdd(a));
}
let dropSql = dropArr.join(';');
let addSql = addArr.join(';');
let commentSql = commentArr.join(';');
return dropSql + ';' + addSql + ';' + commentSql + ';';
}
getDataType(columnType: string): DataType {
if (DbInst.isNumber(columnType)) {
return DataType.Number;
}
// 日期时间类型
if (/datetime|timestamp/gi.test(columnType)) {
return DataType.DateTime;
}
// 日期类型
if (/date/gi.test(columnType)) {
return DataType.Date;
}
// 时间类型
if (/time/gi.test(columnType)) {
return DataType.Time;
}
return DataType.String;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
wrapStrValue(columnType: string, value: string): string {
return `'${value}'`;
}
}

View File

@@ -113,7 +113,7 @@ class MysqlDialect implements DbDialect {
return mysqlDialectInfo;
}
getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
return `SELECT * FROM ${this.quoteIdentifier(table)} ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''} ${this.getPageSql(
pageNum,
limit
@@ -252,7 +252,7 @@ class MysqlDialect implements DbDialect {
return sql + arr.join(',') + ';';
}
getModifyIndexSql(tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
// 搜集修改和删除的索引添加到drop index xx
// 收集新增和修改的索引添加到ADD xx
// ALTER TABLE `test1`

View File

@@ -92,7 +92,35 @@ const replaceFunctions: EditorCompletionItem[] = [
{ label: 'NVL2', insertText: 'NVL2(x,value1,value2)', description: '如果x非空返回value1否则返回value2' },
];
const addCustomKeywords = ['ROWNUM', 'DUAL'];
const addCustomKeywords: EditorCompletionItem[] = [
{
label: 'ROWNUM',
description: 'keyword',
insertText: 'ROWNUM',
},
{
label: 'DUAL',
description: 'keyword',
insertText: 'DUAL',
},
// 分页代码块
{
label: 'SELECT ROWNUM',
description: 'code block',
insertText: 'SELECT * from table_name where rownum <= 10',
},
{
label: 'SELECT PAGE',
description: 'code block',
insertText: ` SELECT * FROM
(
SELECT t.*, ROWNUM AS rn
FROM table_name t
WHERE ROWNUM <= 25
)
WHERE rn > 0 \n`,
},
];
let oracleDialectInfo: DialectInfo;
class OracleDialect implements DbDialect {
@@ -104,6 +132,7 @@ class OracleDialect implements DbDialect {
let { keywords, operators, builtinVariables } = sqlLanguage;
let functionNames = replaceFunctions.map((a) => a.label);
let excludeKeywords = new Set(functionNames.concat(operators));
excludeKeywords.add('SELECT');
let editorCompletions: EditorCompletion = {
keywords: keywords
@@ -118,15 +147,7 @@ class OracleDialect implements DbDialect {
})
)
)
.concat(
// 加上自定义的关键字
addCustomKeywords.map(
(a): EditorCompletionItem => ({
label: a,
description: 'keyword',
})
)
),
.concat(addCustomKeywords),
operators: operators.map((a: string): EditorCompletionItem => ({ label: a, description: 'operator' })),
functions: replaceFunctions,
variables: builtinVariables.map((a: string): EditorCompletionItem => ({ label: a, description: 'var' })),
@@ -142,7 +163,7 @@ class OracleDialect implements DbDialect {
return oracleDialectInfo;
}
getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
return `
SELECT *
FROM (
@@ -399,7 +420,7 @@ class OracleDialect implements DbDialect {
return dropPkSql + modifySql + dropSql + renameSql + addPkSql + commentSql;
}
getModifyIndexSql(tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
// 不能直接修改索引名或字段、需要先删后加
let dropIndexNames: string[] = [];
let addIndexs: any[] = [];

View File

@@ -132,7 +132,7 @@ class PostgresqlDialect implements DbDialect {
return pgDialectInfo;
}
getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
return `SELECT * FROM ${this.quoteIdentifier(table)} ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''} ${this.getPageSql(
pageNum,
limit
@@ -365,7 +365,7 @@ class PostgresqlDialect implements DbDialect {
return dropPkSql + modifySql + dropSql + renameSql + addPkSql + commentSql;
}
getModifyIndexSql(tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
// 不能直接修改索引名或字段、需要先删后加
let dropIndexNames: string[] = [];
let addIndexs: any[] = [];

View File

@@ -132,7 +132,7 @@ class SqliteDialect implements DbDialect {
return sqliteDialectInfo;
}
getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
return `SELECT * FROM ${this.quoteIdentifier(table)} ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''} ${this.getPageSql(
pageNum,
limit
@@ -284,7 +284,7 @@ class SqliteDialect implements DbDialect {
return sql.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 {
// sqlite创建索引需要先删除再创建
// CREATE INDEX "main"."aa1" ON "t_sys_resource" ( "ui_path" );

View File

@@ -20,6 +20,7 @@ require (
github.com/gorilla/websocket v1.5.1
github.com/kanzihuang/vitess/go/vt/sqlparser v0.0.0-20231018071450-ac8d9f0167e9
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230712084735-068dc2aee82d
github.com/microsoft/go-mssqldb v1.6.0
github.com/mojocn/base64Captcha v1.3.6 //
github.com/pkg/errors v0.9.1
github.com/pkg/sftp v1.13.6
@@ -54,6 +55,8 @@ require (
github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/glog v1.0.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect

View File

@@ -29,5 +29,5 @@ type DbCopyTableForm struct {
Id uint64 `binding:"required" json:"id"`
Db string `binding:"required" json:"db" `
TableName string `binding:"required" json:"tableName"`
CopyData bool `binding:"required" json:"copyData"` // 是否复制数据
CopyData bool `json:"copyData"` // 是否复制数据
}

View File

@@ -154,7 +154,7 @@ func (d *dbAppImpl) GetDbConn(dbId uint64, dbName string) (*dbi.DbConn, error) {
checkDb := dbName
// 兼容pgsql/dm db/schema模式
if dbi.DbTypePostgres.Equal(instance.Type) || dbi.DbTypeDM.Equal(instance.Type) || dbi.DbTypeOracle.Equal(instance.Type) {
if dbi.DbTypePostgres.Equal(instance.Type) || dbi.DbTypeDM.Equal(instance.Type) || dbi.DbTypeOracle.Equal(instance.Type) || dbi.DbTypeMssql.Equal(instance.Type) {
ss := strings.Split(dbName, "/")
if len(ss) > 1 {
checkDb = ss[0]

View File

@@ -295,7 +295,7 @@ func (app *dataSyncAppImpl) srcData2TargetDb(srcRes []map[string]any, fieldMap [
}
// 解决字段大小写问题
updFieldVal := srcRes[len(srcRes)-1][strings.ToUpper(task.UpdField)]
if updFieldVal == "" {
if updFieldVal == "" || updFieldVal == nil {
updFieldVal = srcRes[len(srcRes)-1][strings.ToLower(task.UpdField)]
}
@@ -332,12 +332,6 @@ func (app *dataSyncAppImpl) srcData2TargetDb(srcRes []map[string]any, fieldMap [
return err
}
// 运行完成一轮就记录一下修改字段最大值
taskParam1 := new(entity.DataSyncTask)
taskParam1.Id = task.Id
taskParam1.UpdFieldVal = task.UpdFieldVal
_ = app.UpdateById(context.Background(), taskParam1)
// 运行过程中,判断状态是否为已关闭,是则结束运行,否则继续运行
taskParam, _ := app.GetById(new(entity.DataSyncTask), task.Id)
if taskParam.RunningState == entity.DataSyncTaskRunStateStop {

View File

@@ -9,6 +9,7 @@ import (
"mayfly-go/internal/db/domain/repository"
"mayfly-go/pkg/contextx"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/model"
"mayfly-go/pkg/utils/jsonx"
"strconv"
@@ -87,8 +88,14 @@ func (d *dbSqlExecAppImpl) Exec(ctx context.Context, execSqlReq *DbSqlExecReq) (
// 如果配置为0则不校验分页参数
maxCount := config.GetDbQueryMaxCount()
if maxCount != 0 {
// 兼容oracle rownum分页
if !strings.Contains(lowerSql, "limit") && !strings.Contains(lowerSql, "rownum") {
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("请完善分页信息后执行")
@@ -164,7 +171,7 @@ func doSelect(ctx context.Context, selectStmt *sqlparser.Select, execSqlReq *DbS
// 如果配置为0则不校验分页参数
maxCount := config.GetDbQueryMaxCount()
// 哪些数据库跳过校验
skipped := dbi.DbTypeOracle == execSqlReq.DbConn.Info.Type
skipped := dbi.DbTypeOracle == execSqlReq.DbConn.Info.Type || dbi.DbTypeMssql == execSqlReq.DbConn.Info.Type
if maxCount != 0 && !skipped {
limit := selectStmt.Limit
if limit == nil {
@@ -204,6 +211,9 @@ func doUpdate(ctx context.Context, update *sqlparser.Update, execSqlReq *DbSqlEx
tableStr := sqlparser.String(update.TableExprs)
// 可能使用别名,故空格切割
tableName := strings.Split(tableStr, " ")[0]
if strings.Index(tableName, ".") > -1 {
tableName = strings.Split(tableName, ".")[1]
}
where := sqlparser.String(update.Where)
if len(where) == 0 {
return nil, errorx.NewBiz("SQL[%s]未执行. 请完善 where 条件后再执行", execSqlReq.Sql)
@@ -223,14 +233,26 @@ func doUpdate(ctx context.Context, update *sqlparser.Update, execSqlReq *DbSqlEx
updateColumnsAndPrimaryKey := strings.Join(updateColumns, ",") + "," + primaryKey
// 查询要更新字段数据的旧值,以及主键值
selectSql := fmt.Sprintf("SELECT %s FROM %s %s LIMIT 200", updateColumnsAndPrimaryKey, tableStr, where)
_, res, err := dbConn.QueryContext(ctx, selectSql)
if err == nil {
dbSqlExec.OldValue = jsonx.ToStr(res)
} else {
dbSqlExec.OldValue = err.Error()
selectSql := fmt.Sprintf("SELECT %s FROM %s %s", updateColumnsAndPrimaryKey, tableStr, where)
// WalkQuery查出最多200条数据
maxRec := 200
nowRec := 0
res := make([]map[string]any, 0)
err = dbConn.WalkQueryRows(ctx, selectSql, func(row map[string]any, columns []*dbi.QueryColumn) error {
nowRec++
res = append(res, row)
if nowRec == maxRec {
return errorx.NewBiz(fmt.Sprintf("超出更新最大查询条数限制: %d", maxRec))
}
return nil
})
if err != nil {
logx.Warn(err.Error())
}
dbSqlExec.OldValue = jsonx.ToStr(res)
dbSqlExec.Table = tableName
dbSqlExec.Type = entity.DbSqlExecTypeUpdate

View File

@@ -173,7 +173,12 @@ func walkQueryRows(ctx context.Context, db *sql.DB, selectSql string, walkFn Wal
// 这里表示一行所有列的值,用[]byte表示
values := make([][]byte, lenCols)
for k, colType := range colTypes {
cols[k] = &QueryColumn{Name: colType.Name(), Type: colType.DatabaseTypeName()}
// 处理字段名,如果为空,则命名为匿名列
colName := colType.Name()
if colName == "" {
colName = fmt.Sprintf("<anonymous%d>", k+1)
}
cols[k] = &QueryColumn{Name: colName, Type: colType.DatabaseTypeName()}
// 这里scans引用values把数据填充到[]byte里
scans[k] = &values[k]
}
@@ -187,7 +192,7 @@ func walkQueryRows(ctx context.Context, db *sql.DB, selectSql string, walkFn Wal
rowData := make(map[string]any, lenCols)
// 把values中的数据复制到row中
for i, v := range values {
rowData[colTypes[i].Name()] = valueConvert(v, colTypes[i])
rowData[cols[i].Name] = valueConvert(v, colTypes[i])
}
if err = walkFn(rowData, cols); err != nil {
logx.Error("游标遍历查询结果集出错,退出遍历: %s", err.Error())

View File

@@ -17,6 +17,7 @@ const (
DbTypeDM DbType = "dm"
DbTypeOracle DbType = "oracle"
DbTypeSqlite DbType = "sqlite"
DbTypeMssql DbType = "mssql"
)
func ToDbType(dbType string) DbType {
@@ -44,6 +45,8 @@ func (dbType DbType) QuoteIdentifier(name string) string {
return quoteIdentifier(name, "`")
case DbTypePostgres:
return quoteIdentifier(name, `"`)
case DbTypeMssql:
return fmt.Sprintf("[%s]", name)
default:
return quoteIdentifier(name, `"`)
}

View File

@@ -42,7 +42,8 @@ type Column struct {
ColumnName string `json:"columnName"` // 列名
ColumnType string `json:"columnType"` // 列类型
ColumnComment string `json:"columnComment"` // 列备注
ColumnKey string `json:"columnKey"` // 是否为主键逐渐的话值钱为PRI
IsPrimaryKey bool `json:"isPrimaryKey"` // 是否为主键
IsIdentity bool `json:"isIdentity"` // 是否自增
ColumnDefault string `json:"columnDefault"` // 默认值
Nullable string `json:"nullable"` // 是否可为null
NumScale string `json:"numScale"` // 小数点
@@ -56,7 +57,7 @@ type Index struct {
IndexType string `json:"indexType"` // 索引类型
IndexComment string `json:"indexComment"` // 备注
SeqInIndex int `json:"seqInIndex"`
NonUnique int `json:"nonUnique"`
IsUnique bool `json:"isUnique"`
}
type DbCopyTable struct {

View File

@@ -36,7 +36,7 @@ ORDER BY a.object_name
select
a.index_name as INDEX_NAME,
a.index_type as INDEX_TYPE,
case when a.uniqueness = 'UNIQUE' then 1 else 0 end as NON_UNIQUE,
case when a.uniqueness = 'UNIQUE' then 1 else 0 end as IS_UNIQUE,
indexdef(b.object_id,1) as INDEX_DEF,
c.column_name as COLUMN_NAME,
c.column_position as SEQ_IN_INDEX,
@@ -64,22 +64,26 @@ select a.table_name
b.comments as COLUMN_COMMENT,
a.data_default as COLUMN_DEFAULT,
a.data_scale as NUM_SCALE,
case when t.COL_NAME = a.column_name then 'PRI' else '' end as COLUMN_KEY
case when t.COL_NAME = a.column_name then 1 else 0 end as IS_IDENTITY,
case when t2.constraint_type = 'P' then 1 else 0 end as IS_PRIMARY_KEY
from all_tab_columns a
left join user_col_comments b
on b.owner = (SELECT SF_GET_SCHEMA_NAME_BY_ID(CURRENT_SCHID)) and b.table_name = a.table_name and
a.column_name = b.column_name
left join (select b.owner, b.table_name, a.name COL_NAME
on b.owner = (SELECT SF_GET_SCHEMA_NAME_BY_ID(CURRENT_SCHID))
and b.table_name = a.table_name
and a.column_name = b.column_name
left join (select b.owner, b.TABLE_NAME, a.NAME as COL_NAME
from SYS.SYSCOLUMNS a,
all_tables b,
sys.sysobjects c,
sys.sysobjects d
SYS.all_tables b,
SYS.SYSOBJECTS c
where a.INFO2 & 0x01 = 0x01
and a.id=c.id and d.type$ = 'SCH' and d.id = c.schid
and b.owner = (SELECT SF_GET_SCHEMA_NAME_BY_ID(CURRENT_SCHID))
and c.schid = ( select id from sys.sysobjects where type$ = 'SCH' and name = (SELECT SF_GET_SCHEMA_NAME_BY_ID(CURRENT_SCHID)))
and c.name = b.table_name) t
on t.table_name = a.table_name
and a.ID = c.ID
and c.NAME = b.TABLE_NAME) t
on t.table_name = a.table_name and t.owner = a.owner
left join (select uc.OWNER, uic.column_name, uic.table_name, uc.constraint_type
from user_ind_columns uic
left join user_constraints uc on uic.index_name = uc.index_name) t2
on t2.table_name = t.table_name and a.column_name = t2.column_name
where a.owner = (SELECT SF_GET_SCHEMA_NAME_BY_ID(CURRENT_SCHID))
and a.table_name in (%s)
order by a.table_name, a.column_id
order by a.table_name,
a.column_id

View File

@@ -0,0 +1,209 @@
--MSSQL_DBS
SELECT name AS dbname
FROM sys.databases
WHERE owner_sid = SUSER_SID()
and name not in ('master', 'tempdb', 'model', 'msdb')
---------------------------------------
--MSSQL_TABLE_DETAIL 查询表名和表注释
SELECT t.name AS tableName,
ep.value AS tableComment
FROM sys.tables t
left OUTER JOIN sys.schemas ss on t.schema_id = ss.schema_id
LEFT OUTER JOIN
sys.extended_properties ep ON ep.major_id = t.object_id AND ep.minor_id = 0 AND ep.class = 1
WHERE ss.name = ?
and t.name = ?
---------------------------------------
--MSSQL_DB_SCHEMAS 数据库下所有schema
SELECT a.SCHEMA_NAME
FROM information_schema.schemata a
where a.catalog_name = DB_NAME()
and (a.SCHEMA_NAME in ('dbo', 'guest') or a.SCHEMA_NAME not like 'db_%')
and a.SCHEMA_NAME not in ('sys', 'INFORMATION_SCHEMA')
---------------------------------------
--MSSQL_TABLE_INFO 表详细信息
SELECT t.name AS tableName,
ss.name AS tableSchema,
c.value AS tableComment,
p.rows AS tableRows,
0 AS dataLength,
0 AS indexLength,
t.create_date AS createTime
FROM sys.tables t
left OUTER JOIN sys.schemas ss on t.schema_id = ss.schema_id
left OUTER JOIN sys.partitions p ON t.object_id = p.object_id AND p.index_id = 1
left OUTER JOIN sys.extended_properties c ON t.object_id = c.major_id AND c.minor_id = 0 AND c.class = 1
where ss.name = ?
ORDER BY t.name DESC;
---------------------------------------
--MSSQL_INDEX_INFO 索引信息
SELECT ind.name AS indexName,
col.name AS columnName,
CASE
WHEN ind.is_primary_key = 1 THEN 'CLUSTERED'
ELSE 'NON-CLUSTERED'
END AS indexType,
IIF(ind.is_unique = 'true', 1, 0) AS isUnique,
ic.key_ordinal AS seqInIndex,
idx.value AS indexComment
FROM sys.indexes ind
LEFT JOIN sys.tables t on t.object_id = ind.object_id
LEFT JOIN sys.schemas ss on t.schema_id = ss.schema_id
LEFT JOIN
sys.index_columns ic ON ind.object_id = ic.object_id AND ind.index_id = ic.index_id
LEFT JOIN
sys.columns col ON ind.object_id = col.object_id AND ic.column_id = col.column_id
LEFT JOIN
sys.extended_properties idx ON ind.object_id = idx.major_id AND ind.index_id = idx.minor_id AND idx.class = 7
WHERE ss.name = ?
and ind.name is not null
and t.name = ?
---------------------------------------
--MSSQL_COLUMN_MA 列信息元数据
SELECT t.name AS TABLE_NAME,
c.name AS COLUMN_NAME,
CASE
WHEN c.is_nullable = 1 THEN 'YES'
ELSE 'NO'
END AS NULLABLE,
tp.name +
CASE
WHEN tp.name IN ('char', 'varchar', 'nchar', 'nvarchar') THEN '(' + CASE
WHEN c.max_length = -1 THEN 'max'
ELSE CAST(c.max_length AS NVARCHAR(255)) END +
')'
WHEN tp.name IN ('numeric', 'decimal') THEN '(' + CAST(c.precision AS NVARCHAR(255)) + ',' +
CAST(c.scale AS NVARCHAR(255)) + ')'
ELSE ''
END AS COLUMN_TYPE,
ep.value AS COLUMN_COMMENT,
COLUMN_DEFAULT = CASE
WHEN c.default_object_id IS NOT NULL THEN object_definition(c.default_object_id)
ELSE ''
END,
c.scale AS NUM_SCALE,
IS_IDENTITY = COLUMNPROPERTY(c.object_id, c.name, 'IsIdentity'),
IS_PRIMARY_KEY = CASE
WHEN (SELECT COUNT(*)
FROM sys.index_columns ic
INNER JOIN sys.indexes i
ON ic.index_id = i.index_id AND ic.object_id = i.object_id
WHERE ic.object_id = c.object_id
AND ic.column_id = c.column_id
AND i.is_primary_key = 1) > 0 THEN 1
ELSE 0
END
FROM sys.tables t
INNER JOIN sys.schemas ss on t.schema_id = ss.schema_id
INNER JOIN
sys.columns c ON t.object_id = c.object_id
INNER JOIN
sys.types tp ON c.system_type_id = tp.system_type_id AND c.user_type_id = tp.user_type_id
LEFT JOIN
sys.extended_properties ep ON t.object_id = ep.major_id AND c.column_id = ep.minor_id AND ep.class = 1
WHERE ss.name = ?
and t.name in (%s)
ORDER BY t.name, c.column_id
---------------------------------------
--MSSQL_TABLE_DDL 建表ddl
declare
@tabname varchar(50)
set @tabname= ? --
if ( object_id('tempdb.dbo.#t') is not null)
begin
DROP TABLE #t
end
select 'create table [' + so.name + '] (' + o.list + ')'
+ CASE
WHEN tc.Constraint_Name IS NULL THEN ''
ELSE 'ALTER TABLE ' + so.Name + ' ADD CONSTRAINT ' + tc.Constraint_Name + ' PRIMARY KEY ' +
' (' + LEFT(j.List, Len(j.List)-1) + ')' END
TABLE_DDL
into #t
from sysobjects so
cross apply
(SELECT
' \n ['+ column_name +'] ' +
data_type + case data_type
when 'sql_variant' then ''
when 'text' then ''
when 'ntext' then ''
when 'xml' then ''
when 'decimal' then '(' + cast (numeric_precision as varchar) + ', ' + cast (numeric_scale as varchar) + ')'
else coalesce ('('+ case when character_maximum_length = -1 then 'MAX' else cast (character_maximum_length as varchar) end +')', '') end + ' ' +
case when exists (
select id from syscolumns
where object_name(id)=so.name
and name = column_name
and columnproperty(id, name, 'IsIdentity') = 1
) then
'IDENTITY(' +
cast (ident_seed(so.name) as varchar) + ',' +
cast (ident_incr(so.name) as varchar) + ')'
else ''
end + ' ' +
(case when IS_NULLABLE = 'No' then 'NOT ' else '' end ) + 'NULL ' +
case when information_schema.columns.COLUMN_DEFAULT IS NOT NULL THEN 'DEFAULT '+ information_schema.columns.COLUMN_DEFAULT ELSE '' END + ', '
from information_schema.columns where table_name = so.name
order by ordinal_position
FOR XML PATH ('')) o (list)
left join
information_schema.table_constraints tc
on tc.Table_name = so.Name
AND tc.Constraint_Type = 'PRIMARY KEY'
cross apply
(select '[' + Column_Name + '], '
FROM information_schema.key_column_usage kcu
WHERE kcu.Constraint_Name = tc.Constraint_Name
ORDER BY
ORDINAL_POSITION
FOR XML PATH ('')) j (list)
where xtype = 'U'
AND name =@tabname
select (
case
when (select count(a.constraint_type)
from information_schema.table_constraints a
inner join information_schema.constraint_column_usage b
on a.constraint_name = b.constraint_name
where a.constraint_type = 'PRIMARY KEY'--
and a.table_name = @tabname) = 1 then replace(table_ddl
, ', )ALTER TABLE'
, ')' + CHAR (13)+'ALTER TABLE')
else SUBSTRING(table_ddl
, 1
, len(table_ddl) - 3) + ')' end
) as TableDDL
from #t
drop table #t
---------------------------------------
--MSSQL_TABLE_INDEX_DDL 建索引ddl
DECLARE
@TableName NVARCHAR(255)
SET @TableName = ?;
SELECT 'CREATE ' +
CASE
WHEN i.is_primary_key = 1 THEN 'CLUSTERED '
WHEN i.type_desc = 'HEAP' THEN ''
ELSE 'NONCLUSTERED '
END +
'INDEX ' + i.name + ' ON ' + t.name + ' (' +
STUFF((SELECT ',' + c.name +
CASE
WHEN ic.is_descending_key = 1 THEN ' DESC'
ELSE ' ASC'
END
FROM sys.index_columns ic
INNER JOIN
sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
WHERE ic.object_id = i.object_id
AND ic.index_id = i.index_id
ORDER BY ic.key_ordinal
FOR XML PATH (''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '') + ');' AS IndexDDL
FROM sys.tables t
INNER JOIN
sys.indexes i ON t.object_id = i.object_id
WHERE t.name = @TableName;

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ import (
"mayfly-go/internal/common/consts"
"mayfly-go/internal/db/dbm/dbi"
_ "mayfly-go/internal/db/dbm/dm"
_ "mayfly-go/internal/db/dbm/mssql"
_ "mayfly-go/internal/db/dbm/mysql"
_ "mayfly-go/internal/db/dbm/oracle"
_ "mayfly-go/internal/db/dbm/postgres"

View File

@@ -101,7 +101,8 @@ func (dd *DMDialect) GetColumns(tableNames ...string) ([]dbi.Column, error) {
ColumnType: anyx.ConvString(re["COLUMN_TYPE"]),
ColumnComment: anyx.ConvString(re["COLUMN_COMMENT"]),
Nullable: anyx.ConvString(re["NULLABLE"]),
ColumnKey: anyx.ConvString(re["COLUMN_KEY"]),
IsPrimaryKey: anyx.ConvInt(re["IS_PRIMARY_KEY"]) == 1,
IsIdentity: anyx.ConvInt(re["IS_IDENTITY"]) == 1,
ColumnDefault: anyx.ConvString(re["COLUMN_DEFAULT"]),
NumScale: anyx.ConvString(re["NUM_SCALE"]),
})
@@ -118,7 +119,7 @@ func (dd *DMDialect) GetPrimaryKey(tablename string) (string, error) {
return "", errorx.NewBiz("[%s] 表不存在", tablename)
}
for _, v := range columns {
if v.ColumnKey == "PRI" {
if v.IsPrimaryKey {
return v.ColumnName, nil
}
}
@@ -140,7 +141,7 @@ func (dd *DMDialect) GetTableIndex(tableName string) ([]dbi.Index, error) {
ColumnName: anyx.ConvString(re["COLUMN_NAME"]),
IndexType: anyx.ConvString(re["INDEX_TYPE"]),
IndexComment: anyx.ConvString(re["INDEX_COMMENT"]),
NonUnique: anyx.ConvInt(re["NON_UNIQUE"]),
IsUnique: anyx.ConvInt(re["IS_UNIQUE"]) == 1,
SeqInIndex: anyx.ConvInt(re["SEQ_IN_INDEX"]),
})
}

View File

@@ -0,0 +1,425 @@
package mssql
import (
"context"
"database/sql"
"fmt"
"mayfly-go/internal/db/dbm/dbi"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/utils/anyx"
"mayfly-go/pkg/utils/collx"
"regexp"
"strings"
"time"
)
const (
MSSQL_META_FILE = "metasql/mssql_meta.sql"
MSSQL_DBS_KEY = "MSSQL_DBS"
MSSQL_DB_SCHEMAS_KEY = "MSSQL_DB_SCHEMAS"
MSSQL_TABLE_INFO_KEY = "MSSQL_TABLE_INFO"
MSSQL_INDEX_INFO_KEY = "MSSQL_INDEX_INFO"
MSSQL_COLUMN_MA_KEY = "MSSQL_COLUMN_MA"
MSSQL_TABLE_DETAIL_KEY = "MSSQL_TABLE_DETAIL"
MSSQL_TABLE_INDEX_DDL_KEY = "MSSQL_TABLE_INDEX_DDL"
)
type MssqlDialect struct {
dc *dbi.DbConn
}
func (md *MssqlDialect) GetDbServer() (*dbi.DbServer, error) {
_, res, err := md.dc.Query("SELECT @@VERSION as version")
if err != nil {
return nil, err
}
ds := &dbi.DbServer{
Version: anyx.ConvString(res[0]["version"]),
}
return ds, nil
}
func (md *MssqlDialect) GetDbNames() ([]string, error) {
_, res, err := md.dc.Query(dbi.GetLocalSql(MSSQL_META_FILE, MSSQL_DBS_KEY))
if err != nil {
return nil, err
}
databases := make([]string, 0)
for _, re := range res {
databases = append(databases, anyx.ConvString(re["dbname"]))
}
return databases, nil
}
// 从连接信息中获取数据库和schema信息
func (md *MssqlDialect) currentSchema() string {
dbName := md.dc.Info.Database
schema := ""
arr := strings.Split(dbName, "/")
if len(arr) == 2 {
schema = arr[1]
}
return schema
}
// 获取表基础元信息, 如表名等
func (md *MssqlDialect) GetTables() ([]dbi.Table, error) {
_, res, err := md.dc.Query(dbi.GetLocalSql(MSSQL_META_FILE, MSSQL_TABLE_INFO_KEY), md.currentSchema())
if err != nil {
return nil, err
}
tables := make([]dbi.Table, 0)
for _, re := range res {
tables = append(tables, dbi.Table{
TableName: anyx.ConvString(re["tableName"]),
TableComment: anyx.ConvString(re["tableComment"]),
CreateTime: anyx.ConvString(re["createTime"]),
TableRows: anyx.ConvInt(re["tableRows"]),
DataLength: anyx.ConvInt64(re["dataLength"]),
IndexLength: anyx.ConvInt64(re["indexLength"]),
})
}
return tables, nil
}
// 获取列元信息, 如列名等
func (md *MssqlDialect) GetColumns(tableNames ...string) ([]dbi.Column, error) {
dbType := md.dc.Info.Type
tableName := strings.Join(collx.ArrayMap[string, string](tableNames, func(val string) string {
return fmt.Sprintf("'%s'", dbType.RemoveQuote(val))
}), ",")
_, res, err := md.dc.Query(fmt.Sprintf(dbi.GetLocalSql(MSSQL_META_FILE, MSSQL_COLUMN_MA_KEY), tableName), md.currentSchema())
if err != nil {
return nil, err
}
columns := make([]dbi.Column, 0)
for _, re := range res {
columns = append(columns, dbi.Column{
TableName: anyx.ToString(re["TABLE_NAME"]),
ColumnName: anyx.ToString(re["COLUMN_NAME"]),
ColumnType: anyx.ToString(re["COLUMN_TYPE"]),
ColumnComment: anyx.ToString(re["COLUMN_COMMENT"]),
Nullable: anyx.ToString(re["NULLABLE"]),
IsPrimaryKey: anyx.ConvInt(re["IS_PRIMARY_KEY"]) == 1,
IsIdentity: anyx.ConvInt(re["IS_IDENTITY"]) == 1,
ColumnDefault: anyx.ToString(re["COLUMN_DEFAULT"]),
NumScale: anyx.ToString(re["NUM_SCALE"]),
})
}
return columns, nil
}
// 获取表主键字段名,不存在主键标识则默认第一个字段
func (md *MssqlDialect) GetPrimaryKey(tablename string) (string, error) {
columns, err := md.GetColumns(tablename)
if err != nil {
return "", err
}
if len(columns) == 0 {
return "", errorx.NewBiz("[%s] 表不存在", tablename)
}
for _, v := range columns {
if v.IsPrimaryKey {
return v.ColumnName, nil
}
}
return columns[0].ColumnName, nil
}
// 获取表索引信息
func (md *MssqlDialect) GetTableIndex(tableName string) ([]dbi.Index, error) {
_, res, err := md.dc.Query(dbi.GetLocalSql(MSSQL_META_FILE, MSSQL_INDEX_INFO_KEY), md.currentSchema(), tableName)
if err != nil {
return nil, err
}
indexs := make([]dbi.Index, 0)
for _, re := range res {
indexs = append(indexs, dbi.Index{
IndexName: anyx.ConvString(re["indexName"]),
ColumnName: anyx.ConvString(re["columnName"]),
IndexType: anyx.ConvString(re["indexType"]),
IndexComment: anyx.ConvString(re["indexComment"]),
IsUnique: anyx.ConvInt(re["isUnique"]) == 1,
SeqInIndex: anyx.ConvInt(re["seqInIndex"]),
})
}
// 把查询结果以索引名分组,索引字段以逗号连接
result := make([]dbi.Index, 0)
key := ""
for _, v := range indexs {
// 当前的索引名
in := v.IndexName
// 过滤掉主键索引,主键索引名为PK__开头的
if strings.HasPrefix(in, "PK__") {
continue
}
if key == in {
// 索引字段已根据名称和顺序排序,故取最后一个即可
i := len(result) - 1
// 同索引字段以逗号连接
result[i].ColumnName = result[i].ColumnName + "," + v.ColumnName
} else {
key = in
result = append(result, v)
}
}
return result, nil
}
func (md MssqlDialect) CopyTableDDL(tableName string, newTableName string) (string, error) {
if newTableName == "" {
newTableName = tableName
}
// 根据列信息生成建表语句
var builder strings.Builder
var commentBuilder strings.Builder
// 查询表名和表注释, 设置表注释
_, res, err := md.dc.Query(dbi.GetLocalSql(MSSQL_META_FILE, MSSQL_TABLE_DETAIL_KEY), md.currentSchema(), tableName)
if err != nil {
return "", err
}
tableComment := ""
if len(res) > 0 {
tableComment = anyx.ToString(res[0]["tableComment"])
if tableComment != "" {
// 注释转义单引号
tableComment = strings.ReplaceAll(tableComment, "'", "\\'")
commentBuilder.WriteString(fmt.Sprintf("\nEXEC sp_addextendedproperty N'MS_Description', N'%s', N'SCHEMA', N'%s', N'TABLE',N'%s';\n", tableComment, md.currentSchema(), newTableName))
}
}
baseTable := fmt.Sprintf("%s.%s", md.dc.Info.Type.QuoteIdentifier(md.currentSchema()), md.dc.Info.Type.QuoteIdentifier(newTableName))
// 查询列信息
columns, err := md.GetColumns(tableName)
if err != nil {
return "", err
}
builder.WriteString(fmt.Sprintf("CREATE TABLE %s (\n", baseTable))
pks := make([]string, 0)
for i, v := range columns {
nullAble := "NULL"
if v.Nullable == "NO" {
nullAble = "NOT NULL"
}
builder.WriteString(fmt.Sprintf("\t[%s] %s %s", v.ColumnName, v.ColumnType, nullAble))
if v.IsIdentity {
builder.WriteString(" IDENTITY(1,11)")
}
if v.ColumnDefault != "" {
builder.WriteString(fmt.Sprintf(" DEFAULT %s", v.ColumnDefault))
}
if v.IsPrimaryKey {
pks = append(pks, fmt.Sprintf("[%s]", v.ColumnName))
}
if i < len(columns)-1 {
builder.WriteString(",")
}
builder.WriteString("\n")
}
// 设置主键
if len(pks) > 0 {
builder.WriteString(fmt.Sprintf("\tCONSTRAINT PK_%s PRIMARY KEY ( %s )", newTableName, strings.Join(pks, ",")))
}
builder.WriteString("\n);\n")
// 设置字段注释
for _, v := range columns {
if v.ColumnComment != "" {
// 注释转义单引号
v.ColumnComment = strings.ReplaceAll(v.ColumnComment, "'", "\\'")
commentBuilder.WriteString(fmt.Sprintf("\nEXEC sp_addextendedproperty N'MS_Description', N'%s', N'SCHEMA', N'%s', N'TABLE',N'%s', N'COLUMN', N'%s';\n", v.ColumnComment, md.currentSchema(), newTableName, v.ColumnName))
}
}
// 设置索引
indexs, err := md.GetTableIndex(tableName)
if err != nil {
return "", err
}
for _, v := range indexs {
builder.WriteString(fmt.Sprintf("\nCREATE NONCLUSTERED INDEX [%s] ON %s (%s);\n", v.IndexName, baseTable, v.ColumnName))
// 设置索引注释
if v.IndexComment != "" {
// 注释转义单引号
v.IndexComment = strings.ReplaceAll(v.IndexComment, "'", "\\'")
commentBuilder.WriteString(fmt.Sprintf("\nEXEC sp_addextendedproperty N'MS_Description', N'%s', N'SCHEMA', N'%s', N'TABLE',N'%s', N'INDEX', N'%s';\n", v.IndexComment, md.currentSchema(), newTableName, v.IndexName))
}
}
return builder.String() + commentBuilder.String(), nil
}
// 获取建表ddl
func (md *MssqlDialect) GetTableDDL(tableName string) (string, error) {
return md.CopyTableDDL(tableName, "")
}
func (md *MssqlDialect) WalkTableRecord(tableName string, walkFn dbi.WalkQueryRowsFunc) error {
return md.dc.WalkQueryRows(context.Background(), fmt.Sprintf("SELECT * FROM %s", tableName), walkFn)
}
func (md *MssqlDialect) GetSchemas() ([]string, error) {
_, res, err := md.dc.Query(dbi.GetLocalSql(MSSQL_META_FILE, MSSQL_DB_SCHEMAS_KEY))
if err != nil {
return nil, err
}
schemas := make([]string, 0)
for _, re := range res {
schemas = append(schemas, anyx.ConvString(re["SCHEMA_NAME"]))
}
return schemas, nil
}
// GetDbProgram 获取数据库程序模块,用于数据库备份与恢复
func (md *MssqlDialect) GetDbProgram() dbi.DbProgram {
panic("implement me")
}
func (md *MssqlDialect) BatchInsert(tx *sql.Tx, tableName string, columns []string, values [][]any) (int64, error) {
schema := md.currentSchema()
// 生成占位符字符串:如:(?,?)
// 重复字符串并用逗号连接
repeated := strings.Repeat("?,", len(columns))
// 去除最后一个逗号,占位符由括号包裹
placeholder := fmt.Sprintf("(%s)", strings.TrimSuffix(repeated, ","))
// 重复占位符字符串n遍
repeated = strings.Repeat(placeholder+",", len(values))
// 去除最后一个逗号
placeholder = strings.TrimSuffix(repeated, ",")
baseTable := fmt.Sprintf("%s.%s", md.dc.Info.Type.QuoteIdentifier(schema), md.dc.Info.Type.QuoteIdentifier(tableName))
sqlStr := fmt.Sprintf("insert into %s (%s) values %s", baseTable, strings.Join(columns, ","), placeholder)
// 执行批量insert sql
// 把二维数组转为一维数组
var args []any
for _, v := range values {
args = append(args, v...)
}
// 设置允许填充自增列之后,显示指定列名可以插入自增列
_, _ = md.dc.Exec(fmt.Sprintf("set identity_insert \"%s\" on", tableName))
exec, err := md.dc.TxExec(tx, sqlStr, args...)
_, _ = md.dc.Exec(fmt.Sprintf("set identity_insert \"%s\" off", tableName))
return exec, err
}
func (md *MssqlDialect) GetDataConverter() dbi.DataConverter {
return new(DataConverter)
}
var (
// 数字类型
numberRegexp = regexp.MustCompile(`(?i)int|double|float|number|decimal|byte|bit`)
// 日期时间类型
datetimeRegexp = regexp.MustCompile(`(?i)datetime|timestamp`)
// 日期类型
dateRegexp = regexp.MustCompile(`(?i)date`)
// 时间类型
timeRegexp = regexp.MustCompile(`(?i)time`)
)
type DataConverter struct {
}
func (dc *DataConverter) GetDataType(dbColumnType string) dbi.DataType {
if numberRegexp.MatchString(dbColumnType) {
return dbi.DataTypeNumber
}
// 日期时间类型
if datetimeRegexp.MatchString(dbColumnType) {
return dbi.DataTypeDateTime
}
// 日期类型
if dateRegexp.MatchString(dbColumnType) {
return dbi.DataTypeDate
}
// 时间类型
if timeRegexp.MatchString(dbColumnType) {
return dbi.DataTypeTime
}
return dbi.DataTypeString
}
func (dc *DataConverter) FormatData(dbColumnValue any, dataType dbi.DataType) string {
return anyx.ToString(dbColumnValue)
}
func (dc *DataConverter) ParseData(dbColumnValue any, dataType dbi.DataType) any {
return dbColumnValue
}
func (md *MssqlDialect) CopyTable(copy *dbi.DbCopyTable) error {
schema := md.currentSchema()
// 生成新表名,为老表明+_copy_时间戳
newTableName := copy.TableName + "_copy_" + time.Now().Format("20060102150405")
// 复制建表语句
ddl, err := md.CopyTableDDL(copy.TableName, newTableName)
if err != nil {
return err
}
// 执行建表
_, err = md.dc.Exec(ddl)
if err != nil {
return err
}
// 复制数据
if copy.CopyData {
go func() {
// 查询所有的列
columns, err := md.GetColumns(copy.TableName)
if err != nil {
logx.Warnf("复制表[%s]数据失败: %s", copy.TableName, err.Error())
return
}
// 取出每列名, 需要显示指定列名插入数据
columnNames := make([]string, 0)
hasIdentity := false
for _, v := range columns {
columnNames = append(columnNames, fmt.Sprintf("[%s]", v.ColumnName))
if v.IsIdentity {
hasIdentity = true
}
}
columnsSql := strings.Join(columnNames, ",")
// 复制数据
// 设置允许填充自增列之后,显示指定列名可以插入自增列
identityInsertOn := ""
identityInsertOff := ""
if hasIdentity {
identityInsertOn = fmt.Sprintf("SET IDENTITY_INSERT [%s].[%s] ON", schema, newTableName)
identityInsertOff = fmt.Sprintf("SET IDENTITY_INSERT [%s].[%s] OFF", schema, newTableName)
}
_, err = md.dc.Exec(fmt.Sprintf(" %s INSERT INTO [%s].[%s] (%s) SELECT * FROM [%s].[%s] %s", identityInsertOn, schema, newTableName, columnsSql, schema, copy.TableName, identityInsertOff))
if err != nil {
logx.Warnf("复制表[%s]数据失败: %s", copy.TableName, err.Error())
}
}()
}
return err
}

View File

@@ -0,0 +1,48 @@
package mssql
import (
"database/sql"
"fmt"
_ "github.com/microsoft/go-mssqldb"
"mayfly-go/internal/db/dbm/dbi"
"net/url"
"strings"
)
func init() {
meta := new(Meta)
dbi.Register(dbi.DbTypeMssql, meta)
}
type Meta struct {
}
func (md *Meta) GetSqlDb(d *dbi.DbInfo) (*sql.DB, error) {
err := d.IfUseSshTunnelChangeIpPort()
if err != nil {
return nil, err
}
query := url.Values{}
// The application name (default is go-mssqldb)
query.Add("app name", "mayfly")
// 指定与服务器协商加密的最低TLS版本
query.Add("tlsmin", "1.0")
// 连接超时时间10秒
query.Add("connection timeout", "10")
if d.Database != "" {
ss := strings.Split(d.Database, "/")
if len(ss) > 1 {
query.Add("database", ss[0])
query.Add("schema", ss[1])
} else {
query.Add("database", d.Database)
}
}
const driverName = "mssql"
dsn := fmt.Sprintf("sqlserver://%s:%s@%s:%d?%s", url.PathEscape(d.Username), url.PathEscape(d.Password), d.Host, d.Port, query.Encode())
return sql.Open(driverName, dsn)
}
func (md *Meta) GetDialect(conn *dbi.DbConn) dbi.Dialect {
return &MssqlDialect{conn}
}

View File

@@ -90,7 +90,8 @@ func (md *MysqlDialect) GetColumns(tableNames ...string) ([]dbi.Column, error) {
ColumnType: anyx.ConvString(re["columnType"]),
ColumnComment: anyx.ConvString(re["columnComment"]),
Nullable: anyx.ConvString(re["nullable"]),
ColumnKey: anyx.ConvString(re["columnKey"]),
IsPrimaryKey: anyx.ConvInt(re["isPrimaryKey"]) == 1,
IsIdentity: anyx.ConvInt(re["isIdentity"]) == 1,
ColumnDefault: anyx.ConvString(re["columnDefault"]),
NumScale: anyx.ConvString(re["numScale"]),
})
@@ -109,7 +110,7 @@ func (md *MysqlDialect) GetPrimaryKey(tablename string) (string, error) {
}
for _, v := range columns {
if v.ColumnKey == "PRI" {
if v.IsPrimaryKey {
return v.ColumnName, nil
}
}
@@ -131,7 +132,7 @@ func (md *MysqlDialect) GetTableIndex(tableName string) ([]dbi.Index, error) {
ColumnName: anyx.ConvString(re["columnName"]),
IndexType: anyx.ConvString(re["indexType"]),
IndexComment: anyx.ConvString(re["indexComment"]),
NonUnique: anyx.ConvInt(re["nonUnique"]),
IsUnique: anyx.ConvInt(re["isUnique"]) == 1,
SeqInIndex: anyx.ConvInt(re["seqInIndex"]),
})
}

View File

@@ -103,7 +103,8 @@ func (od *OracleDialect) GetColumns(tableNames ...string) ([]dbi.Column, error)
ColumnType: anyx.ConvString(re["COLUMN_TYPE"]),
ColumnComment: anyx.ConvString(re["COLUMN_COMMENT"]),
Nullable: anyx.ConvString(re["NULLABLE"]),
ColumnKey: anyx.ConvString(re["COLUMN_KEY"]),
IsPrimaryKey: anyx.ConvInt(re["IS_PRIMARY_KEY"]) == 1,
IsIdentity: anyx.ConvInt(re["IS_IDENTITY"]) == 1,
ColumnDefault: defaultVal,
NumScale: anyx.ConvString(re["NUM_SCALE"]),
})
@@ -120,7 +121,7 @@ func (od *OracleDialect) GetPrimaryKey(tablename string) (string, error) {
return "", errorx.NewBiz("[%s] 表不存在", tablename)
}
for _, v := range columns {
if v.ColumnKey == "PRI" {
if v.IsPrimaryKey {
return v.ColumnName, nil
}
}
@@ -142,7 +143,7 @@ func (od *OracleDialect) GetTableIndex(tableName string) ([]dbi.Index, error) {
ColumnName: anyx.ConvString(re["COLUMN_NAME"]),
IndexType: anyx.ConvString(re["INDEX_TYPE"]),
IndexComment: anyx.ConvString(re["INDEX_COMMENT"]),
NonUnique: anyx.ConvInt(re["NON_UNIQUE"]),
IsUnique: anyx.ConvInt(re["IS_UNIQUE"]) == 1,
SeqInIndex: anyx.ConvInt(re["SEQ_IN_INDEX"]),
})
}

View File

@@ -91,7 +91,8 @@ func (md *PgsqlDialect) GetColumns(tableNames ...string) ([]dbi.Column, error) {
ColumnType: anyx.ConvString(re["columnType"]),
ColumnComment: anyx.ConvString(re["columnComment"]),
Nullable: anyx.ConvString(re["nullable"]),
ColumnKey: anyx.ConvString(re["columnKey"]),
IsPrimaryKey: anyx.ConvInt(re["isPrimaryKey"]) == 1,
IsIdentity: anyx.ConvInt(re["isIdentity"]) == 1,
ColumnDefault: anyx.ConvString(re["columnDefault"]),
NumScale: anyx.ConvString(re["numScale"]),
})
@@ -108,7 +109,7 @@ func (md *PgsqlDialect) GetPrimaryKey(tablename string) (string, error) {
return "", errorx.NewBiz("[%s] 表不存在", tablename)
}
for _, v := range columns {
if v.ColumnKey == "PRI" {
if v.IsPrimaryKey {
return v.ColumnName, nil
}
}
@@ -130,7 +131,7 @@ func (md *PgsqlDialect) GetTableIndex(tableName string) ([]dbi.Index, error) {
ColumnName: anyx.ConvString(re["columnName"]),
IndexType: anyx.ConvString(re["IndexType"]),
IndexComment: anyx.ConvString(re["indexComment"]),
NonUnique: anyx.ConvInt(re["nonUnique"]),
IsUnique: anyx.ConvInt(re["isUnique"]) == 1,
SeqInIndex: anyx.ConvInt(re["seqInIndex"]),
})
}

View File

@@ -90,17 +90,14 @@ func (sd *SqliteDialect) GetColumns(tableNames ...string) ([]dbi.Column, error)
if strings.Contains(defaultValue, "'") {
defaultValue = strings.ReplaceAll(defaultValue, "'", "")
}
columnKey := ""
if anyx.ConvInt(re["pk"]) == 1 {
columnKey = "PRI"
}
columns = append(columns, dbi.Column{
TableName: tableName,
ColumnName: anyx.ConvString(re["name"]),
ColumnType: strings.ToLower(anyx.ConvString(re["type"])),
ColumnComment: "",
Nullable: nullable,
ColumnKey: columnKey,
IsPrimaryKey: anyx.ConvInt(re["pk"]) == 1,
IsIdentity: anyx.ConvInt(re["pk"]) == 1,
ColumnDefault: defaultValue,
NumScale: "0",
})
@@ -150,17 +147,13 @@ func (sd *SqliteDialect) GetTableIndex(tableName string) ([]dbi.Index, error) {
for _, re := range res {
indexSql := anyx.ConvString(re["indexSql"])
isUnique := strings.Contains(indexSql, "CREATE UNIQUE INDEX")
nonUnique := 1
if isUnique {
nonUnique = 0
}
indexs = append(indexs, dbi.Index{
IndexName: anyx.ConvString(re["indexName"]),
ColumnName: extractIndexFields(indexSql),
IndexType: anyx.ConvString(re["indexType"]),
IndexComment: anyx.ConvString(re["indexComment"]),
NonUnique: nonUnique,
IsUnique: isUnique,
SeqInIndex: 1,
})
}

View File

@@ -24,6 +24,7 @@ type MachineInfo struct {
Passphrase string `json:"-"` // 私钥口令
SshTunnelMachine *MachineInfo `json:"-"` // ssh隧道机器
TempSshMachineId uint64 `json:"-"` // ssh隧道机器id用于记录隧道机器id连接出错后关闭隧道
EnableRecorder int8 `json:"-"` // 是否启用终端回放记录
TagPath []string `json:"tagPath"`
}
@@ -47,10 +48,10 @@ func (mi *MachineInfo) Conn() (*Cli, error) {
}
cli := &Cli{Info: mi}
sshClient, err := GetSshClient(mi)
sshClient, err := GetSshClient(mi, nil)
if err != nil {
if mi.UseSshTunnel() {
CloseSshTunnelMachine(int(mi.SshTunnelMachine.Id), mi.GetTunnelId())
CloseSshTunnelMachine(int(mi.TempSshMachineId), mi.GetTunnelId())
}
return nil, err
}
@@ -83,10 +84,29 @@ func (me *MachineInfo) IfUseSshTunnelChangeIpPort() error {
// 修改机器ip地址
me.Ip = exposeIp
me.Port = exposePort
// 代理之后置空跳板机信息,防止重复跳
me.TempSshMachineId = me.SshTunnelMachine.Id
me.SshTunnelMachine = nil
return nil
}
func GetSshClient(m *MachineInfo) (*ssh.Client, error) {
func GetSshClient(m *MachineInfo, jumpClient *ssh.Client) (*ssh.Client, error) {
// 递归一直取到底层没有跳板机的机器信息
if m.SshTunnelMachine != nil {
jumpClient, _ = GetSshClient(m.SshTunnelMachine, jumpClient)
// 新建一个没有跳板机的机器信息
m1 := &MachineInfo{
Ip: m.Ip,
Port: m.Port,
AuthMethod: m.AuthMethod,
Username: m.Username,
Password: m.Password,
Passphrase: m.Passphrase,
}
// 使用跳板机连接目标机器
return GetSshClient(m1, jumpClient)
}
// 配置 SSH 客户端
config := &ssh.ClientConfig{
User: m.Username,
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
@@ -94,7 +114,6 @@ func GetSshClient(m *MachineInfo) (*ssh.Client, error) {
},
Timeout: 5 * time.Second,
}
if m.AuthMethod == entity.AuthCertAuthMethodPassword {
config.Auth = []ssh.AuthMethod{ssh.Password(m.Password)}
} else if m.AuthMethod == entity.MachineAuthMethodPublicKey {
@@ -113,6 +132,19 @@ func GetSshClient(m *MachineInfo) (*ssh.Client, error) {
}
addr := fmt.Sprintf("%s:%d", m.Ip, m.Port)
if jumpClient != nil {
// 连接目标服务器
netConn, err := jumpClient.Dial("tcp", addr)
if err != nil {
return nil, err
}
conn, channel, reqs, err := ssh.NewClientConn(netConn, addr, config)
if err != nil {
return nil, err
}
// 创建目标服务器的 SSH 客户端
return ssh.NewClient(conn, channel, reqs), nil
}
sshClient, err := ssh.Dial("tcp", addr, config)
if err != nil {
return nil, err

View File

@@ -156,7 +156,7 @@ func GetSshTunnelMachine(machineId int, getMachine func(uint64) (*MachineInfo, e
return nil, err
}
sshClient, err := GetSshClient(me)
sshClient, err := GetSshClient(me, nil)
if err != nil {
return nil, err
}