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