From 234099aebd68d73fc4e840e23a55cd78ed45a5fb Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Mon, 25 May 2026 19:07:30 +0800 Subject: [PATCH] feat: use native postgres path for GaussDB connections --- Cargo.lock | 8 +- Cargo.toml | 5 + .../assets/database-drivers.manifest.json | 14 +- crates/dbx-core/src/agent_service.rs | 2 - crates/dbx-core/src/connection.rs | 173 +++++++++++++++++- crates/dbx-core/src/database_capabilities.rs | 2 - crates/dbx-core/src/db/postgres.rs | 1 + .../dbx-core/tests/database_capabilities.rs | 7 +- src-tauri/src/commands/connection.rs | 18 +- 9 files changed, 196 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fa59c3513..24a0f63c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5519,8 +5519,6 @@ dependencies = [ [[package]] name = "postgres-protocol" version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56201207dac53e2f38e848e31b4b91616a6bb6e0c7205b77718994a7f49e70fc" dependencies = [ "base64 0.22.1", "byteorder", @@ -5529,7 +5527,9 @@ dependencies = [ "hmac 0.13.0", "md-5 0.11.0", "memchr", + "pbkdf2 0.12.2", "rand 0.10.1", + "sha1 0.10.6", "sha2 0.11.0", "stringprep", ] @@ -5537,8 +5537,6 @@ dependencies = [ [[package]] name = "postgres-types" version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dc729a129e682e8d24170cd30ae1aa01b336b096cbb56df6d534ffec133d186" dependencies = [ "bytes", "chrono", @@ -8210,8 +8208,6 @@ dependencies = [ [[package]] name = "tokio-postgres" version = "0.7.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dd8df5ef180f6364759a6f00f7aadda4fbbac86cdee37480826a6ff9f3574ce" dependencies = [ "async-trait", "byteorder", diff --git a/Cargo.toml b/Cargo.toml index 52072b2d7..674fd7ebd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,11 @@ resolver = "2" members = ["src-tauri", "crates/dbx-core", "crates/dbx-web"] +[patch.crates-io] +tokio-postgres = { path = "../tokio-postgres-gaussdb/tokio-postgres" } +postgres-types = { path = "../tokio-postgres-gaussdb/postgres-types" } +postgres-protocol = { path = "../tokio-postgres-gaussdb/postgres-protocol" } + [profile.release] panic = "abort" strip = true diff --git a/crates/dbx-core/assets/database-drivers.manifest.json b/crates/dbx-core/assets/database-drivers.manifest.json index 3cdf61b4b..138b1a005 100644 --- a/crates/dbx-core/assets/database-drivers.manifest.json +++ b/crates/dbx-core/assets/database-drivers.manifest.json @@ -192,12 +192,11 @@ { "dbType": "gaussdb", "label": "GaussDB", - "runtimeMode": "agent", - "mcpMode": "bridge", - "agentKey": "gaussdb", + "runtimeMode": "native", + "mcpMode": "direct", "singleConnectionPool": false, "metadataConnectionScoped": false, - "skipTcpProbe": true, + "skipTcpProbe": false, "defaultPort": 8000 }, { @@ -280,12 +279,11 @@ { "dbType": "opengauss", "label": "openGauss", - "runtimeMode": "agent", - "mcpMode": "bridge", - "agentKey": "opengauss", + "runtimeMode": "native", + "mcpMode": "direct", "singleConnectionPool": false, "metadataConnectionScoped": false, - "skipTcpProbe": true, + "skipTcpProbe": false, "defaultPort": 5432 }, { diff --git a/crates/dbx-core/src/agent_service.rs b/crates/dbx-core/src/agent_service.rs index 9c8b28b9e..bd5069938 100644 --- a/crates/dbx-core/src/agent_service.rs +++ b/crates/dbx-core/src/agent_service.rs @@ -23,7 +23,6 @@ pub const AGENT_TYPES: &[(&str, &str)] = &[ ("vertica", "Vertica"), ("firebird", "Firebird"), ("exasol", "Exasol"), - ("opengauss", "openGauss"), ("oceanbase-oracle", "OceanBase Oracle Mode"), ("gbase", "GBase"), ("access", "Microsoft Access"), @@ -40,7 +39,6 @@ pub const AGENT_TYPES: &[(&str, &str)] = &[ ("bigquery", "Google BigQuery"), ("kylin", "Apache Kylin"), ("sundb", "SunDB"), - ("gaussdb", "GaussDB"), ("yashandb", "崖山 YashanDB"), ("tdengine", "TDengine"), ("mongodb", "MongoDB (Legacy)"), diff --git a/crates/dbx-core/src/connection.rs b/crates/dbx-core/src/connection.rs index cedfce964..6b1abfb88 100644 --- a/crates/dbx-core/src/connection.rs +++ b/crates/dbx-core/src/connection.rs @@ -190,7 +190,9 @@ impl AppState { DatabaseType::Doris | DatabaseType::StarRocks => { PoolKind::Mysql(db::mysql::connect_bare(&url).await?, MysqlMode::Bare) } - DatabaseType::Postgres | DatabaseType::Redshift => PoolKind::Postgres(db::postgres::connect(&url).await?), + DatabaseType::Postgres | DatabaseType::Redshift | DatabaseType::Gaussdb | DatabaseType::OpenGauss => { + PoolKind::Postgres(db::postgres::connect(&url).await?) + } DatabaseType::Sqlite => PoolKind::Sqlite(db::sqlite::connect_path(&expand_tilde(&db_config.host)).await?), DatabaseType::Redis => { let con = db::redis_driver::connect(&url).await?; @@ -268,7 +270,6 @@ impl AppState { | DatabaseType::Vertica | DatabaseType::Firebird | DatabaseType::Exasol - | DatabaseType::OpenGauss | DatabaseType::OceanbaseOracle | DatabaseType::Gbase | DatabaseType::Oracle @@ -284,8 +285,7 @@ impl AppState { | DatabaseType::Kylin | DatabaseType::Sundb | DatabaseType::Tdengine - | DatabaseType::Access - | DatabaseType::Gaussdb => { + | DatabaseType::Access => { let connect_params = agent_connect_params(&db_config, &host, port, db_config.effective_database().unwrap_or("")); let mut client = @@ -586,6 +586,8 @@ fn default_dbx_dir() -> PathBuf { } pub fn connection_url_for_endpoint(config: &ConnectionConfig, host: &str, port: u16) -> String { + let normalized = native_postgres_url_config(config); + let config = normalized.as_ref().unwrap_or(config); if host == config.host && port == config.port { config.connection_url() } else { @@ -594,6 +596,8 @@ pub fn connection_url_for_endpoint(config: &ConnectionConfig, host: &str, port: } pub fn redacted_connection_url_for_endpoint(config: &ConnectionConfig, host: &str, port: u16) -> String { + let normalized = native_postgres_url_config(config); + let config = normalized.as_ref().unwrap_or(config); if host == config.host && port == config.port { config.redacted_connection_url() } else { @@ -601,6 +605,27 @@ pub fn redacted_connection_url_for_endpoint(config: &ConnectionConfig, host: &st } } +fn native_postgres_url_config(config: &ConnectionConfig) -> Option { + match config.db_type { + DatabaseType::Gaussdb | DatabaseType::OpenGauss => { + let mut normalized = config.clone(); + if config.db_type == DatabaseType::Gaussdb { + let params = normalized.url_params.as_deref().unwrap_or("").trim().trim_start_matches('?'); + if !params.to_lowercase().contains("sslmode=") { + normalized.url_params = Some(if params.is_empty() { + "sslmode=disable".to_string() + } else { + format!("sslmode=disable&{params}") + }); + } + } + normalized.db_type = DatabaseType::Postgres; + Some(normalized) + } + _ => None, + } +} + pub fn agent_connect_params(config: &ConnectionConfig, host: &str, port: u16, database: &str) -> serde_json::Value { let agent_database = if config.db_type == DatabaseType::MongoDb { mongo_agent_database(config, database) @@ -808,9 +833,10 @@ async fn detect_ob_oracle_mode(config: &ConnectionConfig, pool: &db::mysql::MySq #[cfg(test)] mod tests { use super::{ - agent_connect_params, database_connection_config, metadata_connection_config, uses_tcp_probe, AppState, - PoolKind, + agent_connect_params, connection_url_for_endpoint, database_connection_config, metadata_connection_config, + redacted_connection_url_for_endpoint, uses_tcp_probe, AppState, PoolKind, }; + use crate::db; use crate::models::connection::{ConnectionConfig, DatabaseType, ProxyType}; use crate::schema; use crate::storage::Storage; @@ -1029,6 +1055,37 @@ mod tests { (AppState::new(storage), dir) } + fn live_postgres_like_config( + db_type: DatabaseType, + host: &str, + port: u16, + username: &str, + password: &str, + url_params: Option<&str>, + ) -> ConnectionConfig { + let mut config = mysql_config(Some("postgres")); + config.db_type = db_type; + config.host = host.to_string(); + config.port = port; + config.username = username.to_string(); + config.password = password.to_string(); + config.url_params = url_params.map(str::to_string); + config + } + + async fn assert_live_postgres_like_query(config: ConnectionConfig) { + let url = connection_url_for_endpoint(&config, &config.host, config.port); + let pool = db::postgres::connect(&url).await.unwrap_or_else(|err| { + panic!("failed to connect to {:?} at {}:{}: {}", config.db_type, config.host, config.port, err) + }); + let result = + db::postgres::execute_query(&pool, "SELECT current_database(), current_schema()").await.unwrap_or_else( + |err| panic!("failed to query {:?} at {}:{}: {}", config.db_type, config.host, config.port, err), + ); + assert_eq!(result.rows.len(), 1); + pool.close(); + } + #[test] fn mysql_metadata_connection_ignores_saved_default_database() { let config = mysql_config(Some("app")); @@ -1058,6 +1115,64 @@ mod tests { assert_eq!(scoped.database.as_deref(), Some("analytics")); } + #[test] + fn gaussdb_endpoint_url_uses_postgres_scheme_for_native_driver() { + let mut config = mysql_config(Some("postgres")); + config.db_type = DatabaseType::Gaussdb; + config.username = "gaussdb".to_string(); + config.password = "secret".to_string(); + + assert_eq!( + connection_url_for_endpoint(&config, &config.host, config.port), + "postgres://gaussdb:secret@127.0.0.1:3306/postgres?sslmode=disable" + ); + assert_eq!( + redacted_connection_url_for_endpoint(&config, &config.host, config.port), + "postgres://127.0.0.1:3306/postgres?sslmode=disable" + ); + } + + #[test] + fn opengauss_endpoint_url_uses_postgres_scheme_for_native_driver() { + let mut config = mysql_config(Some("postgres")); + config.db_type = DatabaseType::OpenGauss; + config.username = "gaussdb".to_string(); + config.password = "secret".to_string(); + + assert_eq!( + connection_url_for_endpoint(&config, &config.host, config.port), + "postgres://gaussdb:secret@127.0.0.1:3306/postgres" + ); + } + + #[test] + fn gaussdb_endpoint_url_keeps_explicit_sslmode() { + let mut config = mysql_config(Some("postgres")); + config.db_type = DatabaseType::Gaussdb; + config.username = "gaussdb".to_string(); + config.password = "secret".to_string(); + config.url_params = Some("sslmode=require&application_name=dbx".to_string()); + + assert_eq!( + connection_url_for_endpoint(&config, &config.host, config.port), + "postgres://gaussdb:secret@127.0.0.1:3306/postgres?sslmode=require&application_name=dbx" + ); + } + + #[test] + fn gaussdb_endpoint_url_prepends_default_sslmode_to_custom_params() { + let mut config = mysql_config(Some("postgres")); + config.db_type = DatabaseType::Gaussdb; + config.username = "gaussdb".to_string(); + config.password = "secret".to_string(); + config.url_params = Some("application_name=dbx".to_string()); + + assert_eq!( + connection_url_for_endpoint(&config, &config.host, config.port), + "postgres://gaussdb:secret@127.0.0.1:3306/postgres?sslmode=disable&application_name=dbx" + ); + } + #[test] fn mongodb_database_connection_keeps_saved_database_for_auth() { let mut config = mysql_config(Some("admin")); @@ -1193,4 +1308,50 @@ mod tests { state.proxy_tunnels.stop_tunnel("proxied").await; let _ = std::fs::remove_dir_all(dir); } + + #[tokio::test] + #[ignore = "requires a reachable GaussDB instance via environment variables"] + async fn live_gaussdb_native_connection_succeeds() { + let host = std::env::var("DBX_TEST_GAUSSDB_HOST").expect("DBX_TEST_GAUSSDB_HOST not set"); + let port = std::env::var("DBX_TEST_GAUSSDB_PORT") + .expect("DBX_TEST_GAUSSDB_PORT not set") + .parse::() + .expect("DBX_TEST_GAUSSDB_PORT should be a u16"); + let username = std::env::var("DBX_TEST_GAUSSDB_USER").expect("DBX_TEST_GAUSSDB_USER not set"); + let password = std::env::var("DBX_TEST_GAUSSDB_PASSWORD").expect("DBX_TEST_GAUSSDB_PASSWORD not set"); + let url_params = std::env::var("DBX_TEST_GAUSSDB_URL_PARAMS").ok(); + + assert_live_postgres_like_query(live_postgres_like_config( + DatabaseType::Gaussdb, + &host, + port, + &username, + &password, + url_params.as_deref(), + )) + .await; + } + + #[tokio::test] + #[ignore = "requires a reachable openGauss instance via environment variables"] + async fn live_opengauss_native_connection_succeeds() { + let host = std::env::var("DBX_TEST_OPENGAUSS_HOST").expect("DBX_TEST_OPENGAUSS_HOST not set"); + let port = std::env::var("DBX_TEST_OPENGAUSS_PORT") + .expect("DBX_TEST_OPENGAUSS_PORT not set") + .parse::() + .expect("DBX_TEST_OPENGAUSS_PORT should be a u16"); + let username = std::env::var("DBX_TEST_OPENGAUSS_USER").expect("DBX_TEST_OPENGAUSS_USER not set"); + let password = std::env::var("DBX_TEST_OPENGAUSS_PASSWORD").expect("DBX_TEST_OPENGAUSS_PASSWORD not set"); + let url_params = std::env::var("DBX_TEST_OPENGAUSS_URL_PARAMS").ok(); + + assert_live_postgres_like_query(live_postgres_like_config( + DatabaseType::OpenGauss, + &host, + port, + &username, + &password, + url_params.as_deref(), + )) + .await; + } } diff --git a/crates/dbx-core/src/database_capabilities.rs b/crates/dbx-core/src/database_capabilities.rs index fe79f6e8d..60a992df5 100644 --- a/crates/dbx-core/src/database_capabilities.rs +++ b/crates/dbx-core/src/database_capabilities.rs @@ -14,7 +14,6 @@ pub fn agent_key(db_type: &DatabaseType, driver_profile: Option<&str>) -> Option DatabaseType::Vertica => Some("vertica"), DatabaseType::Firebird => Some("firebird"), DatabaseType::Exasol => Some("exasol"), - DatabaseType::OpenGauss => Some("opengauss"), DatabaseType::OceanbaseOracle => Some("oceanbase-oracle"), DatabaseType::Gbase => Some("gbase"), DatabaseType::Oracle => match driver_profile { @@ -32,7 +31,6 @@ pub fn agent_key(db_type: &DatabaseType, driver_profile: Option<&str>) -> Option DatabaseType::Bigquery => Some("bigquery"), DatabaseType::Kylin => Some("kylin"), DatabaseType::Sundb => Some("sundb"), - DatabaseType::Gaussdb => Some("gaussdb"), DatabaseType::MongoDb => Some("mongodb"), DatabaseType::Tdengine => Some("tdengine"), DatabaseType::Access => Some("access"), diff --git a/crates/dbx-core/src/db/postgres.rs b/crates/dbx-core/src/db/postgres.rs index 742cc6367..63043f8d2 100644 --- a/crates/dbx-core/src/db/postgres.rs +++ b/crates/dbx-core/src/db/postgres.rs @@ -122,6 +122,7 @@ fn pg_value_to_json(row: &Row, idx: usize, type_name: &str) -> serde_json::Value pub async fn connect(url: &str) -> Result { validate_postgres_ssl_paths(url)?; + let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); let tz = iana_time_zone::get_timezone().unwrap_or_else(|_| "UTC".to_string()); diff --git a/crates/dbx-core/tests/database_capabilities.rs b/crates/dbx-core/tests/database_capabilities.rs index 1a698d925..e87a47540 100644 --- a/crates/dbx-core/tests/database_capabilities.rs +++ b/crates/dbx-core/tests/database_capabilities.rs @@ -49,7 +49,6 @@ fn driver_manifest() -> DriverManifest { fn maps_agent_database_types_to_driver_keys() { assert_eq!(agent_key(&DatabaseType::Trino, None), Some("trino")); assert_eq!(agent_key(&DatabaseType::Hive, None), Some("hive")); - assert_eq!(agent_key(&DatabaseType::Gaussdb, None), Some("gaussdb")); assert_eq!(agent_key(&DatabaseType::Tdengine, None), Some("tdengine")); assert_eq!(agent_key(&DatabaseType::Yashandb, None), Some("yashandb")); assert_eq!(agent_key(&DatabaseType::Databricks, None), Some("databricks")); @@ -58,7 +57,6 @@ fn maps_agent_database_types_to_driver_keys() { assert_eq!(agent_key(&DatabaseType::Vertica, None), Some("vertica")); assert_eq!(agent_key(&DatabaseType::Firebird, None), Some("firebird")); assert_eq!(agent_key(&DatabaseType::Exasol, None), Some("exasol")); - assert_eq!(agent_key(&DatabaseType::OpenGauss, None), Some("opengauss")); assert_eq!(agent_key(&DatabaseType::OceanbaseOracle, None), Some("oceanbase-oracle")); assert_eq!(agent_key(&DatabaseType::Gbase, None), Some("gbase")); assert_eq!(agent_key(&DatabaseType::Access, None), Some("access")); @@ -80,12 +78,13 @@ fn classifies_agent_database_types() { assert!(is_agent_type(&DatabaseType::Vertica)); assert!(is_agent_type(&DatabaseType::Firebird)); assert!(is_agent_type(&DatabaseType::Exasol)); - assert!(is_agent_type(&DatabaseType::OpenGauss)); assert!(is_agent_type(&DatabaseType::OceanbaseOracle)); assert!(is_agent_type(&DatabaseType::Gbase)); assert!(is_agent_type(&DatabaseType::Access)); assert!(!is_agent_type(&DatabaseType::Mysql)); assert!(!is_agent_type(&DatabaseType::Jdbc)); + assert!(!is_agent_type(&DatabaseType::Gaussdb)); + assert!(!is_agent_type(&DatabaseType::OpenGauss)); } #[test] @@ -128,6 +127,8 @@ fn skips_tcp_probe_for_local_file_plugin_and_agent_types() { assert!(skips_tcp_probe(&DatabaseType::Gbase)); assert!(!skips_tcp_probe(&DatabaseType::Postgres)); assert!(!skips_tcp_probe(&DatabaseType::Mysql)); + assert!(!skips_tcp_probe(&DatabaseType::Gaussdb)); + assert!(!skips_tcp_probe(&DatabaseType::OpenGauss)); } #[test] diff --git a/src-tauri/src/commands/connection.rs b/src-tauri/src/commands/connection.rs index 7830c2793..2e8278c0a 100644 --- a/src-tauri/src/commands/connection.rs +++ b/src-tauri/src/commands/connection.rs @@ -236,13 +236,15 @@ pub async fn test_connection(state: State<'_, Arc>, config: Connection } Err(e) => Err(e), }, - DatabaseType::Postgres | DatabaseType::Redshift => match db::postgres::connect(&url).await { - Ok(pool) => { - pool.close(); - Ok("Connection successful".to_string()) + DatabaseType::Postgres | DatabaseType::Redshift | DatabaseType::Gaussdb | DatabaseType::OpenGauss => { + match db::postgres::connect(&url).await { + Ok(pool) => { + pool.close(); + Ok("Connection successful".to_string()) + } + Err(e) => Err(e), } - Err(e) => Err(e), - }, + } DatabaseType::Sqlite => match db::sqlite::connect_path(&expand_tilde(&config.host)).await { Ok(_) => Ok("Connection successful".to_string()), Err(e) => Err(e), @@ -344,7 +346,9 @@ pub async fn connect_db(state: State<'_, Arc>, config: ConnectionConfi DatabaseType::Doris | DatabaseType::StarRocks => { PoolKind::Mysql(db::mysql::connect_bare(&url).await?, MysqlMode::Bare) } - DatabaseType::Postgres | DatabaseType::Redshift => PoolKind::Postgres(db::postgres::connect(&url).await?), + DatabaseType::Postgres | DatabaseType::Redshift | DatabaseType::Gaussdb | DatabaseType::OpenGauss => { + PoolKind::Postgres(db::postgres::connect(&url).await?) + } DatabaseType::Sqlite => PoolKind::Sqlite(db::sqlite::connect_path(&expand_tilde(&db_config.host)).await?), DatabaseType::Redis => { let con = db::redis_driver::connect(&url).await?;