diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index bcac7f01ce..0748f85c22 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -77,6 +77,7 @@ const defaultForm = (): Omit => ({ proxy_username: "", proxy_password: "", ssl: false, + oracle_connection_type: "service_name", connection_string: undefined, jdbc_driver_class: undefined, jdbc_driver_paths: [], @@ -297,6 +298,7 @@ watch( proxy_username: config.proxy_username || "", proxy_password: config.proxy_password || "", ssl: config.ssl || false, + oracle_connection_type: config.oracle_connection_type || "service_name", connection_string: config.connection_string, jdbc_driver_class: config.jdbc_driver_class, jdbc_driver_paths: config.jdbc_driver_paths || [], @@ -547,6 +549,11 @@ function connectionConfigForSubmit(id: string): ConnectionConfig { if (config.db_type === "mongodb" && !mongoUseUrl.value) { config.connection_string = undefined; } + if (config.db_type !== "oracle") { + config.oracle_connection_type = undefined; + } else { + config.oracle_connection_type = config.oracle_connection_type || "service_name"; + } if (config.db_type === "jdbc") { config.host = ""; config.port = 0; @@ -1189,6 +1196,40 @@ function openExternalUrl(url: string) { +
+ +
+ + +
+
+

diff --git a/apps/desktop/src/lib/connectionUrl.ts b/apps/desktop/src/lib/connectionUrl.ts index 19752f2762..7d64c81eb8 100644 --- a/apps/desktop/src/lib/connectionUrl.ts +++ b/apps/desktop/src/lib/connectionUrl.ts @@ -12,6 +12,7 @@ export interface ParsedConnectionUrl { urlParams: string; ssl: boolean; connectionString?: string; + oracleConnectionType?: "service_name" | "sid"; useMongoUrl?: boolean; } @@ -108,6 +109,46 @@ function parseJdbcSqlServerUrl(source: string): ParsedConnectionUrl | null { }; } +function parseJdbcOracleUrl(source: string): ParsedConnectionUrl | null { + const serviceMatch = source.match(/^jdbc:oracle:thin:@\/\/([^:/?#]+)(?::(\d+))?\/([^?]+)(?:\?(.*))?$/i); + if (serviceMatch) { + const profile = SCHEME_PROFILES.oracle; + return { + dbType: profile.type, + driverProfile: profile.profile, + driverLabel: profile.label, + host: serviceMatch[1], + port: serviceMatch[2] ? Number(serviceMatch[2]) : profile.defaultPort, + username: "", + password: "", + database: decodeUrlPart(serviceMatch[3]), + urlParams: serviceMatch[4] || "", + ssl: false, + oracleConnectionType: "service_name", + }; + } + + const sidMatch = source.match(/^jdbc:oracle:thin:@([^:/?#]+)(?::(\d+))?:([^?]+)(?:\?(.*))?$/i); + if (sidMatch) { + const profile = SCHEME_PROFILES.oracle; + return { + dbType: profile.type, + driverProfile: profile.profile, + driverLabel: profile.label, + host: sidMatch[1], + port: sidMatch[2] ? Number(sidMatch[2]) : profile.defaultPort, + username: "", + password: "", + database: decodeUrlPart(sidMatch[3]), + urlParams: sidMatch[4] || "", + ssl: false, + oracleConnectionType: "sid", + }; + } + + return null; +} + function parseJdbcUCanAccessUrl(source: string): ParsedConnectionUrl | null { const match = source.match(/^jdbc:ucanaccess:\/\/(.+?)(?:;.*)?$/i); if (!match) return null; @@ -138,6 +179,8 @@ export function parseConnectionUrl(value: string, preferredProfile?: string): Pa } const jdbcUCanAccess = parseJdbcUCanAccessUrl(input); if (jdbcUCanAccess) return jdbcUCanAccess; + const jdbcOracle = parseJdbcOracleUrl(input); + if (jdbcOracle) return jdbcOracle; const jdbcSqlServer = parseJdbcSqlServerUrl(input); if (jdbcSqlServer) return jdbcSqlServer; const source = input.replace(/^jdbc:/i, ""); @@ -204,5 +247,6 @@ export function applyParsedConnectionUrl( url_params: parsed.urlParams, ssl: parsed.ssl, connection_string: parsed.connectionString, + oracle_connection_type: parsed.oracleConnectionType, }; } diff --git a/apps/desktop/src/lib/dbeaverImport.ts b/apps/desktop/src/lib/dbeaverImport.ts index dd9d630611..cad3302305 100644 --- a/apps/desktop/src/lib/dbeaverImport.ts +++ b/apps/desktop/src/lib/dbeaverImport.ts @@ -119,6 +119,7 @@ function parseJdbcUrl(url: string, profile: ConnectionProfile) { params?: string; username?: string; password?: string; + oracleConnectionType?: "service_name" | "sid"; } = {}; const source = url.trim(); if (!source) return result; @@ -143,6 +144,7 @@ function parseJdbcUrl(url: string, profile: ConnectionProfile) { result.host = oracleServiceMatch[1]; result.port = getNumber(oracleServiceMatch[2]); result.database = oracleServiceMatch[3]; + result.oracleConnectionType = "service_name"; return result; } @@ -151,6 +153,7 @@ function parseJdbcUrl(url: string, profile: ConnectionProfile) { result.host = oracleSidMatch[1]; result.port = getNumber(oracleSidMatch[2]); result.database = oracleSidMatch[3]; + result.oracleConnectionType = "sid"; return result; } @@ -228,6 +231,7 @@ function buildConnection( ssh_expose_lan: false, ssh_connect_timeout_secs: 5, ssl: false, + oracle_connection_type: profile.dbType === "oracle" ? parsedUrl.oracleConnectionType || "service_name" : undefined, connection_string: profile.dbType === "jdbc" || profile.dbType === "mongodb" ? url || undefined : undefined, jdbc_driver_class: profile.dbType === "jdbc" ? getString(config["driver-class"] || entry.driver) || undefined : undefined, diff --git a/apps/desktop/src/lib/navicatImport.ts b/apps/desktop/src/lib/navicatImport.ts index 791df98e1f..8461cc7f72 100644 --- a/apps/desktop/src/lib/navicatImport.ts +++ b/apps/desktop/src/lib/navicatImport.ts @@ -134,6 +134,12 @@ async function parseConnection(node: ParsedNode): Promise serde_json::Value { let connection_string = if config.db_type == DatabaseType::MongoDb { config.connection_url_with_host(host, port) + } else if config.db_type == DatabaseType::Oracle { + oracle_jdbc_connection_string(config, host, port, database) } else { config.connection_string.as_deref().unwrap_or("").to_string() }; @@ -455,6 +457,19 @@ pub fn agent_connect_params(config: &ConnectionConfig, host: &str, port: u16, da }) } +fn oracle_jdbc_connection_string(config: &ConnectionConfig, host: &str, port: u16, database: &str) -> String { + let database = database.trim(); + if database.is_empty() { + return config.connection_string.as_deref().unwrap_or("").to_string(); + } + + if config.oracle_connection_type.as_deref() == Some("sid") { + format!("jdbc:oracle:thin:@{host}:{port}:{database}") + } else { + format!("jdbc:oracle:thin:@//{host}:{port}/{database}") + } +} + pub async fn probe_connection_endpoint(config: &ConnectionConfig, host: &str, port: u16) -> Result<(), String> { if config.db_type == DatabaseType::MongoDb && config.connection_string.as_deref().is_some_and(|value| !value.is_empty()) @@ -521,6 +536,7 @@ mod tests { proxy_password: String::new(), ssl: false, sysdba: false, + oracle_connection_type: None, connection_string: None, external_config: None, jdbc_driver_class: None, @@ -560,6 +576,32 @@ mod tests { assert_eq!(params["connection_string"], "mongodb://mongouser:secret@172.22.4.42:27017/RestCloud%5FV45PUB%5FGateway?authSource=admin&authMechanism=SCRAM-SHA-1"); } + #[test] + fn agent_connect_params_build_oracle_service_connection_string() { + let mut config = mysql_config(Some("ORCLPDB1")); + config.db_type = DatabaseType::Oracle; + config.host = "oracle.example.com".to_string(); + config.port = 1521; + config.username = "system".to_string(); + config.password = "oracle".to_string(); + + let params = agent_connect_params(&config, "oracle.example.com", 1521, "ORCLPDB1"); + + assert_eq!(params["database"], "ORCLPDB1"); + assert_eq!(params["connection_string"], "jdbc:oracle:thin:@//oracle.example.com:1521/ORCLPDB1"); + } + + #[test] + fn agent_connect_params_build_oracle_sid_connection_string() { + let mut config = mysql_config(Some("ORCL")); + config.db_type = DatabaseType::Oracle; + config.oracle_connection_type = Some("sid".to_string()); + + let params = agent_connect_params(&config, "127.0.0.1", 11521, "ORCL"); + + assert_eq!(params["connection_string"], "jdbc:oracle:thin:@127.0.0.1:11521:ORCL"); + } + async fn test_app_state() -> (AppState, std::path::PathBuf) { let dir = std::env::temp_dir().join(format!("dbx-core-test-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); diff --git a/crates/dbx-core/src/connection_secrets.rs b/crates/dbx-core/src/connection_secrets.rs index 9c193d4b60..fb44bea609 100644 --- a/crates/dbx-core/src/connection_secrets.rs +++ b/crates/dbx-core/src/connection_secrets.rs @@ -308,6 +308,7 @@ mod tests { proxy_password: String::new(), ssl: false, sysdba: false, + oracle_connection_type: None, connection_string: None, external_config: None, jdbc_driver_class: None, diff --git a/crates/dbx-core/src/models/connection.rs b/crates/dbx-core/src/models/connection.rs index 86ed00bd2d..6a76bfe715 100644 --- a/crates/dbx-core/src/models/connection.rs +++ b/crates/dbx-core/src/models/connection.rs @@ -57,6 +57,8 @@ pub struct ConnectionConfig { pub ssl: bool, #[serde(default)] pub sysdba: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub oracle_connection_type: Option, #[serde(default)] pub connection_string: Option, /// Typed configuration for external tabular sources. @@ -611,6 +613,7 @@ mod tests { proxy_password: String::new(), ssl: false, sysdba: false, + oracle_connection_type: None, connection_string: None, external_config: None, jdbc_driver_class: None, diff --git a/crates/dbx-core/src/query.rs b/crates/dbx-core/src/query.rs index e2b05f58af..3ec29a46bb 100644 --- a/crates/dbx-core/src/query.rs +++ b/crates/dbx-core/src/query.rs @@ -1041,6 +1041,7 @@ mod tests { proxy_password: String::new(), ssl: false, sysdba: false, + oracle_connection_type: None, connection_string: Some("jdbc:h2:mem:test".to_string()), external_config: None, jdbc_driver_class: None, diff --git a/crates/dbx-web/src/routes/connection.rs b/crates/dbx-web/src/routes/connection.rs index dc8dd1a7e7..9cd4a31a54 100644 --- a/crates/dbx-web/src/routes/connection.rs +++ b/crates/dbx-web/src/routes/connection.rs @@ -161,6 +161,7 @@ mod tests { proxy_password: String::new(), ssl: false, sysdba: false, + oracle_connection_type: None, connection_string: None, external_config: None, jdbc_driver_class: None, diff --git a/packages/app-tests/connectionDialogOracleMode.test.ts b/packages/app-tests/connectionDialogOracleMode.test.ts new file mode 100644 index 0000000000..9c9257b2a1 --- /dev/null +++ b/packages/app-tests/connectionDialogOracleMode.test.ts @@ -0,0 +1,17 @@ +import { readFileSync } from "node:fs"; +import assert from "node:assert/strict"; +import test from "node:test"; + +const source = readFileSync("apps/desktop/src/components/connection/ConnectionDialog.vue", "utf8"); + +test("Oracle connection mode uses an inline option group", () => { + const oracleModeBlock = source.match( + /

]*>\s*]*>连接方式<\/Label>[\s\S]*?<\/div>/, + )?.[0]; + + assert.ok(oracleModeBlock, "expected Oracle connection mode block"); + assert.match(oracleModeBlock, /type="button"/); + assert.match(oracleModeBlock, /form\.oracle_connection_type = 'service_name'/); + assert.match(oracleModeBlock, /form\.oracle_connection_type = 'sid'/); + assert.doesNotMatch(oracleModeBlock, /