From bb2768a62f4d66bfc744c7173006ac80f01adab5 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Fri, 12 Jun 2026 11:52:38 +0800 Subject: [PATCH] fix(db): update Oracle and OceanBase agent URLs --- crates/dbx-core/src/agent_connection.rs | 195 +++++++++++++++++++++--- crates/dbx-core/src/connection.rs | 69 ++++++--- 2 files changed, 217 insertions(+), 47 deletions(-) diff --git a/crates/dbx-core/src/agent_connection.rs b/crates/dbx-core/src/agent_connection.rs index 22f799eac3..4664d73c64 100644 --- a/crates/dbx-core/src/agent_connection.rs +++ b/crates/dbx-core/src/agent_connection.rs @@ -15,8 +15,10 @@ pub fn agent_connect_params(config: &ConnectionConfig, host: &str, port: u16, da }; let connection_string = if config.db_type == DatabaseType::MongoDb { config.connection_url_with_host(host, port) - } else if matches!(config.db_type, DatabaseType::Oracle | DatabaseType::OceanbaseOracle) { + } else if config.db_type == DatabaseType::Oracle { oracle_jdbc_connection_string(config, host, port, database) + } else if config.db_type == DatabaseType::OceanbaseOracle { + oceanbase_oracle_jdbc_connection_string(config, host, port, database) } else if matches!(config.db_type, DatabaseType::Kingbase | DatabaseType::Highgo | DatabaseType::Vastbase) { postgres_like_agent_jdbc_connection_string(config, host, port, database) } else if config.db_type == DatabaseType::SapHana { @@ -205,6 +207,64 @@ pub fn oracle_error_with_driver_hint(config: &ConnectionConfig, err: &str) -> St ) } +pub fn oracle_alternate_connect_configs(config: &ConnectionConfig, err: &str) -> Vec { + if config.db_type != DatabaseType::Oracle { + return Vec::new(); + } + if config.driver_profile.as_deref() == Some("oracle-10g") { + return Vec::new(); + } + if config.connection_string.as_deref().is_some_and(|value| !value.trim().is_empty()) { + return Vec::new(); + } + if !oracle_listener_error_can_retry(err) { + return Vec::new(); + } + + let database = config.effective_database().unwrap_or("").trim(); + if database.is_empty() { + return Vec::new(); + } + + let host = config.host.trim(); + let port = config.port; + let current_url = oracle_jdbc_connection_string(config, host, port, database); + let service_url = oracle_service_jdbc_url(host, port, database); + let sid_url = oracle_sid_jdbc_url(host, port, database); + let legacy_service_url = oracle_legacy_service_jdbc_url(host, port, database); + let descriptor_service_url = oracle_descriptor_jdbc_url(host, port, database, "SERVICE_NAME"); + let descriptor_sid_url = oracle_descriptor_jdbc_url(host, port, database, "SID"); + + let normalized = err.to_lowercase(); + let candidates = if normalized.contains("ora-12505") { + vec![service_url, descriptor_service_url, legacy_service_url] + } else if normalized.contains("ora-12514") { + vec![sid_url, descriptor_sid_url, legacy_service_url] + } else { + match config.oracle_connection_type.as_deref() { + Some("sid") => vec![service_url, legacy_service_url, descriptor_sid_url, descriptor_service_url], + _ => vec![sid_url, legacy_service_url, descriptor_service_url, descriptor_sid_url], + } + }; + + let mut urls = Vec::new(); + for url in candidates { + if url == current_url || urls.iter().any(|seen| seen == &url) { + continue; + } + urls.push(url); + } + + urls.into_iter() + .map(|url| { + let mut retry = config.clone(); + retry.oracle_connection_type = None; + retry.connection_string = Some(url); + retry + }) + .collect() +} + fn oracle_jdbc_connection_string(config: &ConnectionConfig, host: &str, port: u16, database: &str) -> String { if let Some(connection_string) = config.connection_string.as_deref().filter(|value| !value.trim().is_empty()) { let connection_string = connection_string.trim(); @@ -220,12 +280,54 @@ fn oracle_jdbc_connection_string(config: &ConnectionConfig, host: &str, port: u1 } if config.oracle_connection_type.as_deref() == Some("sid") { - format!("jdbc:oracle:thin:@{host}:{port}:{database}") + oracle_sid_jdbc_url(host, port, database) } else { - format!("jdbc:oracle:thin:@//{host}:{port}/{database}") + oracle_service_jdbc_url(host, port, database) } } +fn oracle_listener_error_can_retry(err: &str) -> bool { + let normalized = err.to_lowercase(); + normalized.contains("ora-12505") + || normalized.contains("ora-12514") + || normalized.contains("ora-12541") + || normalized.contains("no listener") + || err.contains("没有监听程序") +} + +fn oracle_service_jdbc_url(host: &str, port: u16, database: &str) -> String { + format!("jdbc:oracle:thin:@//{host}:{port}/{database}") +} + +fn oracle_sid_jdbc_url(host: &str, port: u16, database: &str) -> String { + format!("jdbc:oracle:thin:@{host}:{port}:{database}") +} + +fn oracle_legacy_service_jdbc_url(host: &str, port: u16, database: &str) -> String { + format!("jdbc:oracle:thin:@{host}:{port}/{database}") +} + +fn oracle_descriptor_jdbc_url(host: &str, port: u16, database: &str, key: &str) -> String { + format!("jdbc:oracle:thin:@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST={host})(PORT={port}))(CONNECT_DATA=({key}={database})))") +} + +fn oceanbase_oracle_jdbc_connection_string(config: &ConnectionConfig, host: &str, port: u16, database: &str) -> String { + if let Some(connection_string) = config.connection_string.as_deref().filter(|value| !value.trim().is_empty()) { + let connection_string = connection_string.trim(); + if host == config.host && port == config.port { + return connection_string.to_string(); + } + return crate::models::connection::rewrite_jdbc_url_host(connection_string, host, port); + } + + let database = database.trim(); + if database.is_empty() { + return String::new(); + } + + append_agent_url_params(format!("jdbc:oceanbase://{host}:{port}/{database}"), config.url_params.as_deref()) +} + fn postgres_like_agent_jdbc_connection_string( config: &ConnectionConfig, host: &str, @@ -262,24 +364,35 @@ pub fn oracle_auth_fallback_profiles(config: &ConnectionConfig, err: &str) -> Ve } pub fn oracle_alternate_connect_config(config: &ConnectionConfig, err: &str) -> Option { - if config.db_type != DatabaseType::Oracle { - return None; - } - if config.driver_profile.as_deref() == Some("oracle-10g") { - return None; - } - if config.connection_string.as_deref().is_some_and(|value| !value.trim().is_empty()) { - return None; - } - let normalized = err.to_lowercase(); - if !normalized.contains("ora-12505") && !normalized.contains("ora-12514") { - return None; - } + oracle_alternate_connect_configs(config, err).into_iter().next() +} - let mut retry = config.clone(); - retry.oracle_connection_type = - Some(if config.oracle_connection_type.as_deref() == Some("sid") { "service_name" } else { "sid" }.to_string()); - Some(retry) +pub fn oracle_alternate_connect_config_labels(configs: &[ConnectionConfig]) -> Vec { + configs + .iter() + .map(|config| { + config + .connection_string + .as_deref() + .map(oracle_connection_string_label) + .unwrap_or_else(|| config.oracle_connection_type.as_deref().unwrap_or("service_name").to_string()) + }) + .collect() +} + +fn oracle_connection_string_label(connection_string: &str) -> String { + let upper = connection_string.to_ascii_uppercase(); + if upper.contains("(SERVICE_NAME=") { + "descriptor service name".to_string() + } else if upper.contains("(SID=") { + "descriptor SID".to_string() + } else if connection_string.starts_with("jdbc:oracle:thin:@//") { + "service name".to_string() + } else if connection_string.contains(':') && !connection_string.contains('/') { + "SID".to_string() + } else { + "legacy service name".to_string() + } } fn sap_hana_jdbc_connection_string(config: &ConnectionConfig, host: &str, port: u16, database: &str) -> String { @@ -575,7 +688,7 @@ mod tests { } #[test] - fn oceanbase_oracle_uses_oracle_jdbc_connection_string_for_agent_protocol() { + fn oceanbase_oracle_uses_oceanbase_jdbc_connection_string_for_agent_protocol() { let mut cfg = config(DatabaseType::OceanbaseOracle, Some("sys")); cfg.host = "oceanbase.example.com".to_string(); cfg.port = 2881; @@ -584,7 +697,19 @@ mod tests { assert_eq!(params["database"], "sys"); assert_eq!(params["sysdba"], false); - assert_eq!(params["connection_string"], "jdbc:oracle:thin:@//oceanbase.example.com:2881/sys"); + assert_eq!(params["connection_string"], "jdbc:oceanbase://oceanbase.example.com:2881/sys"); + } + + #[test] + fn oceanbase_oracle_jdbc_url_appends_params_and_rewrites_forwarded_host() { + let mut cfg = config(DatabaseType::OceanbaseOracle, Some("sys")); + cfg.host = "oceanbase.example.com".to_string(); + cfg.port = 2881; + cfg.url_params = Some("useSSL=false".to_string()); + + let params = agent_connect_params(&cfg, "127.0.0.1", 12881, "sys"); + + assert_eq!(params["connection_string"], "jdbc:oceanbase://127.0.0.1:12881/sys?useSSL=false"); } #[test] @@ -631,9 +756,9 @@ mod tests { let retry = oracle_alternate_connect_config(&cfg, "ORA-12514: listener does not know service").unwrap(); - assert_eq!(retry.oracle_connection_type.as_deref(), Some("sid")); + assert_eq!(retry.connection_string.as_deref(), Some("jdbc:oracle:thin:@127.0.0.1:3306:ORCL")); assert!(oracle_alternate_connect_config(&retry, "ORA-01017: invalid username/password").is_none()); - assert!(oracle_alternate_connect_config(&cfg, "ORA-12541: TNS:no listener").is_none()); + assert!(oracle_alternate_connect_config(&cfg, "ORA-12541: TNS:no listener").is_some()); } #[test] @@ -666,6 +791,28 @@ mod tests { assert!(oracle_alternate_connect_config(&cfg, "ORA-12514: listener does not know service").is_none()); } + #[test] + fn oracle_no_listener_errors_try_common_jdbc_url_variants() { + let mut cfg = config(DatabaseType::Oracle, Some("ORCL")); + cfg.host = "oracle.example.com".to_string(); + cfg.port = 1521; + cfg.driver_profile = Some("oracle".to_string()); + cfg.oracle_connection_type = Some("service_name".to_string()); + + let retries = oracle_alternate_connect_configs(&cfg, "ORA-12541: TNS:no listener"); + let urls: Vec<_> = retries.iter().filter_map(|retry| retry.connection_string.as_deref()).collect(); + + assert_eq!( + urls, + vec![ + "jdbc:oracle:thin:@oracle.example.com:1521:ORCL", + "jdbc:oracle:thin:@oracle.example.com:1521/ORCL", + "jdbc:oracle:thin:@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=oracle.example.com)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=ORCL)))", + "jdbc:oracle:thin:@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=oracle.example.com)(PORT=1521))(CONNECT_DATA=(SID=ORCL)))", + ] + ); + } + #[test] fn sap_hana_url_includes_selected_database_and_params() { let mut cfg = config(DatabaseType::SapHana, Some("TENANT1")); diff --git a/crates/dbx-core/src/connection.rs b/crates/dbx-core/src/connection.rs index bb8467671e..fdb0707ca4 100644 --- a/crates/dbx-core/src/connection.rs +++ b/crates/dbx-core/src/connection.rs @@ -8,8 +8,8 @@ use mysql_async::Row as MysqlRow; use crate::agent_connection::{ agent_connect_params, h2_file_path_from_jdbc_url, is_h2_file_connection, mongo_legacy_error_with_auth_hint, - oracle_alternate_connect_config, oracle_auth_fallback_profiles, oracle_error_with_driver_hint, - should_retry_oracle_with_10g_driver, + oracle_alternate_connect_config_labels, oracle_alternate_connect_configs, oracle_auth_fallback_profiles, + oracle_error_with_driver_hint, should_retry_oracle_with_10g_driver, }; use crate::agent_manager::{JavaRuntimeMode, DEFAULT_JRE_KEY}; use crate::database_capabilities; @@ -553,27 +553,48 @@ impl AppState { let connect_result = client.call_method::(AgentMethod::Connect, connect_params.clone()).await; if let Err(err) = connect_result { - if let Some(alternate_config) = oracle_alternate_connect_config(&db_config, &err) { + let alternate_configs = oracle_alternate_connect_configs(&db_config, &err); + if !alternate_configs.is_empty() { log::warn!( - "Oracle connect failed with {:?} descriptor: {}. Retrying with {:?} descriptor.", + "Oracle connect failed with {:?} descriptor: {}. Retrying with Oracle JDBC URL variants: {:?}.", db_config.oracle_connection_type, err, - alternate_config.oracle_connection_type + oracle_alternate_connect_config_labels(&alternate_configs) ); - client - .call_method::( - AgentMethod::Connect, - agent_connect_params( - &alternate_config, - &host, - port, - alternate_config.effective_database().unwrap_or(""), - ), - ) - .await - .map_err(|alternate_err| { - format!("{err}\n\nFallback with alternate Oracle descriptor failed: {alternate_err}") - })?; + let mut fallback_errors = Vec::new(); + let mut connected = false; + for alternate_config in alternate_configs { + let label = oracle_alternate_connect_config_labels(std::slice::from_ref(&alternate_config)) + .into_iter() + .next() + .unwrap_or_else(|| "alternate".to_string()); + match client + .call_method::( + AgentMethod::Connect, + agent_connect_params( + &alternate_config, + &host, + port, + alternate_config.effective_database().unwrap_or(""), + ), + ) + .await + { + Ok(_) => { + connected = true; + break; + } + Err(alternate_err) => { + fallback_errors.push(format!("{label}: {alternate_err}")); + } + } + } + if !connected { + return Err(format!( + "{err}\n\nFallback with alternate Oracle JDBC URLs failed: {}", + fallback_errors.join("\n") + )); + } } else if should_retry_oracle_with_10g_driver(&db_config, &err) { log::warn!( "Oracle connect failed with profile {:?}: {}. Retrying with legacy Oracle profiles.", @@ -1480,16 +1501,18 @@ mod tests { ) .expect("listener errors should allow alternate descriptor retry"); assert_eq!(retry.driver_profile.as_deref(), Some("oracle")); - assert_eq!(retry.oracle_connection_type.as_deref(), Some("sid")); + assert_eq!(retry.connection_string.as_deref(), Some("jdbc:oracle:thin:@127.0.0.1:3306:ORCL")); + let mut sid_config = config.clone(); + sid_config.oracle_connection_type = Some("sid".to_string()); let service_retry = oracle_alternate_connect_config( - &retry, + &sid_config, "Agent RPC error (-1): ORA-12505: listener does not currently know of SID given", ) .expect("SID listener errors should allow service-name retry"); - assert_eq!(service_retry.oracle_connection_type.as_deref(), Some("service_name")); + assert_eq!(service_retry.connection_string.as_deref(), Some("jdbc:oracle:thin:@//127.0.0.1:3306/ORCL")); - assert!(oracle_alternate_connect_config(&config, "ORA-12541: TNS:no listener").is_none()); + assert!(oracle_alternate_connect_config(&config, "ORA-12541: TNS:no listener").is_some()); } #[test]