Files
mayfly-go/mayfly_go_web/src/views/ops/db/SqlExec.vue

522 lines
18 KiB
Vue
Raw Normal View History

<template>
<div>
2023-02-15 21:28:01 +08:00
<el-row>
<el-col :span="4">
<el-button type="primary" icon="plus" @click="addQueryTab({ id: nowDbInst.id }, state.db)"
size="small">新建查询</el-button>
2023-02-06 17:14:16 +08:00
</el-col>
2023-02-15 21:28:01 +08:00
<el-col :span="20" v-if="state.db">
<el-descriptions :column="4" size="small" border style="height: 10px">
<el-descriptions-item label-align="right" label="tag">{{ nowDbInst.tagPath }}</el-descriptions-item>
<el-descriptions-item label="实例" label-align="right">
{{ nowDbInst.id }}
<el-divider direction="vertical" border-style="dashed" />
{{ nowDbInst.type }}
<el-divider direction="vertical" border-style="dashed" />
{{ nowDbInst.name }}
</el-descriptions-item>
<el-descriptions-item label="库名" label-align="right">{{ state.db }}</el-descriptions-item>
</el-descriptions>
</el-col>
</el-row>
<el-row type="flex">
2023-02-06 17:14:16 +08:00
<el-col :span="4" style="border-left: 1px solid #eee; margin-top: 10px">
<InstanceTree ref="instanceTreeRef" @change-instance="changeInstance" @change-schema="changeSchema"
@clickSqlName="onClickSqlName" @clickSchemaTable="loadTableData" />
2023-02-06 17:14:16 +08:00
</el-col>
<el-col :span="20">
<el-container id="data-exec" style="border-left: 1px solid #eee; margin-top: 10px">
2023-02-15 21:28:01 +08:00
<el-tabs @tab-remove="onRemoveTab" @tab-change="onTabChange" style="width: 100%"
2023-02-06 17:14:16 +08:00
v-model="state.activeName">
<el-tab-pane closable v-for="dt in state.tabs.values()" :key="dt.key" :label="dt.key"
2023-02-06 17:14:16 +08:00
:name="dt.key">
<table-data v-if="dt.type === TabType.TableData" @gen-insert-sql="onGenerateInsertSql"
:data="dt" :table-height="state.dataTabsTableHeight"></table-data>
<query v-else @save-sql-success="reloadSqls" @delete-sql-success="deleteSqlScript(dt)"
:data="dt" :editor-height="state.editorHeight">
</query>
2023-02-06 17:14:16 +08:00
</el-tab-pane>
</el-tabs>
</el-container>
</el-col>
</el-row>
<el-dialog @close="state.genSqlDialog.visible = false" v-model="state.genSqlDialog.visible" title="SQL"
width="1000px">
2023-01-31 10:37:40 +08:00
<el-input v-model="state.genSqlDialog.sql" type="textarea" rows="20" />
</el-dialog>
</div>
</template>
<script lang="ts" setup>
2023-02-15 21:28:01 +08:00
import { onMounted, reactive, ref, Ref, toRefs } from 'vue';
import { ElMessage } from 'element-plus';
2022-11-01 22:18:54 +08:00
2023-02-06 17:14:16 +08:00
import { language as sqlLanguage } from 'monaco-editor/esm/vs/basic-languages/mysql/mysql.js';
2022-11-01 22:18:54 +08:00
import * as monaco from 'monaco-editor';
2023-02-06 17:14:16 +08:00
import { editor, languages, Position } from 'monaco-editor';
2023-01-31 10:37:40 +08:00
import InstanceTree from '@/views/ops/db/component/InstanceTree.vue';
import { DbInst, TabInfo, TabType } from './db'
import TableData from './component/tab/TableData.vue'
import Query from './component/tab/Query.vue'
const instanceTreeRef = ref(null) as Ref;
2023-01-31 10:37:40 +08:00
const tabs: Map<string, TabInfo> = new Map();
const state = reactive({
2023-02-15 21:28:01 +08:00
/**
* 当前操作的数据库实例
*/
nowDbInst: {} as DbInst,
db: '', // 当前操作的数据库
activeName: 'Query',
tabs,
dataTabsTableHeight: '600',
editorHeight: '600',
genSqlDialog: {
visible: false,
sql: '',
},
});
2023-02-15 21:28:01 +08:00
const {
nowDbInst,
} = toRefs(state);
onMounted(() => {
self.completionItemProvider?.dispose()
setHeight();
// 监听浏览器窗口大小变化,更新对应组件高度
window.onresize = () => setHeight();
2022-11-05 22:18:59 +08:00
});
/**
* 设置editor高度和数据表高度
*/
const setHeight = () => {
// 默认300px
// state.monacoOptions.height = window.innerHeight - 518 + 'px'
state.editorHeight = window.innerHeight - 518 + 'px';
state.dataTabsTableHeight = window.innerHeight - 219 - 36 + 'px';
2022-10-28 17:42:23 +08:00
};
// 选择数据库实例
const changeInstance = (inst: any, fn?: Function) => {
fn && fn()
}
2023-02-06 17:14:16 +08:00
// 选择数据库
const changeSchema = (inst: any, schema: string) => {
2023-02-15 21:28:01 +08:00
state.nowDbInst = DbInst.getOrNewInst(inst);
state.db = schema;
}
2022-10-28 17:42:23 +08:00
// 加载选中的表数据即新增表数据操作tab
const loadTableData = async (inst: any, schema: string, tableName: string) => {
changeSchema(inst, schema)
if (tableName == '') {
return;
}
2022-11-07 21:57:51 +08:00
const label = `${inst.id}:\`${schema}\`.${tableName}`;
let tab = state.tabs.get(label);
state.activeName = label;
// 如果存在该表tab则直接返回
if (tab) {
return;
}
tab = new TabInfo();
tab.key = label;
tab.dbId = inst.id;
tab.db = schema;
tab.type = TabType.TableData;
2023-02-14 11:44:48 +08:00
tab.params = {
table: tableName
}
state.tabs.set(label, tab)
}
// 新建查询panel
const addQueryTab = async (inst: any, db: string, sqlName: string = '') => {
2023-02-15 21:28:01 +08:00
if (!db || !inst.id) {
ElMessage.warning('请选择数据库实例及对应的schema')
return
}
const dbId = inst.id;
let label;
// 存在sql模板名则该模板名只允许一个tab
if (sqlName) {
label = `查询:${dbId}:${db}.${sqlName}`;
} else {
let count = 1;
state.tabs.forEach((v) => {
2023-02-14 11:44:48 +08:00
if (v.type == TabType.Query && !v.params.sqlName) {
count++;
2022-11-07 21:57:51 +08:00
}
})
label = `新查询${count}:${dbId}:${db}`;
}
state.activeName = label;
let tab = state.tabs.get(label);
if (tab) {
return;
}
tab = new TabInfo();
tab.key = label;
tab.dbId = dbId;
tab.db = db;
tab.type = TabType.Query;
2023-02-14 11:44:48 +08:00
tab.params = {
sqlName: sqlName,
dbs: instanceTreeRef.value.getSchemas(dbId)
}
state.tabs.set(label, tab)
registerSqlCompletionItemProvider();
}
2023-02-15 21:28:01 +08:00
const onRemoveTab = (targetName: string) => {
let activeName = state.activeName;
const tabNames = [...state.tabs.keys()]
for (let i = 0; i < tabNames.length; i++) {
const tabName = tabNames[i]
if (tabName !== targetName) {
continue;
2022-11-07 21:57:51 +08:00
}
const nextTab = tabNames[i + 1] || tabNames[i - 1];
if (nextTab) {
activeName = nextTab;
2023-02-15 21:28:01 +08:00
} else {
activeName = '';
}
state.tabs.delete(targetName);
state.activeName = activeName;
}
};
2023-02-15 21:28:01 +08:00
const onTabChange = () => {
if (!state.activeName) {
state.nowDbInst = {} as DbInst;
state.db = '';
return;
}
state.nowDbInst = DbInst.getInst(state.tabs.get(state.activeName)?.dbId);
}
2022-11-07 21:57:51 +08:00
const onGenerateInsertSql = async (sql: string) => {
state.genSqlDialog.sql = sql;
state.genSqlDialog.visible = true;
};
const onClickSqlName = (inst: any, schema: string, sqlName: string) => {
addQueryTab(inst, schema, sqlName);
}
2022-11-01 22:18:54 +08:00
const reloadSqls = (dbId: number, db: string) => {
instanceTreeRef.value.reloadSqls({ id: dbId }, db);
}
const deleteSqlScript = (ti: TabInfo) => {
instanceTreeRef.value.reloadSqls({ id: ti.dbId }, ti.db);
2023-02-15 21:28:01 +08:00
onRemoveTab(ti.key);
}
const registerSqlCompletionItemProvider = () => {
2022-10-28 17:42:23 +08:00
// 参考 https://microsoft.github.io/monaco-editor/playground.html#extending-language-services-completion-provider-example
2023-01-31 10:37:40 +08:00
self.completionItemProvider = self.completionItemProvider || monaco.languages.registerCompletionItemProvider('sql', {
2022-11-05 15:13:40 +08:00
triggerCharacters: ['.'],
2022-11-01 22:18:54 +08:00
provideCompletionItems: async (model: editor.ITextModel, position: Position): Promise<languages.CompletionList | null | undefined> => {
2022-10-28 17:42:23 +08:00
let word = model.getWordUntilPosition(position);
const nowTab = state.tabs.get(state.activeName);
if (!nowTab) {
return;
}
2023-02-15 21:28:01 +08:00
const { db, dbId } = nowTab;
const dbInst = DbInst.getInst(dbId);
2022-11-05 15:13:40 +08:00
const { lineNumber, column } = position
const { startColumn, endColumn } = word
2022-11-01 22:18:54 +08:00
// 当前行文本
let lineContent = model.getLineContent(lineNumber);
// 注释行不需要代码提示
2022-11-05 15:13:40 +08:00
if (lineContent.startsWith('--')) {
return { suggestions: [] }
2022-11-01 22:18:54 +08:00
}
2022-11-05 15:13:40 +08:00
2022-10-28 17:42:23 +08:00
let range = {
2022-11-01 22:18:54 +08:00
startLineNumber: lineNumber,
endLineNumber: lineNumber,
startColumn,
endColumn,
2022-10-28 17:42:23 +08:00
};
2022-11-05 15:13:40 +08:00
2022-11-01 22:18:54 +08:00
// 光标前文本
const textBeforePointer = model.getValueInRange({
startLineNumber: lineNumber,
startColumn: 0,
endLineNumber: lineNumber,
endColumn: column
})
const textBeforePointerMulti = model.getValueInRange({
startLineNumber: 1,
startColumn: 0,
endLineNumber: lineNumber,
endColumn: column
})
// 光标后文本
const textAfterPointerMulti = model.getValueInRange({
startLineNumber: lineNumber,
startColumn: column,
endLineNumber: model.getLineCount(),
endColumn: model.getLineMaxColumn(model.getLineCount())
})
// // const nextTokens = textAfterPointer.trim().split(/\s+/)
// // const nextToken = nextTokens[0].toLowerCase()
const tokens = textBeforePointer.trim().split(/\s+/)
const lastToken = tokens[tokens.length - 1].toLowerCase()
2023-02-14 11:44:48 +08:00
const dbs = nowTab.params && nowTab.params.dbs;
2022-11-05 15:13:40 +08:00
// console.log("光标前文本:=>" + textBeforePointerMulti)
2022-11-01 22:18:54 +08:00
2022-11-05 15:13:40 +08:00
// console.log("最后输入的:=>" + lastToken)
2022-11-01 22:18:54 +08:00
if (lastToken.endsWith('.')) {
// 如果是.触发代码提示,则进行【 库.表名联想 】 或 【 表别名.表字段联想 】
let str = lastToken.substring(0, lastToken.lastIndexOf('.'))
// 库.表名联想
2023-02-15 21:28:01 +08:00
if (dbs.filter((a: any) => a.name === str)?.length > 0) {
let tables = await dbInst.loadTables(str)
2022-11-01 22:18:54 +08:00
let suggestions: languages.CompletionItem[] = []
2022-11-05 15:13:40 +08:00
for (let item of tables) {
const { tableName, tableComment } = item
2022-11-01 22:18:54 +08:00
suggestions.push({
label: {
2022-11-21 19:49:50 +08:00
label: tableName + (tableComment ? ' - ' + tableComment : ''),
description: 'table'
},
2022-11-01 22:18:54 +08:00
kind: monaco.languages.CompletionItemKind.File,
insertText: tableName,
range
2022-11-05 15:13:40 +08:00
});
2022-11-01 22:18:54 +08:00
}
return { suggestions }
}
let sql = textBeforePointerMulti.split(';')[textBeforePointerMulti.split(';').length - 1] + textAfterPointerMulti.split(';')[0];
// 表别名.表字段联想
2023-01-31 10:37:40 +08:00
let tableInfo = getTableByAlias(sql, db, str)
2022-11-05 15:13:40 +08:00
if (tableInfo.tableName) {
2022-11-01 22:18:54 +08:00
let table = tableInfo.tableName
let db = tableInfo.dbName;
2022-11-01 22:18:54 +08:00
// 取出表名并提示
let dbHits = await dbInst.loadDbHints(db)
let columns = dbHits[table]
2022-11-01 22:18:54 +08:00
let suggestions: languages.CompletionItem[] = []
2023-02-14 12:23:38 +08:00
columns?.forEach((a: string, index: any) => {
2022-11-01 22:18:54 +08:00
// 字段数据格式 字段名 字段注释, 如: create_time [datetime][创建时间]
2022-11-05 15:13:40 +08:00
const nameAndComment = a.split(" ")
const fieldName = nameAndComment[0]
2022-11-01 22:18:54 +08:00
suggestions.push({
label: {
label: a,
description: 'column'
},
2022-11-01 22:18:54 +08:00
kind: monaco.languages.CompletionItemKind.Property,
2022-11-05 15:13:40 +08:00
detail: '', // 不显示detail, 否则选中时备注等会被遮挡
2022-11-19 15:05:44 +08:00
insertText: fieldName + ' ', // create_time
2022-11-05 15:13:40 +08:00
range,
2022-11-05 21:08:01 +08:00
sortText: 100 + index + '' // 使用表字段声明顺序排序,排序需为字符串类型
2022-11-01 22:18:54 +08:00
});
})
return { suggestions }
}
2022-11-05 15:13:40 +08:00
return { suggestions: [] }
}
2022-10-28 17:42:23 +08:00
2022-11-01 22:18:54 +08:00
// 库名联想
let suggestions: languages.CompletionItem[] = []
// mysql关键字
2022-10-28 17:42:23 +08:00
sqlLanguage.keywords.forEach((item: any) => {
suggestions.push({
label: {
2022-11-21 19:49:50 +08:00
label: item,
description: 'keyword'
},
2022-10-28 17:42:23 +08:00
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: item,
range
});
})
2022-11-19 15:05:44 +08:00
// 操作符
2022-10-28 17:42:23 +08:00
sqlLanguage.operators.forEach((item: any) => {
suggestions.push({
label: {
2022-11-21 19:49:50 +08:00
label: item,
description: 'opt'
},
2022-10-28 17:42:23 +08:00
kind: monaco.languages.CompletionItemKind.Operator,
insertText: item,
range
});
})
2022-11-01 22:18:54 +08:00
// 内置函数
2022-10-28 17:42:23 +08:00
sqlLanguage.builtinFunctions.forEach((item: any) => {
suggestions.push({
label: {
2022-11-21 19:49:50 +08:00
label: item,
description: 'func'
},
2022-10-28 17:42:23 +08:00
kind: monaco.languages.CompletionItemKind.Function,
insertText: item,
range
});
})
2022-11-01 22:18:54 +08:00
// 内置变量
sqlLanguage.builtinVariables.forEach((item: string) => {
2022-10-28 17:42:23 +08:00
suggestions.push({
label: {
2022-11-21 19:49:50 +08:00
label: item,
description: 'var'
},
2022-10-28 17:42:23 +08:00
kind: monaco.languages.CompletionItemKind.Variable,
insertText: item,
range
});
})
2022-11-01 22:18:54 +08:00
// 库名提示
2023-02-14 12:23:38 +08:00
dbs.forEach((a: any) => {
2022-11-01 22:18:54 +08:00
suggestions.push({
label: {
2023-02-14 12:23:38 +08:00
label: a.name,
description: 'schema'
},
2022-11-01 22:18:54 +08:00
kind: monaco.languages.CompletionItemKind.Folder,
2023-02-14 12:23:38 +08:00
insertText: a.name,
2022-11-01 22:18:54 +08:00
range
2022-11-05 15:13:40 +08:00
});
2022-11-01 22:18:54 +08:00
})
const tables = await dbInst.loadTables(db);
2022-11-01 22:18:54 +08:00
// 表名联想
tables.forEach((tableMeta: any) => {
2022-11-05 15:13:40 +08:00
const { tableName, tableComment } = tableMeta
2022-11-01 22:18:54 +08:00
suggestions.push({
label: {
2022-11-21 19:49:50 +08:00
label: tableName + ' - ' + tableComment,
description: 'table'
},
2022-11-01 22:18:54 +08:00
kind: monaco.languages.CompletionItemKind.File,
detail: tableComment,
2022-11-05 15:13:40 +08:00
insertText: tableName + ' ',
2022-11-01 22:18:54 +08:00
range
2022-11-05 15:13:40 +08:00
});
2022-11-01 22:18:54 +08:00
})
2022-10-28 17:42:23 +08:00
// 默认提示
return {
suggestions: suggestions
2022-11-05 15:13:40 +08:00
};
2022-10-28 17:42:23 +08:00
},
});
}
2022-11-01 22:18:54 +08:00
/**
* 根据别名获取sql里的表名
* @param sql sql
* @param db 默认数据库
* @param alias 别名
*/
2022-11-05 15:13:40 +08:00
const getTableByAlias = (sql: string, db: string, alias: string): { dbName: string, tableName: string } => {
2022-11-01 22:18:54 +08:00
// 表别名:表名
let result = {};
let defName = '';
let defResult = {};
// 正则匹配取出表名和表别名
// 测试sql
/*
2022-11-19 15:05:44 +08:00
2022-11-01 22:18:54 +08:00
`select * from database.Outvisit l
left join patient p on l.patid=p.patientid
join patstatic c on l.patid=c.patid inner join patphone ph on l.patid=ph.patid
where l.name='kevin' and exsits(select 1 from pharmacywestpas pw where p.outvisitid=l.outvisitid)
unit all
select * from invisit v where`.match(/(join|from)\s+(\w*-?\w*\.?\w+)\s*(as)?\s*(\w*)/gi)
*/
let match = sql.match(/(join|from)\n*\s+\n*(\w*-?\w*\.?\w+)\s*(as)?\s*(\w*)\n*/gi)
2022-11-05 15:13:40 +08:00
if (match && match.length > 0) {
match.forEach(a => {
2022-11-01 22:18:54 +08:00
// 去掉前缀,取出
let t = a.substring(5, a.length)
.replaceAll(/\s+/g, ' ')
.replaceAll(/\s+as\s+/gi, ' ')
.replaceAll(/\r\n/g, ' ').trim()
2022-11-01 22:18:54 +08:00
.split(/\s+/);
let withDb = t[0].split('.');
// 表名是 db名.表名
let tName = withDb.length > 1 ? withDb[1] : withDb[0]
2022-11-05 15:13:40 +08:00
let dbName = withDb.length > 1 ? withDb[0] : (db || '')
if (t.length == 2) {
2022-11-01 22:18:54 +08:00
// 表别名:表名
2022-11-05 15:13:40 +08:00
result[t[1]] = { tableName: tName, dbName }
} else {
2022-11-01 22:18:54 +08:00
// 只有表名无别名 取第一个无别名的表为默认表
!defName && (defResult = { tableName: tName, dbName: db })
2022-10-28 17:42:23 +08:00
}
2022-11-01 22:18:54 +08:00
})
2022-10-28 17:42:23 +08:00
}
2022-11-01 22:18:54 +08:00
return result[alias] || defResult
2022-10-28 17:42:23 +08:00
}
</script>
2022-03-15 22:27:39 +08:00
<style lang="scss">
2022-11-22 00:34:42 +08:00
.sql-file-exec {
2022-11-22 17:15:08 +08:00
display: inline-flex;
flex-direction: row;
align-items: center;
justify-content: center;
vertical-align: middle;
position: relative;
text-decoration: none;
2022-11-22 00:34:42 +08:00
}
2022-11-22 17:15:08 +08:00
.sqlEditor {
font-size: 8pt;
font-weight: 600;
border: 1px solid #ccc;
}
2022-11-01 22:18:54 +08:00
.editor-move-resize {
cursor: n-resize;
height: 3px;
text-align: center;
}
2022-01-16 19:40:20 +08:00
.el-tabs__header {
2022-01-19 15:10:17 +08:00
padding: 0 10px;
2022-01-16 19:40:20 +08:00
background-color: #fff;
}
2022-02-07 17:49:48 +08:00
#data-exec {
min-height: calc(100vh - 155px);
2023-02-06 17:14:16 +08:00
.el-tabs__header {
margin: 0 0 5px;
.el-tabs__item {
padding: 0 5px;
}
2023-01-31 10:37:40 +08:00
}
2022-02-07 17:49:48 +08:00
}
2022-11-22 17:15:08 +08:00
.update_field_active {
background-color: var(--el-color-success)
2022-11-22 00:34:42 +08:00
}
</style>