!82 feat: dbms支持oracle数据库

* fix:oracle bug修复
* feat: dbms支持oracle数据库
This commit is contained in:
zongyangleo
2024-01-15 11:55:59 +00:00
committed by Coder慌
parent 9c524292f0
commit b873855b44
38 changed files with 1026 additions and 26 deletions

View File

@@ -2,6 +2,7 @@
node_modules
/dist
*.lock
pnpm-lock.yaml
# local env files
.env.local

File diff suppressed because one or more lines are too long

View File

@@ -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
}
]
}

View File

@@ -9,7 +9,7 @@
</el-form-item>
<el-form-item prop="type" label="类型" required>
<el-select @change="changeDbType" style="width: 100%" v-model="form.type" placeholder="请选择数据库类型">
<el-option v-for="dt in dbTypes" :key="dt.type" :value="dt.type">
<el-option v-for="dt in dbTypes" :key="dt.type" :value="dt.type" :label="dt.label">
<SvgIcon :name="getDbDialect(dt.type).getInfo().icon" :size="18" />
{{ dt.label }}
</el-option>
@@ -24,6 +24,9 @@
<el-input type="number" v-model.number="form.port" placeholder="端口"></el-input>
</el-col>
</el-form-item>
<el-form-item v-if="form.type === DbType.oracle" prop="sid" label="SID">
<el-input v-model.trim="form.sid" placeholder="请输入服务id"></el-input>
</el-form-item>
<el-form-item prop="username" label="用户名" required>
<el-input v-model.trim="form.username" placeholder="请输入用户名"></el-input>
</el-form-item>
@@ -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: '',

View File

@@ -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 });

View File

@@ -89,7 +89,7 @@
<el-row>
<el-col :span="8">
<el-form-item prop="pageSize" label="分页大小" required>
<el-input type="number" v-model.trim="form.pageSize" placeholder="同步数据时查询的每页数据大小" auto-complete="off" />
<el-input type="number" v-model.number="form.pageSize" placeholder="同步数据时查询的每页数据大小" auto-complete="off" />
</el-form-item>
</el-col>
@@ -184,7 +184,7 @@
</template>
<script lang="ts" setup>
import { reactive, ref, toRefs, watch, computed } from 'vue';
import { computed, reactive, ref, toRefs, watch } from 'vue';
import { dbApi } from './api';
import { ElMessage } from 'element-plus';
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';

View File

@@ -18,6 +18,10 @@ export const dbApi = {
sqlExec: Api.newPost('/dbs/{id}/exec-sql').withBeforeHandler((param: any) => {
// sql编码处理
if (param.sql) {
// 判断是开发环境就打印sql
if (process.env.NODE_ENV === 'development') {
console.log(param.sql);
}
param.sql = Base64.encode(param.sql);
}
return param;
@@ -58,7 +62,7 @@ export const dbApi = {
enableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/enable'),
disableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/disable'),
saveDbRestore: Api.newPut('/dbs/{dbId}/restores/{id}'),
// 数据同步相关
datasyncTasks: Api.newGet('/datasync/tasks'),
saveDatasyncTask: Api.newPost('/datasync/tasks/save').withBeforeHandler((param: any) => {

View File

@@ -386,8 +386,8 @@ const onRunSql = async (newTab = false) => {
const tableName = sql.split(/from/i)[1];
if (tableName) {
const tn = tableName.trim().split(' ')[0].split('\n')[0];
execRes.table = tn;
execRes.table = tn;
// 去除表名前后的字符`或者"
execRes.table = tn.replace(/`/g, '').replace(/"/g, '');
} else {
execRes.table = '';
}

View File

@@ -714,6 +714,7 @@ const submitUpdateFields = async () => {
const db = state.db;
let res = '';
const dbDialect = getDbDialect(dbInst.type);
for (let updateRow of cellUpdateMap.values()) {
let sql = `UPDATE ${dbInst.wrapName(state.table)} SET `;
@@ -731,7 +732,8 @@ const submitUpdateFields = async () => {
}
// 更新字段列信息
const updateColumn = await dbInst.loadTableColumn(db, state.table, k);
sql += ` ${dbInst.wrapName(k)} = ${DbInst.wrapColumnValue(updateColumn.columnType, rowData[k])},`;
sql += ` ${dbInst.wrapName(k)} = ${DbInst.wrapColumnValue(updateColumn.columnType, rowData[k], dbDialect)},`;
// 如果修改的字段是主键
if (k === primaryKeyName) {
@@ -790,6 +792,10 @@ const getFormatTimeValue = (dataType: DataType, originValue: string): string =>
if (!originValue || dataType === DataType.Number || dataType === DataType.String) {
return originValue;
}
// 把Z去掉
originValue = originValue.replace('Z', '');
switch (dataType) {
case DataType.Time:
return dateStrFormat('HH:mm:ss', originValue);

View File

@@ -140,7 +140,7 @@
import { reactive, ref, toRefs, watch } from 'vue';
import { ElMessage } from 'element-plus';
import SqlExecBox from '../sqleditor/SqlExecBox';
import { getDbDialect, DbType, RowDefinition, IndexDefinition } from '../../dialect/index';
import { DbType, getDbDialect, IndexDefinition, RowDefinition } from '../../dialect/index';
const props = defineProps({
visible: {

View File

@@ -181,7 +181,7 @@ const state = reactive({
visible: false,
activeName: '1',
type: '',
enableEditTypes: [DbType.mysql, DbType.mariadb, DbType.postgresql, DbType.dm], // 支持"编辑表"的数据库类型
enableEditTypes: [DbType.mysql, DbType.mariadb, DbType.postgresql, DbType.dm, DbType.oracle], // 支持"编辑表"的数据库类型
data: {
// 修改表时,传递修改数据
edit: false,
@@ -321,7 +321,7 @@ const dropTable = async (row: any) => {
dbId: props.dbId as any,
db: props.db as any,
runSuccessCallback: async () => {
state.tables = await dbApi.tableInfos.request({ id: props.dbId, db: props.db });
await getTables();
},
});
} catch (err) {
@@ -357,7 +357,7 @@ const openEditTable = async (row: any) => {
const onSubmitSql = async (row: { tableName: string }) => {
await openEditTable(row);
state.tables = await dbApi.tableInfos.request({ id: props.dbId, db: props.db });
await getTables();
};
</script>
<style lang="scss"></style>

View File

@@ -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);
}
/**

View File

@@ -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}'`;
}
}

View File

@@ -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('不支持的数据库');
}

View File

@@ -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;
}
}

View File

@@ -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}'`;
}
}

View File

@@ -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}'`;
}
}

View File

@@ -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}'`;
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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"`

View File

@@ -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"`

View File

@@ -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

View File

@@ -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") {

View File

@@ -15,6 +15,7 @@ const (
DbTypeMariadb DbType = "mariadb"
DbTypePostgres DbType = "postgres"
DbTypeDM DbType = "dm"
DbTypeOracle DbType = "oracle"
)
func ToDbType(dbType string) DbType {

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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))
}

View File

@@ -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
}

View File

@@ -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}
}

View File

@@ -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"`

View File

@@ -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
}

View File

@@ -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...)',

View File

@@ -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`;