Files
mayfly-go/mayfly_go_web/src/views/ops/db/db.ts

734 lines
24 KiB
TypeScript
Raw Normal View History

/* eslint-disable no-unused-vars */
import { dbApi } from './api';
import { getTextWidth } from '@/common/utils/string';
import SqlExecBox from './component/sqleditor/SqlExecBox';
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import { editor, languages, Position } from 'monaco-editor';
import { registerCompletionItemProvider } from '@/components/monaco/completionItemProvider';
2024-01-08 11:24:37 +08:00
import {DbDialect, EditorCompletionItem, getDbDialect} from './dialect'
const dbInstCache: Map<number, DbInst> = new Map();
export class DbInst {
2023-02-15 21:28:01 +08:00
/**
*
*/
tagPath: string;
2023-02-15 21:28:01 +08:00
/**
* id
*/
id: number;
2023-11-13 17:41:03 +08:00
/**
* ip:port
*/
host: string;
2023-02-15 21:28:01 +08:00
/**
*
*/
name: string;
2023-02-15 21:28:01 +08:00
/**
* , mysql postgres
*/
type: string;
/**
* dbName -> db
*/
dbs: Map<string, Db> = new Map();
/** 数据库,多个用空格隔开 */
databases: string[];
2023-06-13 15:57:08 +08:00
/**
*
*/
static DefaultLimit = 25;
/**
*
* @param dbName
* @returns db实例
*/
getDb(dbName: string) {
if (!dbName) {
throw new Error('dbName不能为空');
}
let db = this.dbs.get(dbName);
if (db) {
return db;
}
console.info(`new db -> dbId: ${this.id}, dbName: ${dbName}`);
db = new Db();
db.name = dbName;
this.dbs.set(dbName, db);
return db;
}
/**
*
* @param dbName
2023-03-28 15:22:05 +08:00
* @param reload
* @returns
*/
2023-03-28 15:22:05 +08:00
async loadTables(dbName: string, reload?: boolean) {
const db = this.getDb(dbName);
// 优先从 table map中获取
let tables = db.tables;
2023-03-28 15:22:05 +08:00
if (!reload && tables) {
return tables;
}
2023-04-05 22:41:53 +08:00
// 重置列信息缓存与表提示信息
db.columnsMap?.clear();
db.tableHints = null;
console.log(`load tables -> dbName: ${dbName}`);
tables = await dbApi.tableInfos.request({ id: this.id, db: dbName });
db.tables = tables;
return tables;
}
2024-01-08 11:24:37 +08:00
async loadTableSuggestions(dbDialect: DbDialect, dbName: string, range: any, reload?: boolean) {
const tables = await this.loadTables(dbName, reload);
// 表名联想
let suggestions: languages.CompletionItem[] = [];
tables?.forEach((tableMeta: any, index: any) => {
const { tableName, tableComment } = tableMeta;
suggestions.push({
label: {
label: tableName + ' - ' + tableComment,
description: 'table',
},
kind: monaco.languages.CompletionItemKind.File,
detail: tableComment,
2024-01-08 11:24:37 +08:00
insertText: dbDialect.wrapName(tableName) + ' ',
range,
sortText: 300 + index + '',
});
});
return { suggestions };
}
/** 加载列信息提示 */
2024-01-08 11:24:37 +08:00
async loadTableColumnSuggestions(dbDialect: DbDialect,db: string, tableName: string, range: any) {
let dbHits = await this.loadDbHints(db);
let columns = dbHits[tableName];
let suggestions: languages.CompletionItem[] = [];
columns?.forEach((a: string, index: any) => {
// 字段数据格式 字段名 字段注释, 如: create_time [datetime][创建时间]
const nameAndComment = a.split(' ');
const fieldName = nameAndComment[0];
suggestions.push({
label: {
label: a,
description: 'column',
},
kind: monaco.languages.CompletionItemKind.Property,
detail: '', // 不显示detail, 否则选中时备注等会被遮挡
2024-01-08 11:24:37 +08:00
insertText: dbDialect.wrapName(fieldName)+ ' ', // create_time
range,
sortText: 100 + index + '', // 使用表字段声明顺序排序,排序需为字符串类型
});
});
return { suggestions };
}
/**
*
* @param dbName
* @param table
*/
async loadColumns(dbName: string, table: string) {
const db = this.getDb(dbName);
// 优先从 table map中获取
let columns = db.getColumns(table);
if (columns) {
return columns;
}
console.log(`load columns -> dbName: ${dbName}, table: ${table}`);
columns = await dbApi.columnMetadata.request({
id: this.id,
db: dbName,
tableName: table,
});
db.columnsMap.set(table, columns);
return columns;
}
/**
*
* @param table
*/
async loadTableColumn(dbName: string, table: string, columnName?: string) {
// 确保该表的列信息都已加载
await this.loadColumns(dbName, table);
return this.getDb(dbName).getColumn(table, columnName);
}
/**
*
*/
async loadDbHints(dbName: string) {
const db = this.getDb(dbName);
if (db.tableHints) {
return db.tableHints;
}
console.log(`load db-hits -> dbName: ${dbName}`);
const hits = await dbApi.hintTables.request({ id: this.id, db: db.name });
db.tableHints = hits;
return hits;
}
/**
* sql
*
* @param sql sql
* @param remark
*/
async runSql(dbName: string, sql: string, remark: string = '') {
return await dbApi.sqlExec.request({
id: this.id,
db: dbName,
sql: sql.trim(),
remark,
});
}
/**
* sql()
*
* @param sql sql
* @param remark
*/
execSql(dbName: string, sql: string, remark: string = '') {
let dbId = this.id;
return dbApi.sqlExec.useApi({
id: dbId,
db: dbName,
sql: sql.trim(),
remark,
});
}
/**
* count sql
* @param table
* @param condition
* @returns count sql
*/
getDefaultCountSql = (table: string, condition?: string) => {
return `SELECT COUNT(*) count FROM ${this.wrapName(table)} ${condition ? 'WHERE ' + condition : ''} limit 1`;
};
// 获取指定表的默认查询sql
getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number = DbInst.DefaultLimit) {
2023-11-26 21:21:35 +08:00
return getDbDialect(this.type).getDefaultSelectSql(table, condition, orderBy, pageNum, limit);
}
/**
* insert语句
* @param dbName
* @param table
* @param datas
*/
async genInsertSql(dbName: string, table: string, datas: any[]) {
if (!datas) {
return '';
}
const columns = await this.loadColumns(dbName, table);
const sqls = [];
for (let data of datas) {
let colNames = [];
let values = [];
for (let column of columns) {
const colName = column.columnName;
colNames.push(this.wrapName(colName));
values.push(DbInst.wrapValueByType(data[colName]));
}
sqls.push(`INSERT INTO ${this.wrapName(table)} (${colNames.join(', ')}) VALUES(${values.join(', ')})`);
}
return sqls.join(';\n') + ';';
}
/**
* sql语句
* @param table
* @param datas
*/
async genDeleteByPrimaryKeysSql(db: string, table: string, datas: any[]) {
const primaryKey = await this.loadTableColumn(db, table);
const primaryKeyColumnName = primaryKey.columnName;
const ids = datas.map((d: any) => `${DbInst.wrapColumnValue(primaryKey.columnType, d[primaryKeyColumnName])}`).join(',');
return `DELETE FROM ${this.wrapName(table)} WHERE ${this.wrapName(primaryKeyColumnName)} IN (${ids})`;
}
2023-06-13 15:57:08 +08:00
/*
* sql
*/
promptExeSql = (db: string, sql: string, cancelFunc: any = null, successFunc: any = null) => {
SqlExecBox({
sql,
dbId: this.id,
db,
runSuccessCallback: successFunc,
cancelCallback: cancelFunc,
});
};
/**
* 使
* @param table
* @param condition
* @returns
*/
wrapName = (name: string) => {
2023-11-26 21:21:35 +08:00
return getDbDialect(this.type).wrapName(name);
};
2023-02-15 21:28:01 +08:00
/**
* dbInst
* @param inst
* @returns DbInst
*/
static getOrNewInst(inst: any) {
if (!inst) {
throw new Error('inst不能为空');
2023-02-15 21:28:01 +08:00
}
let dbInst = dbInstCache.get(inst.id);
if (dbInst) {
return dbInst;
}
console.info(`new dbInst: ${inst.id}, tagPath: ${inst.tagPath}`);
dbInst = new DbInst();
dbInst.tagPath = inst.tagPath;
dbInst.id = inst.id;
2023-11-13 17:41:03 +08:00
dbInst.host = inst.host;
2023-02-15 21:28:01 +08:00
dbInst.name = inst.name;
dbInst.type = inst.type;
2023-06-13 15:57:08 +08:00
dbInst.databases = inst.databases;
2023-02-15 21:28:01 +08:00
dbInstCache.set(dbInst.id, dbInst);
return dbInst;
}
/**
* id
* @param dbId id
* @param dbType
* @returns
*/
2023-02-15 21:28:01 +08:00
static getInst(dbId?: number): DbInst {
if (!dbId) {
throw new Error('dbId不能为空');
}
let dbInst = dbInstCache.get(dbId);
if (dbInst) {
return dbInst;
}
2023-02-15 21:28:01 +08:00
throw new Error('dbInst不存在! 请在合适调用点使用DbInst.newInst()新建该实例');
}
/**
*
*/
static clearAll() {
dbInstCache.clear();
}
/**
* ''
* @param val
* @returns
*/
static wrapValueByType = (val: any) => {
if (val == null) {
return 'NULL';
}
if (typeof val == 'number') {
return val;
}
return `'${val}'`;
};
/**
*
*/
static wrapColumnValue(columnType: string, value: any) {
if (this.isNumber(columnType)) {
return value;
}
return `'${value}'`;
}
/**
*
* @param columnType
2023-06-13 15:57:08 +08:00
* @returns
*/
static isNumber(columnType: string) {
return columnType.match(/int|double|float|number|decimal|byte|bit/gi);
}
/**
2023-06-13 15:57:08 +08:00
*
* @param str
* @param tableData
* @param flag
* @returns
*/
static flexColumnWidth = (prop: any, tableData: any) => {
if (!prop || !prop.length || prop.length === 0 || prop === undefined) {
return;
}
// 获取列名称的长度 加上排序图标长度、abc为字段类型简称占位符
const columnWidth: number = getTextWidth(prop + 'abc') + 23;
// prop为该列的字段名(传字符串);tableData为该表格的数据源(传变量);
if (!tableData || !tableData.length || tableData.length === 0 || tableData === undefined) {
return columnWidth;
}
// 获取该列中最长的数据(内容)
let maxWidthText = '';
// 获取该列中最长的数据(内容)
for (let i = 0; i < tableData.length; i++) {
let nowValue = tableData[i][prop];
if (!nowValue) {
continue;
}
// 转为字符串比较长度
let nowText = nowValue + '';
if (nowText.length > maxWidthText.length) {
maxWidthText = nowText;
}
}
const contentWidth: number = getTextWidth(maxWidthText) + 15;
const flexWidth: number = contentWidth > columnWidth ? contentWidth : columnWidth;
return flexWidth > 500 ? 500 : flexWidth;
};
}
/**
*
*/
class Db {
name: string; // 库名
tables: []; // 数据库实例表信息
columnsMap: Map<string, any> = new Map(); // table -> columns
tableHints: any = null; // 提示词
/**
* dbInst.loadColumns
* @param table
*/
getColumns(table: string) {
return this.columnsMap.get(table);
}
/**
*
* @param table
* @param columnName
*/
getColumn(table: string, columnName: string = '') {
const cols = this.getColumns(table);
if (!columnName) {
const col = cols.find((c: any) => c.columnKey == 'PRI');
return col || cols[0];
}
return cols.find((c: any) => c.columnName == columnName);
}
}
export enum TabType {
/**
*
*/
TableData,
/**
*
*/
Query,
/**
*
*/
TablesOp,
}
export class TabInfo {
label: string;
/**
* tab唯一keyname都一致
*/
key: string;
/**
* key
*/
treeNodeKey: string;
/**
* id
*/
dbId: number;
/**
*
*/
db: string = '';
/**
* tab
*/
type: TabType;
/**
* tab需要的其他信息
*/
params: any;
getNowDbInst() {
2023-02-15 21:28:01 +08:00
return DbInst.getInst(this.dbId);
}
getNowDb() {
return this.getNowDbInst().getDb(this.db);
}
}
2023-12-12 12:41:44 +08:00
function registerCompletions(
completions: EditorCompletionItem[],
suggestions: languages.CompletionItem[],
kind: monaco.languages.CompletionItemKind,
range: any
) {
// mysql关键字
completions.forEach((item: EditorCompletionItem) => {
let { label, insertText, description } = item;
suggestions.push({
label: { label, description },
kind,
insertText: insertText || label,
range,
});
});
}
/**
*
*
* @param dbId id
* @param db
* @param dbs
2023-12-12 12:41:44 +08:00
* @param dbType
*/
2023-12-12 12:41:44 +08:00
export function registerDbCompletionItemProvider(dbId: number, db: string, dbs: any[] = [], dbType: string) {
let dbDialect = getDbDialect(dbType);
let dbDialectInfo = dbDialect.getInfo();
let { keywords, operators, functions, variables } = dbDialectInfo.editorCompletions;
registerCompletionItemProvider('sql', {
triggerCharacters: ['.', ' '],
provideCompletionItems: async (model: editor.ITextModel, position: Position): Promise<languages.CompletionList | null | undefined> => {
let word = model.getWordUntilPosition(position);
const dbInst = DbInst.getInst(dbId);
const { lineNumber, column } = position;
const { startColumn, endColumn } = word;
// 当前行文本
let lineContent = model.getLineContent(lineNumber);
// 注释行不需要代码提示
if (lineContent.startsWith('--')) {
return { suggestions: [] };
}
let range = {
startLineNumber: lineNumber,
endLineNumber: lineNumber,
startColumn,
endColumn,
};
// 光标前文本
const textBeforePointer = model.getValueInRange({
startLineNumber: lineNumber,
startColumn: 0,
endLineNumber: lineNumber,
endColumn: column,
});
// // const nextTokens = textAfterPointer.trim().split(/\s+/)
// // const nextToken = nextTokens[0].toLowerCase()
const tokens = textBeforePointer.trim().split(/\s+/);
let lastToken = tokens[tokens.length - 1].toLowerCase();
const secondToken = (tokens.length > 2 && tokens[tokens.length - 2].toLowerCase()) || '';
// 获取光标所在行之前的所有文本内容
const textBeforeCursor = model.getValueInRange({
startLineNumber: 1,
startColumn: 0,
endLineNumber: lineNumber,
endColumn: column,
});
// 获取光标所在行之后的所有文本内容
const textAfterCursor = model.getValueInRange({
startLineNumber: lineNumber,
startColumn: column,
endLineNumber: model.getLineCount(),
endColumn: model.getLineMaxColumn(model.getLineCount()),
});
// 检测光标前后文本中的分号位置,确定完整 SQL 语句的范围
const start = textBeforeCursor.lastIndexOf(';');
const end = textAfterCursor.indexOf(';');
let sqlStatement = '';
// 如果光标前后都有分号,则取二者之间的文本作为完整 SQL 语句
if (start !== -1 && end !== -1) {
sqlStatement = textBeforeCursor.substring(start + 1) + textAfterCursor.substring(0, end);
}
// 如果只有光标前面有分号,则取分号后的文本作为完整 SQL 语句
else if (start !== -1) {
sqlStatement = textBeforeCursor.substring(start + 1) + textAfterCursor;
}
// 如果只有光标后面有分号,则取分号前的文本作为完整 SQL 语句
else if (end !== -1) {
sqlStatement = textBeforeCursor + textAfterCursor.substring(0, end);
}
// 如果光标前后都没有分号,则取整个文本作为完整 SQL 语句
else {
sqlStatement = textBeforeCursor + textAfterCursor;
}
let suggestions: languages.CompletionItem[] = [];
// 库名提示
if (dbs && dbs.length > 0) {
dbs.forEach((a: any) => {
suggestions.push({
label: {
label: a,
description: 'schema',
},
kind: monaco.languages.CompletionItemKind.Folder,
insertText: dbDialect.wrapName(a),
range,
});
});
}
let alias = '';
if (lastToken.indexOf('.') > -1 || secondToken.indexOf('.') > -1) {
// 如果是.触发代码提示,则进行【 库.表名联想 】 或 【 表别名.表字段联想 】
alias = lastToken.substring(0, lastToken.lastIndexOf('.'));
if (lastToken.trim().startsWith('.')) {
alias = secondToken;
}
if (!alias && secondToken.indexOf('.') > -1) {
alias = secondToken.substring(secondToken.indexOf('.') + 1);
}
// 如果字符串粘连起了如:'a.creator,a.',需要重新取出别名
let aliasArr = lastToken.split(',');
if (aliasArr.length > 1) {
lastToken = aliasArr[aliasArr.length - 1];
alias = lastToken.substring(0, lastToken.lastIndexOf('.'));
if (lastToken.trim().startsWith('.')) {
alias = secondToken;
}
}
// 如果是【库.表名联想】.前的字符串是库名
if (dbs?.filter((a) => alias === a?.toLowerCase()).length > 0) {
let dbName = alias;
if (db.indexOf('/') > 0) {
dbName = db.substring(0, db.indexOf('/') + 1) + alias;
}
2024-01-08 11:24:37 +08:00
return await dbInst.loadTableSuggestions(dbDialect, dbName, range);
}
// 表下列名联想 .前的字符串是表名或表别名
const sqlInfo = getTableName4SqlCtx(sqlStatement, alias, db);
// 提出到表名,则将表对应的字段也添加进提示建议
if (sqlInfo) {
2024-01-08 11:24:37 +08:00
return await dbInst.loadTableColumnSuggestions(dbDialect, sqlInfo.db, sqlInfo.tableName, range);
}
}
// 空格触发也会提示字段信息
const sqlInfo = getTableName4SqlCtx(sqlStatement, alias, db);
if (sqlInfo) {
2024-01-08 11:24:37 +08:00
const columnSuggestions = await dbInst.loadTableColumnSuggestions(dbDialect, sqlInfo.db, sqlInfo.tableName, range);
suggestions.push(...columnSuggestions.suggestions);
}
// 当前库的表名联想
const tables = await dbInst.loadTables(db);
tables.forEach((tableMeta: any, index: any) => {
const { tableName, tableComment } = tableMeta;
suggestions.push({
label: {
label: tableName + ' - ' + tableComment,
description: 'table',
},
kind: monaco.languages.CompletionItemKind.File,
detail: tableComment,
insertText: dbDialect.wrapName(tableName) + ' ',
range,
sortText: 300 + index + '',
});
});
2023-12-12 12:41:44 +08:00
registerCompletions(keywords, suggestions, monaco.languages.CompletionItemKind.Keyword, range);
registerCompletions(operators, suggestions, monaco.languages.CompletionItemKind.Operator, range);
registerCompletions(functions, suggestions, monaco.languages.CompletionItemKind.Function, range);
registerCompletions(variables, suggestions, monaco.languages.CompletionItemKind.Variable, range);
// 默认提示
return {
suggestions: suggestions,
};
},
});
}
function getTableName4SqlCtx(sql: string, alias: string = '', defaultDb: string): { tableName: string; tableAlias: string; db: string } | undefined {
// 去除多余的换行、空格和制表符
sql = sql.replace(/[\r\n\s\t]+/g, ' ');
// 提取所有可能的表名和别名
const regex = /(?:FROM|JOIN|UPDATE)\s+(\S+)\s+(?:AS\s+)?(\S+)/gi;
let matches;
const tables = [];
// 使用正则表达式匹配所有的表和别名
while ((matches = regex.exec(sql)) !== null) {
let tableName = matches[1].replace(/[`"]/g, '');
let db = defaultDb;
if (tableName.indexOf('.') >= 0) {
let info = tableName.split('.');
db = info[0];
if (defaultDb.indexOf('/') > 0) {
db = defaultDb.substring(0, defaultDb.indexOf('/') + 1) + db;
}
tableName = info[1];
}
const tableAlias = matches[2] ? matches[2].replace(/[`"]/g, '') : tableName;
tables.push({ tableName, tableAlias, db });
}
if (alias) {
// 如果指定了别名参数,则返回对应的表名
return tables.find((t) => t.tableAlias === alias);
} else {
// 如果未指定别名参数,则返回第一个表名
return tables.length > 0 ? tables[0] : undefined;
}
}