diff --git a/mayfly_go_web/.gitignore b/mayfly_go_web/.gitignore index 47d255b6..aff4e4fe 100644 --- a/mayfly_go_web/.gitignore +++ b/mayfly_go_web/.gitignore @@ -2,6 +2,7 @@ node_modules /dist *.lock +pnpm-lock.yaml # local env files .env.local diff --git a/mayfly_go_web/src/assets/iconfont/iconfont.js b/mayfly_go_web/src/assets/iconfont/iconfont.js index 7e31d647..5a4677b8 100644 --- a/mayfly_go_web/src/assets/iconfont/iconfont.js +++ b/mayfly_go_web/src/assets/iconfont/iconfont.js @@ -1,5 +1,5 @@ (window._iconfont_svg_string_3953964 = - ''), + ''), (function (c) { var t = (t = document.getElementsByTagName('script'))[t.length - 1], a = t.getAttribute('data-injectcss'), diff --git a/mayfly_go_web/src/assets/iconfont/iconfont.json b/mayfly_go_web/src/assets/iconfont/iconfont.json index 9cdae18d..8c38f080 100644 --- a/mayfly_go_web/src/assets/iconfont/iconfont.json +++ b/mayfly_go_web/src/assets/iconfont/iconfont.json @@ -53,6 +53,20 @@ "font_class": "redis", "unicode": "e619", "unicode_decimal": 58905 + }, + { + "icon_id": "11617944", + "name": "oracle", + "font_class": "oracle", + "unicode": "e6ea", + "unicode_decimal": 59114 + }, + { + "icon_id": "8105644", + "name": "mariadb", + "font_class": "mariadb", + "unicode": "e513", + "unicode_decimal": 58643 } ] } diff --git a/mayfly_go_web/src/views/ops/db/InstanceEdit.vue b/mayfly_go_web/src/views/ops/db/InstanceEdit.vue index f3054012..89692c9c 100644 --- a/mayfly_go_web/src/views/ops/db/InstanceEdit.vue +++ b/mayfly_go_web/src/views/ops/db/InstanceEdit.vue @@ -9,7 +9,7 @@ - + {{ dt.label }} @@ -24,6 +24,9 @@ + + + @@ -87,7 +90,7 @@ import { ElMessage } from 'element-plus'; import { notBlank } from '@/common/assert'; import { RsaEncrypt } from '@/common/rsa'; import SshTunnelSelect from '../component/SshTunnelSelect.vue'; -import { getDbDialect } from './dialect'; +import { DbType, getDbDialect } from './dialect'; import SvgIcon from '@/components/svgIcon/index.vue'; const props = defineProps({ @@ -134,6 +137,13 @@ const rules = { trigger: ['change', 'blur'], }, ], + sid: [ + { + required: true, + message: '请输入SID', + trigger: ['change', 'blur'], + }, + ], }; const dbForm: any = ref(null); @@ -155,6 +165,10 @@ const dbTypes = [ type: 'dm', label: '达梦', }, + { + type: 'oracle', + label: 'oracle', + }, ]; const state = reactive({ @@ -167,6 +181,7 @@ const state = reactive({ host: '', port: null, username: null, + sid: null, // oracle类项目需要服务id password: null, params: null, remark: '', diff --git a/mayfly_go_web/src/views/ops/db/SqlExec.vue b/mayfly_go_web/src/views/ops/db/SqlExec.vue index adcf4690..d85e3377 100644 --- a/mayfly_go_web/src/views/ops/db/SqlExec.vue +++ b/mayfly_go_web/src/views/ops/db/SqlExec.vue @@ -165,7 +165,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 } from './dialect/index'; +import { DbType, getDbDialect } from './dialect/index'; import { sleep } from '@/common/utils/loading'; import { TagResourceTypeEnum } from '@/common/commonEnum'; import { Pane, Splitpanes } from 'splitpanes'; @@ -259,7 +259,7 @@ const NodeTypeDb = new NodeType(SqlExecNodeType.Db) .withLoadNodesFunc(async (parentNode: TagTreeNode) => { const params = parentNode.params; // pg类数据库会多一层schema - if (params.type == 'postgres' || params.type === 'dm') { + if (params.type == DbType.postgresql || params.type === DbType.dm || params.type === DbType.oracle) { const params = parentNode.params; const { id, db } = params; const schemaNames = await dbApi.pgSchemas.request({ id, db }); diff --git a/mayfly_go_web/src/views/ops/db/SyncTaskEdit.vue b/mayfly_go_web/src/views/ops/db/SyncTaskEdit.vue index c04c0b66..1541ebeb 100644 --- a/mayfly_go_web/src/views/ops/db/SyncTaskEdit.vue +++ b/mayfly_go_web/src/views/ops/db/SyncTaskEdit.vue @@ -89,7 +89,7 @@ - + @@ -184,7 +184,7 @@ diff --git a/mayfly_go_web/src/views/ops/db/db.ts b/mayfly_go_web/src/views/ops/db/db.ts index d48cca1c..ba81424f 100644 --- a/mayfly_go_web/src/views/ops/db/db.ts +++ b/mayfly_go_web/src/views/ops/db/db.ts @@ -221,7 +221,7 @@ export class DbInst { * @returns count sql */ getDefaultCountSql = (table: string, condition?: string) => { - return `SELECT COUNT(*) count FROM ${this.wrapName(table)} ${condition ? 'WHERE ' + condition : ''} limit 1`; + return `SELECT COUNT(*) count FROM ${this.wrapName(table)} ${condition ? 'WHERE ' + condition : ''}`; }; // 获取指定表的默认查询sql @@ -358,11 +358,14 @@ export class DbInst { /** * 根据字段类型包装字段值,如为字符串等则添加‘’,数字类型则直接返回即可 */ - static wrapColumnValue(columnType: string, value: any) { + static wrapColumnValue(columnType: string, value: any, dbDialect?: DbDialect) { if (this.isNumber(columnType)) { return value; } - return `'${value}'`; + if (!dbDialect) { + return `${value}`; + } + return dbDialect.wrapStrValue(columnType, value); } /** diff --git a/mayfly_go_web/src/views/ops/db/dialect/dm_dialect.ts b/mayfly_go_web/src/views/ops/db/dialect/dm_dialect.ts index d8257d7d..cbc71e6c 100644 --- a/mayfly_go_web/src/views/ops/db/dialect/dm_dialect.ts +++ b/mayfly_go_web/src/views/ops/db/dialect/dm_dialect.ts @@ -541,7 +541,7 @@ class DMDialect implements DbDialect { // 创建索引 let sql: string[] = []; tableData.indexs.res.forEach((a: any) => { - sql.push(` CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${a.indexName} USING btree ("${a.columnNames.join('","')})"`); + sql.push(` CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${a.indexName} ON "${tableData.tableName}" ("${a.columnNames.join('","')})"`); }); return sql.join(';'); } @@ -608,10 +608,7 @@ class DMDialect implements DbDialect { if (addIndexs.length > 0) { addIndexs.forEach((a) => { - sql.push(`CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${a.indexName}(${a.columnNames.join(',')})`); - if (a.indexComment) { - sql.push(`COMMENT ON INDEX ${a.indexName} IS '${a.indexComment}'`); - } + sql.push(`CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${a.indexName} ON "${tableName}" (${a.columnNames.join(',')})`); }); } return sql.join(';'); @@ -637,4 +634,9 @@ class DMDialect implements DbDialect { } return DataType.String; } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars + wrapStrValue(columnType: string, value: string): string { + return `'${value}'`; + } } diff --git a/mayfly_go_web/src/views/ops/db/dialect/index.ts b/mayfly_go_web/src/views/ops/db/dialect/index.ts index 5a6d693e..d0f1c948 100644 --- a/mayfly_go_web/src/views/ops/db/dialect/index.ts +++ b/mayfly_go_web/src/views/ops/db/dialect/index.ts @@ -2,6 +2,8 @@ import { MysqlDialect } from './mysql_dialect'; import { PostgresqlDialect } from './postgres_dialect'; import { DMDialect } from '@/views/ops/db/dialect/dm_dialect'; import { SqlLanguage } from 'sql-formatter/lib/src/sqlFormatter'; +import { OracleDialect } from '@/views/ops/db/dialect/oracle_dialect'; +import { MariadbDialect } from '@/views/ops/db/dialect/mariadb_dialect'; export interface sqlColumnType { udtName: string; @@ -108,6 +110,7 @@ export const DbType = { mariadb: 'mariadb', postgresql: 'postgres', dm: 'dm', // 达梦 + oracle: 'oracle', }; export const compatibleMysql = (dbType: string): boolean => { @@ -176,11 +179,16 @@ export interface DbDialect { /** 通过数据库字段类型,返回基本数据类型 */ getDataType: (columnType: string) => DataType; + + /** 包装字符串数据, 如:oracle需要把date类型改为 to_date(str, 'yyyy-mm-dd hh24:mi:ss') */ + wrapStrValue(columnType: string, value: string): string; } let mysqlDialect = new MysqlDialect(); +let mariadbDialect = new MariadbDialect(); let postgresDialect = new PostgresqlDialect(); let dmDialect = new DMDialect(); +let oracleDialect = new OracleDialect(); export const getDbDialect = (dbType: string | undefined): DbDialect => { if (!dbType) { @@ -188,12 +196,15 @@ export const getDbDialect = (dbType: string | undefined): DbDialect => { } switch (dbType) { case DbType.mysql: - case DbType.mariadb: return mysqlDialect; + case DbType.mariadb: + return mariadbDialect; case DbType.postgresql: return postgresDialect; case DbType.dm: return dmDialect; + case DbType.oracle: + return oracleDialect; default: throw new Error('不支持的数据库'); } diff --git a/mayfly_go_web/src/views/ops/db/dialect/mariadb_dialect.ts b/mayfly_go_web/src/views/ops/db/dialect/mariadb_dialect.ts new file mode 100644 index 00000000..872ad3b5 --- /dev/null +++ b/mayfly_go_web/src/views/ops/db/dialect/mariadb_dialect.ts @@ -0,0 +1,18 @@ +import { DbDialect, DialectInfo } from './index'; +import { MysqlDialect } from '@/views/ops/db/dialect/mysql_dialect'; + +export { MariadbDialect }; + +let mariadbDialectInfo: DialectInfo; +class MariadbDialect extends MysqlDialect implements DbDialect { + getInfo(): DialectInfo { + if (mariadbDialectInfo) { + return mariadbDialectInfo; + } + + mariadbDialectInfo = {} as DialectInfo; + Object.assign(mariadbDialectInfo, super.getInfo()); + mariadbDialectInfo.icon = 'iconfont icon-mariadb'; + return mariadbDialectInfo; + } +} diff --git a/mayfly_go_web/src/views/ops/db/dialect/mysql_dialect.ts b/mayfly_go_web/src/views/ops/db/dialect/mysql_dialect.ts index d0667e56..adc2c8b0 100644 --- a/mayfly_go_web/src/views/ops/db/dialect/mysql_dialect.ts +++ b/mayfly_go_web/src/views/ops/db/dialect/mysql_dialect.ts @@ -328,4 +328,8 @@ class MysqlDialect implements DbDialect { } return DataType.String; } + // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars + wrapStrValue(columnType: string, value: string): string { + return `'${value}'`; + } } diff --git a/mayfly_go_web/src/views/ops/db/dialect/oracle_dialect.ts b/mayfly_go_web/src/views/ops/db/dialect/oracle_dialect.ts new file mode 100644 index 00000000..349e0bfb --- /dev/null +++ b/mayfly_go_web/src/views/ops/db/dialect/oracle_dialect.ts @@ -0,0 +1,402 @@ +import { DbInst } from '../db'; +import { + commonCustomKeywords, + DataType, + DbDialect, + DialectInfo, + EditorCompletion, + EditorCompletionItem, + IndexDefinition, + RowDefinition, + sqlColumnType, +} from './index'; +import { language as sqlLanguage } from 'monaco-editor/esm/vs/basic-languages/sql/sql.js'; + +export { OracleDialect, ORACLE_TYPE_LIST }; + +// 参考文档:https://eco.dameng.com/document/dm/zh-cn/sql-dev/dmpl-sql-datatype.html#%E5%AD%97%E7%AC%A6%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B +const ORACLE_TYPE_LIST: sqlColumnType[] = [ + // 字符数据类型 + { udtName: 'CHAR', dataType: 'CHAR', desc: '定长字符串,自动在末尾用空格补全,非unicode', space: '', range: '1 - 2000' }, + { udtName: 'NCHAR', dataType: 'NCHAR', desc: '定长字符串,自动在末尾用空格补全,unicode', space: '', range: '1 - 1000' }, + { udtName: 'VARCHAR2', dataType: 'VARCHAR2', desc: '变长字符串,不自动补全空格,非unicode', space: '', range: '1 - 4000' }, + { udtName: 'NVARCHAR2', dataType: 'NVARCHAR2', desc: '变长字符串,不自动补全空格,unicode', space: '', range: '1 - 2000' }, + + // 精确数值数据类型 NUMERIC、DECIMAL、DEC 类型、NUMBER 类型、INTEGER 类型、INT 类型、BIGINT 类型、TINYINT 类型、BYTE 类型、SMALLINT + { udtName: 'NUMBER', dataType: 'NUMBER', desc: 'NUMBER(p,s)', space: '1-38', range: '' }, + { udtName: 'INTEGER', dataType: 'INTEGER', desc: '同于number(38)', space: '', range: '' }, + { udtName: 'INT', dataType: 'INT', desc: '同INTEGER', space: '10', range: '' }, + { udtName: 'SMALLINT', dataType: 'SMALLINT', desc: '同于number(38)', space: '', range: '' }, + { udtName: 'DECIMAL', dataType: 'DECIMAL', desc: 'decimal(p,s) 默认number(38)', space: '', range: '' }, + { udtName: 'FLOAT', dataType: 'FLOAT', desc: 'float(b二进制进度),b的取值范围[1,126],默认126', space: '', range: '' }, + { udtName: 'REAL', dataType: 'REAL', desc: '同FLOAT(63)', space: '', range: '' }, + { udtName: 'BINARY_FLOAT', dataType: 'BINARY_FLOAT', desc: '32位单精度浮点数数据类型', space: '', range: '' }, + { udtName: 'BINARY_DOUBLE', dataType: 'BINARY_DOUBLE', desc: '64位双精度浮点数数据类型', space: '', range: '' }, + + // 一般日期时间数据类型 DATE TIME TIMESTAMP 默认精度 6 + // 多媒体数据类型 TEXT/LONG/LONGVARCHAR 类型:变长字符串类型 IMAGE/LONGVARBINARY 类型 BLOB CLOB BFILE 100G-1 + { udtName: 'DATE', dataType: 'DATE', desc: '世纪,年,月,日,时,分,秒', space: '', range: '' }, + { udtName: 'TIMESTAMP', dataType: 'TIMESTAMP', desc: '', space: '', range: '' }, + // { udtName: 'timestamp(precision) with time zone', dataType: 'TIMESTAMP', desc: '在timestamp(precison)的基础上加入了时区偏移量的值', space: '', range: '' }, + // { udtName: 'timestamp with local time zone', dataType: 'TIMESTAMP', desc: '存储时转化为数据库时区进行规范化存储,但不存储时区信息,客户端检索时,按客户端时区的时间数据返回给客户端', space: '', range: '' }, + // { udtName: 'interval year(precision) to month', dataType: 'interval year(precision) to month', desc: '可以用来表示几年几月的时间间隔', space: '', range: '' }, + // { udtName: 'nterval day(days_precision) to second(seconds_precision)', dataType: 'nterval day(days_precision) to second(seconds_precision)', desc: '可以用来存储天、小时、分和秒的时间间隔', space: '', range: '' }, + + { udtName: 'LONG', dataType: 'LONG', desc: '文本类型,不能作为主键或唯一约束', space: '', range: '最多达2GB' }, + { udtName: 'LONG RAW', dataType: 'LONG RAW', desc: '可变长二进制数据,不用进行字符集转换的数据', space: '', range: '最多达2GB' }, + { udtName: 'BLOB', dataType: 'BLOB', desc: '二进制大型对象', space: '', range: '最大长度4G' }, + { udtName: 'CLOB', dataType: 'CLOB', desc: '字符大型对象', space: '', range: '最大长度4G' }, + { udtName: 'NCLOB', dataType: 'NCLOB', desc: 'Unicode类型的数据', space: '', range: '最大长度4G' }, + { udtName: 'BFILE', dataType: 'BFILE', desc: '二进制文件', space: '', range: '' }, +]; + +const replaceFunctions: EditorCompletionItem[] = [ + // 字符函数 + { label: 'ASCII', insertText: 'ASCII(x)', description: '返回字符X的ASCII码' }, + { label: 'CONCAT', insertText: 'CONCAT(x,y)', description: '连接字符串X和Y' }, + { label: 'INSTR', insertText: 'INSTR(X,STR[,START][,N)', description: '从X中查找str,可以指定从start开始,也可以指定从n开始' }, + { label: 'LENGTH', insertText: 'LENGTH(x)', description: '返回X的长度' }, + { label: 'LOWER', insertText: 'LOWER(X)', description: 'X转换成小写' }, + { label: 'UPPER', insertText: 'UPPER(X)', description: 'X转换成大写' }, + { label: 'LTRIM', insertText: 'LTRIM(X[,TRIM_STR])', description: '把X的左边截去trim_str字符串,缺省截去空格' }, + { label: 'RTRIM', insertText: 'RTRIM(X[,TRIM_STR])', description: '把X的右边截去trim_str字符串,缺省截去空格' }, + { label: 'TRIM', insertText: 'TRIM(X[,TRIM_STR])', description: '把X的两边截去trim_str字符串,缺省截去空格' }, + { label: 'REPLACE', insertText: 'REPLACE(X,old,new)', description: '在X中查找old,并替换成new' }, + { label: 'SUBSTR', insertText: 'SUBSTR(X,start[,length])', description: '返回X的字串,从start处开始,截取length个字符,缺省length,默认到结尾' }, + // 数值函数 + { label: 'ABS', insertText: 'ABS(X)', description: 'X的绝对值' }, + { label: 'ACOS', insertText: 'ACOS(X)', description: 'X的反余弦' }, + { label: 'COS', insertText: 'COS(X)', description: '余弦' }, + { label: 'CEIL', insertText: 'CEIL(X)', description: '大于或等于X的最小值' }, + { label: 'FLOOR', insertText: 'FLOOR(X)', description: '小于或等于X的最大值' }, + { label: 'LOG', insertText: 'LOG(X,Y)', description: 'X为底Y的对数' }, + { label: 'MOD', insertText: 'MOD(X,Y)', description: 'X除以Y的余数' }, + { label: 'POWER', insertText: 'POWER(X,Y)', description: 'X的Y次幂' }, + { label: 'ROUND', insertText: 'ROUND(X [,Y]})', description: 'X在第Y位四舍五入' }, + { label: 'SQRT', insertText: 'SQRT(n)', description: '求数值 n 的平方根' }, + { label: 'TRUNC', insertText: 'TRUNC(n [,m])', description: "截取数值函数,str 内只能为数字和'-', '+', '.' 的组合" }, + //日期时间函数 + { label: 'ADD_MONTHS', insertText: 'ADD_MONTHS(date,n)', description: '在输入日期上加上指定的几个月返回一个新日期' }, + { label: 'LAST_DAY', insertText: 'LAST_DAY(date)', description: '返回输入日期所在月份最后一天的日期' }, + { label: 'EXTRACT', insertText: 'EXTRACT(fmt FROM d)', description: '提取日期中的特定部分' }, + { label: 'CURRENT_DATE', insertText: 'CURRENT_DATE', description: '获取当前日期' }, + { label: 'CURRENT_TIMESTAMP', insertText: 'TIMESTAMP', description: '获取当前时间' }, + // 转换函数 + { label: 'TO_CHAR', insertText: 'TO_CHAR(d|n[,fmt])', description: '把日期和数字转换为制定格式的字符串' }, + { label: 'TO_DATE', insertText: 'TO_DATE(X,[,fmt])', description: '把一个字符串以fmt格式转换成一个日期类型' }, + { label: 'TO_NUMBER', insertText: 'TO_NUMBER(X,[,fmt])', description: '把一个字符串以fmt格式转换为一个数字' }, + { label: 'TO_TIMESTAMP', insertText: 'TO_TIMESTAMP(X,[,fmt])', description: '把一个字符串以fmt格式转换为日期类型' }, + // 其他 + { label: 'NVL', insertText: 'NVL(X,VALUE)', description: '如果X为空,返回value,否则返回X' }, + { label: 'NVL2', insertText: 'NVL2(x,value1,value2)', description: '如果x非空,返回value1,否则返回value2' }, +]; + +const addCustomKeywords = ['ROWNUM', 'DUAL']; + +let oracleDialectInfo: DialectInfo; +class OracleDialect implements DbDialect { + getInfo(): DialectInfo { + if (oracleDialectInfo) { + return oracleDialectInfo; + } + + let { keywords, operators, builtinVariables } = sqlLanguage; + let functionNames = replaceFunctions.map((a) => a.label); + let excludeKeywords = new Set(functionNames.concat(operators)); + + let editorCompletions: EditorCompletion = { + keywords: keywords + .filter((a: string) => !excludeKeywords.has(a)) // 移除已存在的operator、function + .map((a: string): EditorCompletionItem => ({ label: a, description: 'keyword' })) + .concat( + // 加上自定义的关键字 + commonCustomKeywords.map( + (a): EditorCompletionItem => ({ + label: a, + description: 'keyword', + }) + ) + ) + .concat( + // 加上自定义的关键字 + addCustomKeywords.map( + (a): EditorCompletionItem => ({ + label: a, + description: 'keyword', + }) + ) + ), + operators: operators.map((a: string): EditorCompletionItem => ({ label: a, description: 'operator' })), + functions: replaceFunctions, + variables: builtinVariables.map((a: string): EditorCompletionItem => ({ label: a, description: 'var' })), + }; + + oracleDialectInfo = { + icon: 'iconfont icon-oracle', + defaultPort: 1521, + formatSqlDialect: 'plsql', + columnTypes: ORACLE_TYPE_LIST.sort((a, b) => a.udtName.localeCompare(b.udtName)), + editorCompletions, + }; + return oracleDialectInfo; + } + + getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number) { + return ` + SELECT * + FROM ( + SELECT t.*, ROWNUM AS rn + FROM "${table}" t + WHERE ROWNUM <=${pageNum * limit} ${condition ? ' and ' + condition : ''} + ${orderBy ? orderBy : ''} + ) + WHERE rn > ${(pageNum - 1) * limit} + `; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars + getPageSql(pageNum: number, limit: number) { + return ``; + } + + getDefaultRows(): RowDefinition[] { + return [ + { name: 'ID', type: 'NUMBER', length: '', numScale: '', value: '', notNull: true, pri: true, auto_increment: true, remark: '主键ID' }, + { name: 'CREATOR_ID', type: 'NUMBER', length: '', numScale: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '创建人id' }, + { + name: 'CREATOR', + type: 'VARCHAR2', + length: '100', + numScale: '', + value: '', + notNull: true, + pri: false, + auto_increment: false, + remark: '创建人姓名', + }, + { + name: 'CREATE_TIME', + type: 'DATE', + length: '', + numScale: '', + value: 'CURRENT_TIMESTAMP', + notNull: true, + pri: false, + auto_increment: false, + remark: '创建时间', + }, + { name: 'UPDATOR_ID', type: 'NUMBER', length: '', numScale: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '修改人id' }, + { + name: 'UPDATOR', + type: 'VARCHAR2', + length: '100', + numScale: '', + value: '', + notNull: true, + pri: false, + auto_increment: false, + remark: '修改人姓名', + }, + { + name: 'UPDATE_TIME', + type: 'DATE', + length: '', + numScale: '', + value: 'CURRENT_TIMESTAMP', + notNull: true, + pri: false, + auto_increment: false, + remark: '修改时间', + }, + ]; + } + + getDefaultIndex(): IndexDefinition { + return { + indexName: '', + columnNames: [], + unique: false, + indexType: 'NORMAL', + indexComment: '', + }; + } + + quoteIdentifier = (name: string) => { + return `"${name}"`; + }; + + matchType(text: string, arr: string[]): boolean { + if (!text || !arr || arr.length === 0) { + return false; + } + for (let i = 0; i < arr.length; i++) { + if (text.indexOf(arr[i]) > -1) { + return true; + } + } + return false; + } + + getDefaultValueSql(cl: any): string { + if (cl.value && cl.value.length > 0) { + // 哪些字段默认值需要加引号 + let marks = false; + if (this.matchType(cl.type, ['CHAR', 'TIME', 'DATE', 'LONG', 'CLOB', 'BLOB', 'BFILE'])) { + // 默认值是时间日期函数的必须要加引号 + let val = cl.value.toUpperCase().replace(' ', ''); + if (this.matchType(cl.type, ['DATE', 'TIMESTAMP']) && ['CURRENT_DATE', 'CURRENT_TIMESTAMP'].includes(val)) { + marks = false; + } else { + marks = true; + } + } + return ` DEFAULT ${marks ? "'" : ''}${cl.value}${marks ? "'" : ''}`; + } + return ''; + } + + getTypeLengthSql(cl: any) { + // 哪些字段可以指定长度 VARCHAR/VARCHAR2/CHAR/BIT/NUMBER/NUMERIC/TIME、TIMESTAMP(可以指定小数秒精度) + if (cl.length && this.matchType(cl.type, ['CHAR', 'BIT', 'TIME', 'NUM', 'DEC'])) { + // 哪些字段类型可以指定小数点 + if (cl.numScale && this.matchType(cl.type, ['NUM', 'DEC'])) { + return `(${cl.length}, ${cl.numScale})`; + } else { + return `(${cl.length})`; + } + } + return ''; + } + + genColumnBasicSql(cl: RowDefinition): string { + let length = this.getTypeLengthSql(cl); + // 默认值 + let defVal = this.getDefaultValueSql(cl); + let incr = cl.auto_increment ? 'generated by default as IDENTITY' : ''; + let pri = cl.pri ? 'PRIMARY KEY' : ''; + return ` ${cl.name.toUpperCase()} ${cl.type}${length} ${incr} ${pri} ${defVal} ${cl.notNull ? 'NOT NULL' : ''} `; + } + + getCreateTableSql(data: any): string { + let createSql = ''; + let tableCommentSql = ''; + let columCommentSql = ''; + + // 创建表结构 + let fields: string[] = []; + data.fields.res.forEach((item: any) => { + item.name && fields.push(this.genColumnBasicSql(item)); + // 列注释 + if (item.remark) { + columCommentSql += ` comment on column ${data.tableName?.toUpperCase()}.${item.name?.toUpperCase()} is '${item.remark}'; `; + } + }); + // 建表 + createSql = `CREATE TABLE ${data.tableName?.toUpperCase()} ( ${fields.join(',')} );`; + // 表注释 + if (data.tableComment) { + tableCommentSql = ` comment on table ${data.tableName?.toUpperCase()} is '${data.tableComment}'; `; + } + + return createSql + tableCommentSql + columCommentSql; + } + + getCreateIndexSql(tableData: any): string { + // CREATE UNIQUE INDEX idx_column_name ON your_table (column1, column2); + // COMMENT ON INDEX idx_column_name IS 'Your index comment here'; + // 创建索引 + let sql: string[] = []; + tableData.indexs.res.forEach((a: any) => { + sql.push(` CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${a.indexName} ON "${tableData.tableName}" ("${a.columnNames.join('","')})"`); + }); + return sql.join(';'); + } + + getModifyColumnSql(tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string { + let sql: string[] = []; + if (changeData.add.length > 0) { + changeData.add.forEach((a) => { + sql.push(`ALTER TABLE "${tableName}" add COLUMN ${this.genColumnBasicSql(a)}`); + if (a.remark) { + sql.push(`comment on COLUMN "${tableName}"."${a.name}" is '${a.remark}'`); + } + }); + } + + if (changeData.upd.length > 0) { + changeData.upd.forEach((a) => { + sql.push(`ALTER TABLE "${tableName}" MODIFY ${this.genColumnBasicSql(a)}`); + if (a.remark) { + sql.push(`comment on COLUMN "${tableName}"."${a.name}" is '${a.remark}'`); + } + }); + } + + if (changeData.del.length > 0) { + changeData.del.forEach((a) => { + sql.push(`ALTER TABLE "${tableName}" DROP COLUMN ${a.name}`); + }); + } + return sql.join(';'); + } + + getModifyIndexSql(tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string { + // 不能直接修改索引名或字段、需要先删后加 + let dropIndexNames: string[] = []; + let addIndexs: any[] = []; + + if (changeData.upd.length > 0) { + changeData.upd.forEach((a) => { + dropIndexNames.push(a.indexName); + addIndexs.push(a); + }); + } + + if (changeData.del.length > 0) { + changeData.del.forEach((a) => { + dropIndexNames.push(a.indexName); + }); + } + + if (changeData.add.length > 0) { + changeData.add.forEach((a) => { + addIndexs.push(a); + }); + } + + if (dropIndexNames.length > 0 || addIndexs.length > 0) { + let sql: string[] = []; + if (dropIndexNames.length > 0) { + dropIndexNames.forEach((a) => { + sql.push(`DROP INDEX ${a}`); + }); + } + + if (addIndexs.length > 0) { + addIndexs.forEach((a) => { + sql.push(`CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${a.indexName} ON "${tableName}" (${a.columnNames.join(',')})`); + }); + } + return sql.join(';'); + } + return ''; + } + + getDataType(columnType: string): DataType { + if (DbInst.isNumber(columnType)) { + return DataType.Number; + } + // 日期时间类型 oracle只有date和timestamp类型 + if (/timestamp|date/gi.test(columnType)) { + return DataType.DateTime; + } + return DataType.String; + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars + wrapStrValue(columnType: string, value: string): string { + if (value && this.getDataType(columnType) === DataType.DateTime) { + return `to_timestamp('${value}', 'yyyy-mm-dd hh24:mi:ss')`; + } + return `'${value}'`; + } +} diff --git a/mayfly_go_web/src/views/ops/db/dialect/postgres_dialect.ts b/mayfly_go_web/src/views/ops/db/dialect/postgres_dialect.ts index b34433a4..e5c55fcd 100644 --- a/mayfly_go_web/src/views/ops/db/dialect/postgres_dialect.ts +++ b/mayfly_go_web/src/views/ops/db/dialect/postgres_dialect.ts @@ -407,4 +407,9 @@ class PostgresqlDialect implements DbDialect { } return DataType.String; } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars + wrapStrValue(value: string, type: string): string { + return `'${value}'`; + } } diff --git a/server/go.mod b/server/go.mod index 6333b7cf..af7eccaa 100644 --- a/server/go.mod +++ b/server/go.mod @@ -26,6 +26,7 @@ require ( github.com/pquerna/otp v1.4.0 github.com/redis/go-redis/v9 v9.4.0 github.com/robfig/cron/v3 v3.0.1 // 定时任务 + github.com/sijms/go-ora/v2 v2.8.5 github.com/stretchr/testify v1.8.4 go.mongodb.org/mongo-driver v1.13.1 // mongo golang.org/x/crypto v0.18.0 // ssh diff --git a/server/internal/db/api/db.go b/server/internal/db/api/db.go index b783fa97..0811cb07 100644 --- a/server/internal/db/api/db.go +++ b/server/internal/db/api/db.go @@ -349,7 +349,11 @@ func (d *Db) dumpDb(writer *gzipWriter, dbId uint64, dbName string, tables []str continue } writer.WriteString(fmt.Sprintf("\n-- ----------------------------\n-- 表记录: %s \n-- ----------------------------\n", table)) - writer.WriteString("BEGIN;\n") + + // 达梦不支持begin语句 + if dbConn.Info.Type != dbi.DbTypeDM { + writer.WriteString("BEGIN;\n") + } insertSql := "INSERT INTO %s VALUES (%s);\n" dbMeta.WalkTableRecord(table, func(record map[string]any, columns []*dbi.QueryColumn) error { var values []string diff --git a/server/internal/db/api/form/instance.go b/server/internal/db/api/form/instance.go index 65fe1968..c15a070e 100644 --- a/server/internal/db/api/form/instance.go +++ b/server/internal/db/api/form/instance.go @@ -6,6 +6,7 @@ type InstanceForm struct { Type string `binding:"required" json:"type"` // 类型,mysql oracle等 Host string `binding:"required" json:"host"` Port int `binding:"required" json:"port"` + Sid string `json:"sid"` Username string `binding:"required" json:"username"` Password string `json:"password"` Params string `json:"params"` diff --git a/server/internal/db/api/vo/instance.go b/server/internal/db/api/vo/instance.go index 0d2ab171..b145e074 100644 --- a/server/internal/db/api/vo/instance.go +++ b/server/internal/db/api/vo/instance.go @@ -9,6 +9,7 @@ type InstanceListVO struct { Port *int `json:"port"` Type *string `json:"type"` Params *string `json:"params"` + Sid *string `json:"sid"` Username *string `json:"username"` Remark *string `json:"remark"` CreateTime *time.Time `json:"createTime"` diff --git a/server/internal/db/application/db.go b/server/internal/db/application/db.go index 373c6807..d6e5c116 100644 --- a/server/internal/db/application/db.go +++ b/server/internal/db/application/db.go @@ -155,7 +155,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) { + if dbi.DbTypePostgres.Equal(instance.Type) || dbi.DbTypeDM.Equal(instance.Type) || dbi.DbTypeOracle.Equal(instance.Type) { ss := strings.Split(dbName, "/") if len(ss) > 1 { checkDb = ss[0] @@ -195,6 +195,7 @@ func (d *dbAppImpl) GetDbConnByInstanceId(instanceId uint64) (*dbi.DbConn, error func toDbInfo(instance *entity.DbInstance, dbId uint64, database string, tagPath ...string) *dbi.DbInfo { di := new(dbi.DbInfo) di.InstanceId = instance.Id + di.Sid = instance.Sid di.Id = dbId di.Database = database di.TagPath = tagPath diff --git a/server/internal/db/dbm/dbi/conn.go b/server/internal/db/dbm/dbi/conn.go index 63be1dfa..5ae1115b 100644 --- a/server/internal/db/dbm/dbi/conn.go +++ b/server/internal/db/dbm/dbi/conn.go @@ -206,6 +206,9 @@ func valueConvert(data []byte, colType *sql.ColumnType) any { if stringV == "" { return "" } + if colType == nil || colType.ScanType() == nil { + return stringV + } colScanType := strings.ToLower(colType.ScanType().Name()) if strings.Contains(colScanType, "int") { diff --git a/server/internal/db/dbm/dbi/db_type.go b/server/internal/db/dbm/dbi/db_type.go index 0258f68e..404bbf39 100644 --- a/server/internal/db/dbm/dbi/db_type.go +++ b/server/internal/db/dbm/dbi/db_type.go @@ -15,6 +15,7 @@ const ( DbTypeMariadb DbType = "mariadb" DbTypePostgres DbType = "postgres" DbTypeDM DbType = "dm" + DbTypeOracle DbType = "oracle" ) func ToDbType(dbType string) DbType { diff --git a/server/internal/db/dbm/dbi/info.go b/server/internal/db/dbm/dbi/info.go index ae1a780e..0c89a707 100644 --- a/server/internal/db/dbm/dbi/info.go +++ b/server/internal/db/dbm/dbi/info.go @@ -16,6 +16,7 @@ type DbInfo struct { Type DbType // 类型,mysql postgres等 Host string Port int + Sid string // oracle数据库需要指定sid Network string Username string Password string diff --git a/server/internal/db/dbm/dbi/metasql/dm_meta.sql b/server/internal/db/dbm/dbi/metasql/dm_meta.sql index 934a34fc..b31fe988 100644 --- a/server/internal/db/dbm/dbi/metasql/dm_meta.sql +++ b/server/internal/db/dbm/dbi/metasql/dm_meta.sql @@ -2,6 +2,7 @@ select distinct owner as SCHEMA_NAME from all_objects +order by owner --------------------------------------- --DM_TABLE_INFO 表详细信息 SELECT a.object_name as TABLE_NAME, @@ -29,6 +30,7 @@ FROM all_objects a WHERE a.owner = (SELECT SF_GET_SCHEMA_NAME_BY_ID(CURRENT_SCHID)) AND a.object_type = 'TABLE' AND a.status = 'VALID' +ORDER BY a.object_name --------------------------------------- --DM_INDEX_INFO 表索引信息 select diff --git a/server/internal/db/dbm/dbi/metasql/mysql_meta.sql b/server/internal/db/dbm/dbi/metasql/mysql_meta.sql index 6f40694a..66823dc1 100644 --- a/server/internal/db/dbm/dbi/metasql/mysql_meta.sql +++ b/server/internal/db/dbm/dbi/metasql/mysql_meta.sql @@ -5,6 +5,7 @@ FROM information_schema.SCHEMATA WHERE SCHEMA_NAME NOT IN ('mysql', 'information_schema', 'performance_schema') +ORDER BY SCHEMA_NAME --------------------------------------- --MYSQL_TABLE_INFO 表详细信息 SELECT @@ -22,6 +23,7 @@ WHERE SELECT database () ) +ORDER BY table_name --------------------------------------- --MYSQL_INDEX_INFO 索引信息 SELECT diff --git a/server/internal/db/dbm/dbi/metasql/oracle_meta.sql b/server/internal/db/dbm/dbi/metasql/oracle_meta.sql new file mode 100644 index 00000000..3fb477f5 --- /dev/null +++ b/server/internal/db/dbm/dbi/metasql/oracle_meta.sql @@ -0,0 +1,70 @@ +--ORACLE_DB_SCHEMAS 库schemas +select distinct owner as SCHEMA_NAME +from all_objects +order by owner +--------------------------------------- +--ORACLE_TABLE_INFO 表详细信息 +select a.TABLE_NAME, + b.COMMENTS as TABLE_COMMENT, + c.CREATED as CREATE_TIME, + d.BYTES as DATA_LENGTH, + 0 as INDEX_LENGTH, + a.NUM_ROWS as TABLE_ROWS +from all_tables a + left join ALL_TAB_COMMENTS b on b.TABLE_NAME = a.TABLE_NAME AND b.OWNER = a.OWNER + left join all_objects c on c.OBJECT_TYPE = 'TABLE' AND c.OWNER = a.OWNER AND c.OBJECT_NAME = a.TABLE_NAME + left join dba_segments d on d.SEGMENT_TYPE = 'TABLE' AND d.OWNER = a.OWNER AND d.SEGMENT_NAME = a.TABLE_NAME +where a.owner = (SELECT sys_context('USERENV', 'CURRENT_SCHEMA') FROM dual) +ORDER BY a.TABLE_NAME +--------------------------------------- +--ORACLE_INDEX_INFO 表索引信息 +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, + (SELECT LISTAGG(column_name, ', ') WITHIN GROUP (ORDER BY column_position) + FROM ALL_IND_COLUMNS aic + WHERE aic.INDEX_NAME = ai.INDEX_NAME + AND aic.TABLE_NAME = ai.TABLE_NAME) AS COLUMN_NAME, + 1 AS SEQ_IN_INDEX, + (SELECT comments + FROM ALL_IND_COLUMNS aic + LEFT JOIN ALL_COL_COMMENTS acc ON aic.column_name = acc.column_name + WHERE aic.INDEX_OWNER = ai.OWNER + AND aic.INDEX_NAME = ai.INDEX_NAME + AND aic.TABLE_NAME = ai.TABLE_NAME + AND ROWNUM = 1) AS INDEX_COMMENT +FROM ALL_INDEXES ai +WHERE ai.OWNER = (SELECT sys_context('USERENV', 'CURRENT_SCHEMA') FROM DUAL) + AND ai.table_name = '%s' +--------------------------------------- +--ORACLE_COLUMN_MA 表列信息 +SELECT a.TABLE_NAME as TABLE_NAME, + a.COLUMN_NAME as COLUMN_NAME, + case + when a.NULLABLE = 'Y' then 'YES' + when a.NULLABLE = 'N' then 'NO' + else 'NO' end as NULLABLE, + case + when a.DATA_PRECISION > 0 then a.DATA_TYPE + else (a.DATA_TYPE || '(' || a.DATA_LENGTH || ')') end as COLUMN_TYPE, + 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 +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 + LEFT JOIN (select ac.TABLE_NAME, ac.OWNER, cc.COLUMN_NAME, 1 as pri + from all_constraints ac + join all_cons_columns cc on cc.CONSTRAINT_NAME = ac.CONSTRAINT_NAME AND cc.OWNER = ac.OWNER + where cc.CONSTRAINT_NAME IS NOT NULL + AND ac.CONSTRAINT_TYPE = 'P') d + on d.OWNER = a.OWNER AND d.TABLE_NAME = a.TABLE_NAME AND d.COLUMN_NAME = a.COLUMN_NAME +WHERE a.OWNER = (SELECT sys_context('USERENV', 'CURRENT_SCHEMA') FROM DUAL) + AND a.TABLE_NAME in (%s) +order by a.COLUMN_ID diff --git a/server/internal/db/dbm/dbi/metasql/pgsql_meta.sql b/server/internal/db/dbm/dbi/metasql/pgsql_meta.sql index 4371aa96..890719e2 100644 --- a/server/internal/db/dbm/dbi/metasql/pgsql_meta.sql +++ b/server/internal/db/dbm/dbi/metasql/pgsql_meta.sql @@ -9,6 +9,8 @@ where and n.nspname not like 'dbms_%' and n.nspname not like 'utl_%' and n.nspname != 'information_schema' +order by + n.nspname --------------------------------------- --PGSQL_TABLE_INFO 表详细信息 select @@ -27,6 +29,7 @@ where has_table_privilege(CAST(c.oid AS regclass), 'SELECT') and n.nspname = current_schema() and c.reltype > 0 +order by c.relname --------------------------------------- --PGSQL_INDEX_INFO 表索引信息 SELECT diff --git a/server/internal/db/dbm/dbm.go b/server/internal/db/dbm/dbm.go index b1fec301..83a0c59e 100644 --- a/server/internal/db/dbm/dbm.go +++ b/server/internal/db/dbm/dbm.go @@ -6,6 +6,7 @@ import ( "mayfly-go/internal/db/dbm/dbi" "mayfly-go/internal/db/dbm/dm" "mayfly-go/internal/db/dbm/mysql" + "mayfly-go/internal/db/dbm/oracle" "mayfly-go/internal/db/dbm/postgres" "mayfly-go/internal/machine/mcm" "mayfly-go/pkg/cache" @@ -45,6 +46,8 @@ func getDbMetaByType(dt dbi.DbType) dbi.Meta { return postgres.GetMeta() case dbi.DbTypeDM: return dm.GetMeta() + case dbi.DbTypeOracle: + return oracle.GetMeta() default: panic(fmt.Sprintf("invalid database type: %s", dt)) } diff --git a/server/internal/db/dbm/oracle/dialect.go b/server/internal/db/dbm/oracle/dialect.go new file mode 100644 index 00000000..ae2683a1 --- /dev/null +++ b/server/internal/db/dbm/oracle/dialect.go @@ -0,0 +1,344 @@ +package oracle + +import ( + "context" + "database/sql" + "fmt" + "mayfly-go/internal/db/dbm/dbi" + "mayfly-go/pkg/errorx" + "mayfly-go/pkg/utils/anyx" + "reflect" + "regexp" + "strings" + "time" + + _ "gitee.com/chunanyong/dm" +) + +// ---------------------------------- DM元数据 ----------------------------------- +const ( + ORACLE_META_FILE = "metasql/oracle_meta.sql" + ORACLE_DB_SCHEMAS = "ORACLE_DB_SCHEMAS" + ORACLE_TABLE_INFO_KEY = "ORACLE_TABLE_INFO" + ORACLE_INDEX_INFO_KEY = "ORACLE_INDEX_INFO" + ORACLE_COLUMN_MA_KEY = "ORACLE_COLUMN_MA" +) + +type OracleDialect struct { + dc *dbi.DbConn +} + +func (od *OracleDialect) GetDbServer() (*dbi.DbServer, error) { + _, res, err := od.dc.Query("select * from v$instance") + if err != nil { + return nil, err + } + ds := &dbi.DbServer{ + Version: anyx.ConvString(res[0]["VERSION"]), + } + return ds, nil +} + +func (od *OracleDialect) GetDbNames() ([]string, error) { + _, res, err := od.dc.Query("SELECT name AS DBNAME FROM v$database") + if err != nil { + return nil, err + } + + databases := make([]string, 0) + for _, re := range res { + databases = append(databases, anyx.ConvString(re["DBNAME"])) + } + + return databases, nil +} + +// 获取表基础元信息, 如表名等 +func (od *OracleDialect) GetTables() ([]dbi.Table, error) { + + // 首先执行更新统计信息sql 这个统计信息在数据量比较大的时候就比较耗时,所以最好定时执行 + // _, _, err := pd.dc.Query("dbms_stats.GATHER_SCHEMA_stats(SELECT SF_GET_SCHEMA_NAME_BY_ID(CURRENT_SCHID))") + + // 查询表信息 + _, res, err := od.dc.Query(dbi.GetLocalSql(ORACLE_META_FILE, ORACLE_TABLE_INFO_KEY)) + if err != nil { + return nil, err + } + + tables := make([]dbi.Table, 0) + for _, re := range res { + tables = append(tables, dbi.Table{ + TableName: re["TABLE_NAME"].(string), + TableComment: anyx.ConvString(re["TABLE_COMMENT"]), + CreateTime: anyx.ConvString(re["CREATE_TIME"]), + TableRows: anyx.ConvInt(re["TABLE_ROWS"]), + DataLength: anyx.ConvInt64(re["DATA_LENGTH"]), + IndexLength: anyx.ConvInt64(re["INDEX_LENGTH"]), + }) + } + return tables, nil +} + +// 获取列元信息, 如列名等 +func (od *OracleDialect) GetColumns(tableNames ...string) ([]dbi.Column, error) { + tableName := "" + for i := 0; i < len(tableNames); i++ { + if i != 0 { + tableName = tableName + ", " + } + tableName = tableName + "'" + tableNames[i] + "'" + } + + _, res, err := od.dc.Query(fmt.Sprintf(dbi.GetLocalSql(ORACLE_META_FILE, ORACLE_COLUMN_MA_KEY), tableName)) + if err != nil { + return nil, err + } + + columns := make([]dbi.Column, 0) + for _, re := range res { + defaultVal := anyx.ConvString(re["COLUMN_DEFAULT"]) + // 如果默认值包含.nextval,说明是序列,默认值为null + if strings.Contains(defaultVal, ".nextval") { + defaultVal = "" + } + columns = append(columns, dbi.Column{ + TableName: re["TABLE_NAME"].(string), + ColumnName: re["COLUMN_NAME"].(string), + ColumnType: anyx.ConvString(re["COLUMN_TYPE"]), + ColumnComment: anyx.ConvString(re["COLUMN_COMMENT"]), + Nullable: anyx.ConvString(re["NULLABLE"]), + ColumnKey: anyx.ConvString(re["COLUMN_KEY"]), + ColumnDefault: defaultVal, + NumScale: anyx.ConvString(re["NUM_SCALE"]), + }) + } + return columns, nil +} + +func (od *OracleDialect) GetPrimaryKey(tablename string) (string, error) { + columns, err := od.GetColumns(tablename) + if err != nil { + return "", err + } + if len(columns) == 0 { + return "", errorx.NewBiz("[%s] 表不存在", tablename) + } + for _, v := range columns { + if v.ColumnKey == "PRI" { + return v.ColumnName, nil + } + } + + return columns[0].ColumnName, nil +} + +// 获取表索引信息 +func (od *OracleDialect) GetTableIndex(tableName string) ([]dbi.Index, error) { + _, res, err := od.dc.Query(fmt.Sprintf(dbi.GetLocalSql(ORACLE_META_FILE, ORACLE_INDEX_INFO_KEY), tableName)) + if err != nil { + return nil, err + } + + indexs := make([]dbi.Index, 0) + for _, re := range res { + indexs = append(indexs, dbi.Index{ + IndexName: re["INDEX_NAME"].(string), + ColumnName: anyx.ConvString(re["COLUMN_NAME"]), + IndexType: anyx.ConvString(re["INDEX_TYPE"]), + IndexComment: anyx.ConvString(re["INDEX_COMMENT"]), + NonUnique: anyx.ConvInt(re["NON_UNIQUE"]), + SeqInIndex: anyx.ConvInt(re["SEQ_IN_INDEX"]), + }) + } + // 把查询结果以索引名分组,索引字段以逗号连接 + result := make([]dbi.Index, 0) + key := "" + for _, v := range indexs { + // 当前的索引名 + in := v.IndexName + 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 +} + +// 获取建表ddl +func (od *OracleDialect) GetTableDDL(tableName string) (string, error) { + ddlSql := fmt.Sprintf("SELECT DBMS_METADATA.GET_DDL('TABLE', '%s', (SELECT sys_context('USERENV', 'CURRENT_SCHEMA') FROM dual)) AS TABLE_DDL FROM DUAL", tableName) + _, res, err := od.dc.Query(ddlSql) + if err != nil { + return "", err + } + // 建表ddl + var builder strings.Builder + for _, re := range res { + builder.WriteString(re["TABLE_DDL"].(string)) + } + + // 表注释 + _, res, err = od.dc.Query(fmt.Sprintf(` + select OWNER, COMMENTS from ALL_TAB_COMMENTS where TABLE_TYPE='TABLE' and TABLE_NAME = '%s' + and owner = (SELECT sys_context('USERENV', 'CURRENT_SCHEMA') FROM dual) `, tableName)) + if err != nil { + return "", err + } + for _, re := range res { + // COMMENT ON TABLE "SYS_MENU" IS '菜单表'; + if re["COMMENTS"] != nil { + tableComment := fmt.Sprintf("\n\nCOMMENT ON TABLE \"%s\".\"%s\" IS '%s';", re["OWNER"].(string), tableName, re["COMMENTS"].(string)) + builder.WriteString(tableComment) + } + } + + // 字段注释 + fieldSql := fmt.Sprintf(` + SELECT OWNER, COLUMN_NAME, COMMENTS + FROM ALL_COL_COMMENTS + WHERE OWNER = (SELECT sys_context('USERENV', 'CURRENT_SCHEMA') FROM dual) + AND TABLE_NAME = '%s' + `, tableName) + _, res, err = od.dc.Query(fieldSql) + if err != nil { + return "", err + } + + builder.WriteString("\n") + for _, re := range res { + // COMMENT ON COLUMN "SYS_MENU"."BIZ_CODE" IS '业务编码,应用编码1'; + if re["COMMENTS"] != nil { + fieldComment := fmt.Sprintf("\nCOMMENT ON COLUMN \"%s\".\"%s\".\"%s\" IS '%s';", re["OWNER"].(string), tableName, re["COLUMN_NAME"].(string), re["COMMENTS"].(string)) + builder.WriteString(fieldComment) + } + } + + // 索引信息 + indexSql := fmt.Sprintf(` + select DBMS_METADATA.GET_DDL('INDEX', a.INDEX_NAME, a.OWNER) AS INDEX_DEF from ALL_INDEXES a + join ALL_objects b on a.owner = b.owner and b.object_name = a.index_name and b.object_type = 'INDEX' + where a.owner = (SELECT sys_context('USERENV', 'CURRENT_SCHEMA') FROM dual) + and a.table_name = '%s' + `, tableName) + _, res, err = od.dc.Query(indexSql) + if err != nil { + return "", err + } + for _, re := range res { + builder.WriteString("\n\n" + re["INDEX_DEF"].(string)) + } + + return builder.String(), nil +} + +func (od *OracleDialect) WalkTableRecord(tableName string, walkFn dbi.WalkQueryRowsFunc) error { + return od.dc.WalkQueryRows(context.Background(), fmt.Sprintf("SELECT * FROM %s", tableName), walkFn) +} + +// 获取DM当前连接的库可访问的schemaNames +func (od *OracleDialect) GetSchemas() ([]string, error) { + sql := dbi.GetLocalSql(ORACLE_META_FILE, ORACLE_DB_SCHEMAS) + _, res, err := od.dc.Query(sql) + if err != nil { + return nil, err + } + schemaNames := make([]string, 0) + for _, re := range res { + schemaNames = append(schemaNames, anyx.ConvString(re["SCHEMA_NAME"])) + } + return schemaNames, nil +} + +// GetDbProgram 获取数据库程序模块,用于数据库备份与恢复 +func (od *OracleDialect) GetDbProgram() dbi.DbProgram { + panic("implement me") +} + +func (od *OracleDialect) GetDataType(dbColumnType string) dbi.DataType { + if regexp.MustCompile(`(?i)int|double|float|number|decimal|byte|bit`).MatchString(dbColumnType) { + return dbi.DataTypeNumber + } + // 日期时间类型 + if regexp.MustCompile(`(?i)datetime|timestamp`).MatchString(dbColumnType) { + return dbi.DataTypeDateTime + } + // 日期类型 + if regexp.MustCompile(`(?i)date`).MatchString(dbColumnType) { + return dbi.DataTypeDate + } + // 时间类型 + if regexp.MustCompile(`(?i)time`).MatchString(dbColumnType) { + return dbi.DataTypeTime + } + return dbi.DataTypeString +} + +func (od *OracleDialect) BatchInsert(tx *sql.Tx, tableName string, columns []string, values [][]any) (int64, error) { + //INSERT ALL + //INTO my_table(field_1,field_2) VALUES (value_1,value_2) + //INTO my_table(field_1,field_2) VALUES (value_3,value_4) + //INTO my_table(field_1,field_2) VALUES (value_5,value_6) + //SELECT 1 FROM DUAL; + + if len(values) <= 0 { + return 0, nil + } + + // 把二维数组转为一维数组 + var args []any + for _, v := range values { + args = append(args, v...) + } + + // 拼接oracle批量插入语句 + sqlArr := make([]string, 0) + sqlArr = append(sqlArr, "INSERT ALL") + + // 拼接带占位符的sql oracle的占位符是:1,:2,:3.... + for i := 0; i < len(args); i += len(columns) { + var placeholder []string + for j := 0; j < len(columns); j++ { + // 判断字符串数据格式是时间"2023-06-25 10:40:10" 占位符需要变成 to_date(:x, 'fmt') + if reflect.TypeOf(args[i+j]) == reflect.TypeOf("") { + if regexp.MustCompile(`^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$`).MatchString(args[i+j].(string)) { + placeholder = append(placeholder, fmt.Sprintf("to_date(:%d, 'yyyy-mm-dd hh24:mi:ss')", i+j+1)) + } else if regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`).MatchString(args[i+j].(string)) { + // 只有年月日的数据,oracle会自动补零时分秒,如:2024-01-02: to_date('2024-01-02','yyyy-mm-dd') 输出:2024-01-02 00:00:00 + placeholder = append(placeholder, fmt.Sprintf("to_date(:%d, 'yyyy-mm-dd')", i+j+1)) + } else if regexp.MustCompile(`^\d{2}:\d{2}:\d{2}$`).MatchString(args[i+j].(string)) { + // 只有时间的数据,oracle会拼接当前月份的年月日,如当前月份是2024-01: to_date('13:23:11','hh24:mi:ss') 输出:2024-01-01 13:23:11 + placeholder = append(placeholder, fmt.Sprintf("to_date(:%d, 'hh24:mi:ss')", i+j+1)) + } + continue + } + + placeholder = append(placeholder, fmt.Sprintf(":%d", i+j+1)) + } + sqlArr = append(sqlArr, fmt.Sprintf("INTO %s (%s) VALUES (%s)", od.dc.Info.Type.QuoteIdentifier(tableName), strings.Join(columns, ","), strings.Join(placeholder, ","))) + } + sqlArr = append(sqlArr, "SELECT 1 FROM DUAL") + + // 执行批量insert sql + res, err := od.dc.TxExec(tx, strings.Join(sqlArr, " "), args...) + return res, err +} + +func (od *OracleDialect) FormatStrData(dbColumnValue string, dataType dbi.DataType) string { + switch dataType { + case dbi.DataTypeDateTime: // "2024-01-02T22:08:22.275697+08:00" + res, _ := time.Parse(time.RFC3339, dbColumnValue) + return res.Format(time.DateTime) + case dbi.DataTypeDate: // "2024-01-02T00:00:00+08:00" + res, _ := time.Parse(time.RFC3339, dbColumnValue) + return res.Format(time.DateOnly) + case dbi.DataTypeTime: // "0000-01-01T22:08:22.275688+08:00" + res, _ := time.Parse(time.RFC3339, dbColumnValue) + return res.Format(time.TimeOnly) + } + return dbColumnValue +} diff --git a/server/internal/db/dbm/oracle/meta.go b/server/internal/db/dbm/oracle/meta.go new file mode 100644 index 00000000..9b0ef943 --- /dev/null +++ b/server/internal/db/dbm/oracle/meta.go @@ -0,0 +1,67 @@ +package oracle + +import ( + "database/sql" + "fmt" + go_ora "github.com/sijms/go-ora/v2" + "mayfly-go/internal/db/dbm/dbi" + "strings" + "sync" +) + +var ( + meta dbi.Meta + once sync.Once +) + +func GetMeta() dbi.Meta { + once.Do(func() { + meta = new(OraMeta) + }) + return meta +} + +type OraMeta struct { +} + +func (md *OraMeta) GetSqlDb(d *dbi.DbInfo) (*sql.DB, error) { + driverName := "oracle" + + err := d.IfUseSshTunnelChangeIpPort() + if err != nil { + return nil, err + } + // 参数参考 https://github.com/sijms/go-ora?tab=readme-ov-file#other-connection-options + urlOptions := make(map[string]string) + + db := d.Database + schema := "" + if db != "" { + // oracle database可以使用db/schema表示,方便连接指定schema, 若不存在schema则使用默认schema + ss := strings.Split(db, "/") + if len(ss) > 1 { + // user=hr&defaultSchema=hr + schema = ss[1] + } + } + + urlOptions["TIMEOUT"] = "60" + urlOptions["client charset"] = "UTF8" + connStr := go_ora.BuildUrl(d.Host, d.Port, d.Sid, d.Username, d.Password, urlOptions) + conn, err := sql.Open(driverName, connStr) + if err != nil { + return nil, err + } + // 目前没找到如何连接的时候就获取schema的方法,只能连接后再设置 + if schema != "" { + _, err := conn.Exec(fmt.Sprintf("ALTER SESSION SET CURRENT_SCHEMA=%s", schema)) + if err != nil { + return nil, err + } + } + return conn, err +} + +func (md *OraMeta) GetDialect(conn *dbi.DbConn) dbi.Dialect { + return &OracleDialect{conn} +} diff --git a/server/internal/db/domain/entity/db_instance.go b/server/internal/db/domain/entity/db_instance.go index 212da345..c82b0fb5 100644 --- a/server/internal/db/domain/entity/db_instance.go +++ b/server/internal/db/domain/entity/db_instance.go @@ -15,6 +15,7 @@ type DbInstance struct { Host string `json:"host"` Port int `json:"port"` Network string `json:"network"` + Sid string `json:"sid"` Username string `json:"username"` Password string `json:"-"` Params string `json:"params"` diff --git a/server/pkg/utils/anyx/anyx.go b/server/pkg/utils/anyx/anyx.go index 77a2d14d..79a0c04b 100644 --- a/server/pkg/utils/anyx/anyx.go +++ b/server/pkg/utils/anyx/anyx.go @@ -2,6 +2,7 @@ package anyx import ( "encoding/json" + "math" "reflect" "strconv" ) @@ -40,6 +41,10 @@ func ConvInt(val any) int { return int(value) case uint8: return int(value) + case float32: + return int(value) + case float64: + return int(math.Round(value)) default: return 0 } diff --git a/server/resources/data/mayfly-go.sqlite b/server/resources/data/mayfly-go.sqlite index 89fc75f2..cb80588d 100644 Binary files a/server/resources/data/mayfly-go.sqlite and b/server/resources/data/mayfly-go.sqlite differ diff --git a/server/resources/script/sql/mayfly-go.sql b/server/resources/script/sql/mayfly-go.sql index ab27c195..f7cc4d9b 100644 --- a/server/resources/script/sql/mayfly-go.sql +++ b/server/resources/script/sql/mayfly-go.sql @@ -10,6 +10,7 @@ CREATE TABLE `t_db_instance` ( `name` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '数据库实例名称', `host` varchar(100) COLLATE utf8mb4_bin NOT NULL, `port` int(8) NOT NULL, + `sid` varchar(255) NULL COMMENT 'oracle数据库需要sid', `username` varchar(255) COLLATE utf8mb4_bin NOT NULL, `password` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL, `type` varchar(20) COLLATE utf8mb4_bin NOT NULL COMMENT '数据库实例类型(mysql...)', diff --git a/server/resources/script/sql/v1.6.2.sql b/server/resources/script/sql/v1.6.2.sql index 689f49ce..84b1db84 100644 --- a/server/resources/script/sql/v1.6.2.sql +++ b/server/resources/script/sql/v1.6.2.sql @@ -234,3 +234,7 @@ INSERT INTO `t_sys_config` (`name`, `key`, `params`, `value`, `remark`, `permiss INSERT INTO `t_sys_config` (`name`, `key`, `params`, `value`, `remark`, `permission`, `create_time`, `creator_id`, `creator`, `update_time`, `modifier_id`, `modifier`, `is_deleted`, `delete_time`) VALUES('数据库备份恢复', 'DbBackupRestore', '[{"model":"backupPath","name":"备份路径","placeholder":"备份文件存储路径"}]', '{"backupPath":"./db/backup"}', '', 'admin,', '2023-12-29 09:55:26', 1, 'admin', '2023-12-29 15:45:24', 1, 'admin', 0, NULL); INSERT INTO `t_sys_config` (`name`, `key`, `params`, `value`, `remark`, `permission`, `create_time`, `creator_id`, `creator`, `update_time`, `modifier_id`, `modifier`, `is_deleted`, `delete_time`) VALUES('Mysql可执行文件', 'MysqlBin', '[{"model":"path","name":"路径","placeholder":"可执行文件路径","required":true},{"model":"mysql","name":"mysql","placeholder":"mysql命令路径(空则为 路径/mysql)","required":false},{"model":"mysqldump","name":"mysqldump","placeholder":"mysqldump命令路径(空则为 路径/mysqldump)","required":false},{"model":"mysqlbinlog","name":"mysqlbinlog","placeholder":"mysqlbinlog命令路径(空则为 路径/mysqlbinlog)","required":false}]', '{"mysql":"","mysqldump":"","mysqlbinlog":"","path":"./db/mysql/bin"}', '', 'admin,', '2023-12-29 10:01:33', 1, 'admin', '2023-12-29 13:34:40', 1, 'admin', 0, NULL); INSERT INTO `t_sys_config` (`name`, `key`, `params`, `value`, `remark`, `permission`, `create_time`, `creator_id`, `creator`, `update_time`, `modifier_id`, `modifier`, `is_deleted`, `delete_time`) VALUES('MariaDB可执行文件', 'MariadbBin', '[{"model":"path","name":"路径","placeholder":"可执行文件路径","required":true},{"model":"mysql","name":"mysql","placeholder":"mysql命令路径(空则为 路径/mysql)","required":false},{"model":"mysqldump","name":"mysqldump","placeholder":"mysqldump命令路径(空则为 路径/mysqldump)","required":false},{"model":"mysqlbinlog","name":"mysqlbinlog","placeholder":"mysqlbinlog命令路径(空则为 路径/mysqlbinlog)","required":false}]', '{"mysql":"","mysqldump":"","mysqlbinlog":"","path":"./db/mariadb/bin"}', '', 'admin,', '2023-12-29 10:01:33', 1, 'admin', '2023-12-29 13:34:40', 1, 'admin', 0, NULL); + + +ALTER TABLE `t_db_instance` + ADD COLUMN `sid` varchar(255) NULL COMMENT 'oracle数据库需要sid' AFTER `port`; \ No newline at end of file