diff --git a/frontend/src/models/system.js b/frontend/src/models/system.js index 8ca6d0dd490..c0e14129987 100644 --- a/frontend/src/models/system.js +++ b/frontend/src/models/system.js @@ -816,7 +816,7 @@ const System = { /** * Validates a SQL connection string. - * @param {'postgresql'|'mysql'|'sql-server'} engine - the database engine identifier + * @param {'postgresql'|'mysql'|'sql-server'|'oracle'} engine - the database engine identifier * @param {string} connectionString - the connection string to validate * @returns {Promise<{success: boolean, error: string | null}>} */ diff --git a/frontend/src/pages/Admin/Agents/SQLConnectorSelection/DBConnection.jsx b/frontend/src/pages/Admin/Agents/SQLConnectorSelection/DBConnection.jsx index 59842082477..c2c8a363b9a 100644 --- a/frontend/src/pages/Admin/Agents/SQLConnectorSelection/DBConnection.jsx +++ b/frontend/src/pages/Admin/Agents/SQLConnectorSelection/DBConnection.jsx @@ -1,6 +1,8 @@ import PostgreSQLLogo from "./icons/postgresql.png"; import MySQLLogo from "./icons/mysql.png"; import MSSQLLogo from "./icons/mssql.png"; +import OracleLogo from "./icons/oracle.png"; + import { PencilSimple, X } from "@phosphor-icons/react"; import { useModal } from "@/hooks/useModal"; import EditSQLConnection from "./SQLConnectionModal"; @@ -9,6 +11,7 @@ export const DB_LOGOS = { postgresql: PostgreSQLLogo, mysql: MySQLLogo, "sql-server": MSSQLLogo, + oracle: OracleLogo, }; export default function DBConnection({ diff --git a/frontend/src/pages/Admin/Agents/SQLConnectorSelection/SQLConnectionModal.jsx b/frontend/src/pages/Admin/Agents/SQLConnectorSelection/SQLConnectionModal.jsx index c68f2e79173..3cd9d043b1b 100644 --- a/frontend/src/pages/Admin/Agents/SQLConnectorSelection/SQLConnectionModal.jsx +++ b/frontend/src/pages/Admin/Agents/SQLConnectorSelection/SQLConnectionModal.jsx @@ -52,6 +52,8 @@ function assembleConnectionString({ return `mysql://${username}:${password}@${host}:${port}/${database}`; case "sql-server": return `mssql://${username}:${password}@${host}:${port}/${database}?encrypt=${encrypt}`; + case "oracle": + return `oracle://${username}:${password}@${host}:${port}/${database}`; default: return null; } @@ -318,6 +320,11 @@ export default function SQLConnectionModal({ active={engine === "sql-server"} onClick={() => setEngine("sql-server")} /> + setEngine("oracle")} + /> diff --git a/frontend/src/pages/Admin/Agents/SQLConnectorSelection/icons/oracle.jpg b/frontend/src/pages/Admin/Agents/SQLConnectorSelection/icons/oracle.jpg new file mode 100644 index 00000000000..71f5acdc09e Binary files /dev/null and b/frontend/src/pages/Admin/Agents/SQLConnectorSelection/icons/oracle.jpg differ diff --git a/server/package.json b/server/package.json index 7bd533a739f..46a6cbfc1cf 100644 --- a/server/package.json +++ b/server/package.json @@ -71,6 +71,7 @@ "mysql2": "^3.9.8", "ollama": "^0.6.3", "openai": "4.95.1", + "oracledb": "^6.10.0", "pg": "^8.11.5", "pinecone-client": "^1.1.0", "pluralize": "^8.0.0", diff --git a/server/utils/agents/aibitat/plugins/sql-agent/SQLConnectors/Oracle.js b/server/utils/agents/aibitat/plugins/sql-agent/SQLConnectors/Oracle.js new file mode 100644 index 00000000000..852870e53a9 --- /dev/null +++ b/server/utils/agents/aibitat/plugins/sql-agent/SQLConnectors/Oracle.js @@ -0,0 +1,115 @@ + + +const { ConnectionStringParser } = require("./utils"); + +// -- oracledb.initOracleClient({libDir: "/opt/oracle/instantclient_23_26",}); +// Thin mode: DO NOT call initOracleClient() + + + + + +class OracleConnector { + #connected = false; + database_id = ""; + + constructor( + config = { + connectionString: null, + } + ) { + // Load the Oracle driver on demand when this constructor is called + this.oracledb = require("oracledb"); + + // Set global output format once (will be set each time an instance is created, + // but that's harmless due to caching) + this.oracledb.outFormat = this.oracledb.OUT_FORMAT_OBJECT; + + // Log thin mode status when an instance is created + console.log("Oracle driver thin mode:", oracledb.thin); + + this.className = "OracleConnector"; + this.connectionString = config.connectionString; + this._client = null; + this.database_id = this.#parseDatabase(); + } + + #parseDatabase() { + const connectionParser = new ConnectionStringParser({ scheme: "oracle" }); + const parsed = connectionParser.parse(this.connectionString); + return parsed?.endpoint; // service name + } + + async connect() { + const connectionParser = new ConnectionStringParser({ scheme: "oracle" }); + const parsed = connectionParser.parse(this.connectionString); + + const host = parsed.hosts[0]?.host; + const port = parsed.hosts[0]?.port || 1521; + const service = parsed.endpoint; + + this._client = await oracledb.getConnection({ + user: parsed.username, + password: parsed.password, + connectString: `${host}:${port}/${service}`, + }); + + this.#connected = true; + return this._client; + } + + /** + * @param {string} queryString + * @returns {Promise} + */ + async runQuery(queryString = "") { + const result = { rows: [], count: 0, error: null }; + + try { + if (!this.#connected) await this.connect(); + + const query = await this._client.execute(queryString, [], { + autoCommit: true, + }); + + result.rows = query.rows || []; + result.count = query.rows ? query.rows.length : 0; + } catch (err) { + console.log(this.className, err); + result.error = err.message; + } finally { + if (this._client) { + await this._client.close(); + this.#connected = false; + } + } + + return result; + } + + async validateConnection() { + try { + const result = await this.runQuery("SELECT 1 FROM DUAL"); + return { success: !result.error, error: result.error }; + } catch (error) { + return { success: false, error: error.message }; + } + } + + getTablesSql() { + return ` + SELECT table_name + FROM user_tables + `; + } + + getTableSchemaSql(table_name) { + return ` + SELECT column_name, data_type + FROM user_tab_columns + WHERE table_name = UPPER('${table_name}') + `; + } +} + +module.exports.OracleConnector = OracleConnector; diff --git a/server/utils/agents/aibitat/plugins/sql-agent/SQLConnectors/index.js b/server/utils/agents/aibitat/plugins/sql-agent/SQLConnectors/index.js index 252ce54167f..a8fb712134e 100644 --- a/server/utils/agents/aibitat/plugins/sql-agent/SQLConnectors/index.js +++ b/server/utils/agents/aibitat/plugins/sql-agent/SQLConnectors/index.js @@ -2,7 +2,7 @@ const { SystemSettings } = require("../../../../../../models/systemSettings"); const { safeJsonParse } = require("../../../../../http"); /** - * @typedef {('postgresql'|'mysql'|'sql-server')} SQLEngine + * @typedef {('postgresql'|'mysql'|'sql-server'|'oracle')} SQLEngine */ /** @@ -36,6 +36,9 @@ function getDBClient(identifier = "", connectionConfig = {}) { case "sql-server": const { MSSQLConnector } = require("./MSSQL"); return new MSSQLConnector(connectionConfig); + case "oracle": + const { OracleConnector } = require("./Oracle"); + return new OracleConnector(connectionConfig); default: throw new Error( `There is no supported database connector for ${identifier}` @@ -64,8 +67,8 @@ async function validateConnection(identifier = "", connectionConfig = {}) { try { const client = getDBClient(identifier, connectionConfig); return await client.validateConnection(); - } catch { - console.log(`Failed to connect to ${identifier} database.`); + } catch (error) { + console.log(`Failed to connect to ${identifier} database.`, error); return { success: false, error: `Unable to connect to ${identifier}. Please verify your connection details.`,