From b79e5b53bcfa129b44a648219e9ef2f4f89a5287 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Thu, 7 May 2026 02:18:20 +0800 Subject: [PATCH] feat: add GaussDB ODBC support and flatten connection list - Add GaussDB/openGauss support via ODBC (resolves #106) - Use pg_catalog queries instead of information_schema for compatibility - Share ODBC environment across drivers with auto ODBCSYSINI detection - Fix ODBC error message false positive on "Driver" keyword match - Merge connection dialog categories into a single flat list - Remove duplicate DDL button from data grid footer --- crates/dbx-core/src/connection.rs | 21 +- crates/dbx-core/src/db/dm_driver.rs | 9 +- crates/dbx-core/src/db/gaussdb_driver.rs | 342 ++++++++++++++++++ crates/dbx-core/src/db/mod.rs | 14 + crates/dbx-core/src/models/connection.rs | 5 + crates/dbx-core/src/query.rs | 14 + crates/dbx-core/src/schema.rs | 101 ++++++ src-tauri/src/commands/connection.rs | 21 ++ .../connection/ConnectionDialog.vue | 34 +- .../diagram/SchemaDiagramDialog.vue | 13 +- src/components/diff/SchemaDiffDialog.vue | 3 +- src/components/grid/DataGrid.vue | 3 - .../search/DatabaseSearchDialog.vue | 2 +- src/components/sidebar/TreeItem.vue | 26 +- .../transfer/DataTransferDialog.vue | 7 +- src/composables/useSchemaOptions.ts | 8 +- src/i18n/locales/en.ts | 2 + src/i18n/locales/zh-CN.ts | 2 + src/lib/tableSelectSql.ts | 3 +- src/stores/connectionStore.ts | 9 +- src/types/database.ts | 3 +- 21 files changed, 602 insertions(+), 40 deletions(-) create mode 100644 crates/dbx-core/src/db/gaussdb_driver.rs diff --git a/crates/dbx-core/src/connection.rs b/crates/dbx-core/src/connection.rs index 7c69994c7..26c1a4622 100644 --- a/crates/dbx-core/src/connection.rs +++ b/crates/dbx-core/src/connection.rs @@ -29,6 +29,7 @@ pub enum PoolKind { Oracle(Arc>), Elasticsearch(db::elasticsearch_driver::EsClient), Dameng(Arc>), + Gaussdb(Arc>), } pub struct AppState { @@ -61,7 +62,8 @@ impl AppState { return Ok(connection_id.to_string()); } - let is_single_conn = matches!(db_type, Some(DatabaseType::Oracle) | Some(DatabaseType::Dameng)); + let is_single_conn = + matches!(db_type, Some(DatabaseType::Oracle) | Some(DatabaseType::Dameng) | Some(DatabaseType::Gaussdb)); let pool_key = if is_single_conn { connection_id.to_string() } else { @@ -96,7 +98,10 @@ impl AppState { let mut db_config = config.clone(); if let Some(db) = database { - if db_config.db_type != DatabaseType::Oracle && db_config.db_type != DatabaseType::Dameng { + if db_config.db_type != DatabaseType::Oracle + && db_config.db_type != DatabaseType::Dameng + && db_config.db_type != DatabaseType::Gaussdb + { db_config.database = Some(db.to_string()); } } @@ -174,6 +179,17 @@ impl AppState { .await?; PoolKind::Dameng(Arc::new(std::sync::Mutex::new(client))) } + DatabaseType::Gaussdb => { + let client = db::gaussdb_driver::connect( + &host, + port, + db_config.database.as_deref().unwrap_or(""), + &db_config.username, + &db_config.password, + ) + .await?; + PoolKind::Gaussdb(Arc::new(std::sync::Mutex::new(client))) + } }; self.connections.lock().await.insert(pool_key.clone(), pool); @@ -221,6 +237,7 @@ impl AppState { c.db_type == DatabaseType::Oracle || c.db_type == DatabaseType::Elasticsearch || c.db_type == DatabaseType::Dameng + || c.db_type == DatabaseType::Gaussdb }) .unwrap_or(false) }; diff --git a/crates/dbx-core/src/db/dm_driver.rs b/crates/dbx-core/src/db/dm_driver.rs index 463e8a12e..a5dec78b1 100644 --- a/crates/dbx-core/src/db/dm_driver.rs +++ b/crates/dbx-core/src/db/dm_driver.rs @@ -1,14 +1,11 @@ use std::time::Instant; -use odbc_api::{buffers::TextRowSet, ConnectionOptions, Cursor, Environment, ResultSetMetadata}; +use odbc_api::{buffers::TextRowSet, ConnectionOptions, Cursor, ResultSetMetadata}; use crate::types::{ColumnInfo, DatabaseInfo, ForeignKeyInfo, IndexInfo, QueryResult, TableInfo, TriggerInfo}; use super::CONNECTION_TIMEOUT_SECS; -static ENV: std::sync::LazyLock = - std::sync::LazyLock::new(|| Environment::new().expect("Failed to initialize ODBC environment")); - pub struct DmClient { conn: odbc_api::Connection<'static>, } @@ -60,10 +57,10 @@ pub async fn connect(host: &str, port: u16, database: &str, user: &str, pass: &s let result = tokio::time::timeout( std::time::Duration::from_secs(CONNECTION_TIMEOUT_SECS), tokio::task::spawn_blocking(move || { - ENV.connect_with_connection_string(&conn_str, ConnectionOptions::default()) + super::ODBC_ENV.connect_with_connection_string(&conn_str, ConnectionOptions::default()) .map_err(|e| { let msg = e.to_string(); - if msg.contains("Data source name not found") || msg.contains("Driver") { + if msg.contains("Data source name not found") || msg.contains("Can't open lib") { format!( "DM8 ODBC driver not found. Please install the DM8 ODBC driver \ and register it in odbcinst.ini (Linux/macOS) or the ODBC Data Source Administrator (Windows). \ diff --git a/crates/dbx-core/src/db/gaussdb_driver.rs b/crates/dbx-core/src/db/gaussdb_driver.rs new file mode 100644 index 000000000..b0e6c36f3 --- /dev/null +++ b/crates/dbx-core/src/db/gaussdb_driver.rs @@ -0,0 +1,342 @@ +use std::time::Instant; + +use odbc_api::{buffers::TextRowSet, ConnectionOptions, Cursor, ResultSetMetadata}; + +use crate::types::{ColumnInfo, DatabaseInfo, ForeignKeyInfo, IndexInfo, QueryResult, TableInfo, TriggerInfo}; + +use super::CONNECTION_TIMEOUT_SECS; + +pub struct GaussdbClient { + conn: odbc_api::Connection<'static>, +} + +unsafe impl Send for GaussdbClient {} + +impl GaussdbClient { + pub fn query_rows(&self, sql: &str) -> Result>, String> { + match self.conn.execute(sql, (), None).map_err(|e| e.to_string())? { + Some(cursor) => read_cursor(cursor), + None => Ok(vec![]), + } + } + + fn query_single_column(&self, sql: &str) -> Result, String> { + Ok(self.query_rows(sql)?.into_iter().filter_map(|r| r.into_iter().next()).collect()) + } +} + +fn read_cursor(cursor: impl Cursor) -> Result>, String> { + let mut cursor = cursor; + let col_count = cursor.num_result_cols().map_err(|e| e.to_string())? as u16; + let buffer = TextRowSet::for_cursor(1000, &mut cursor, Some(8192)).map_err(|e| e.to_string())?; + let mut row_cursor = cursor.bind_buffer(buffer).map_err(|e| e.to_string())?; + let mut rows = Vec::new(); + while let Some(batch) = row_cursor.fetch().map_err(|e| e.to_string())? { + for row_idx in 0..batch.num_rows() { + let vals: Vec = (0..col_count as usize) + .map(|col| { + batch.at(col, row_idx).and_then(|bytes| std::str::from_utf8(bytes).ok()).unwrap_or("").to_string() + }) + .collect(); + rows.push(vals); + } + } + Ok(rows) +} + +pub async fn connect(host: &str, port: u16, database: &str, user: &str, pass: &str) -> Result { + let conn_str = format!( + "Driver={{GaussDBA}};Servername={host};Port={port};Database={db};UID={user};PWD={pass}", + host = host, + port = port, + db = database, + user = user, + pass = pass, + ); + + let result = tokio::time::timeout( + std::time::Duration::from_secs(CONNECTION_TIMEOUT_SECS), + tokio::task::spawn_blocking(move || { + super::ODBC_ENV.connect_with_connection_string(&conn_str, ConnectionOptions::default()) + .map_err(|e| { + let msg = e.to_string(); + if msg.contains("Data source name not found") || msg.contains("Can't open lib") { + format!( + "GaussDB ODBC driver not found. Please install the GaussDB ODBC driver \ + and register it in odbcinst.ini (Linux/macOS) or the ODBC Data Source Administrator (Windows). \ + Original error: {msg}" + ) + } else { + format!("GaussDB connection failed: {msg}") + } + }) + .map(|conn| GaussdbClient { conn }) + }), + ) + .await + .map_err(|_| format!("GaussDB connection timed out ({CONNECTION_TIMEOUT_SECS}s)"))? + .map_err(|e| format!("GaussDB connection task failed: {e}"))?; + + result +} + +pub fn list_databases(client: &GaussdbClient) -> Result, String> { + let rows = + client.query_single_column("SELECT datname FROM pg_database WHERE datistemplate = false ORDER BY datname")?; + Ok(rows.into_iter().map(|name| DatabaseInfo { name }).collect()) +} + +pub fn list_schemas(client: &GaussdbClient) -> Result, String> { + client.query_single_column( + "SELECT DISTINCT nspname FROM pg_catalog.pg_namespace n \ + WHERE nspname NOT LIKE 'pg_%' \ + AND nspname NOT IN ('information_schema', 'cstore', 'snapshot', 'db4ai', 'dbe_perf', \ + 'dbe_pldebugger', 'dbe_pldeveloper', 'pkg_service', 'pkg_util', 'sqladvisor', 'blockchain') \ + AND EXISTS (SELECT 1 FROM pg_catalog.pg_class c WHERE c.relnamespace = n.oid AND c.relkind IN ('r', 'v', 'm')) \ + ORDER BY nspname", + ) +} + +pub fn list_tables(client: &GaussdbClient, schema: &str) -> Result, String> { + let s = schema.replace('\'', "''"); + let sql = format!( + "SELECT c.relname, CASE c.relkind WHEN 'r' THEN 'TABLE' WHEN 'v' THEN 'VIEW' WHEN 'm' THEN 'VIEW' ELSE 'TABLE' END \ + FROM pg_catalog.pg_class c \ + JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace \ + WHERE n.nspname = '{s}' AND c.relkind IN ('r', 'v', 'm') \ + ORDER BY c.relname" + ); + let rows = client.query_rows(&sql)?; + Ok(rows + .into_iter() + .map(|r| { + let raw_type = r.get(1).cloned().unwrap_or_default(); + TableInfo { + name: r.first().cloned().unwrap_or_default(), + table_type: if raw_type.contains("VIEW") { "VIEW".to_string() } else { "TABLE".to_string() }, + } + }) + .collect()) +} + +pub fn get_columns(client: &GaussdbClient, schema: &str, table: &str) -> Result, String> { + let s = schema.replace('\'', "''"); + let t = table.replace('\'', "''"); + + let pk_rows = client.query_single_column(&format!( + "SELECT a.attname FROM pg_catalog.pg_index i \ + JOIN pg_catalog.pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) \ + WHERE i.indrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname = '{t}' \ + AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = '{s}')) \ + AND i.indisprimary" + ))?; + let pk_names: std::collections::HashSet = pk_rows.into_iter().collect(); + + let col_rows = client.query_rows(&format!( + "SELECT a.attname, format_type(a.atttypid, a.atttypmod), \ + CASE WHEN a.attnotnull THEN 'NO' ELSE 'YES' END, \ + pg_catalog.pg_get_expr(d.adbin, d.adrelid), \ + CASE WHEN t.typname IN ('numeric', 'float4', 'float8') THEN COALESCE(((a.atttypmod - 4) >> 16) & 65535, -1) ELSE NULL END, \ + CASE WHEN t.typname = 'numeric' THEN COALESCE((a.atttypmod - 4) & 65535, -1) ELSE NULL END, \ + CASE WHEN a.atttypmod > 0 AND t.typname IN ('varchar', 'bpchar') THEN a.atttypmod - 4 ELSE NULL END \ + FROM pg_catalog.pg_attribute a \ + JOIN pg_catalog.pg_type t ON t.oid = a.atttypid \ + LEFT JOIN pg_catalog.pg_attrdef d ON d.adrelid = a.attrelid AND d.adnum = a.attnum \ + WHERE a.attrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname = '{t}' \ + AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = '{s}')) \ + AND a.attnum > 0 AND NOT a.attisdropped \ + ORDER BY a.attnum" + ))?; + + Ok(col_rows + .into_iter() + .map(|r| { + let name = r.first().cloned().unwrap_or_default(); + let data_type = r.get(1).cloned().unwrap_or_default(); + let num_prec = r.get(4).and_then(|v| v.parse::().ok()); + let num_scale = r.get(5).and_then(|v| v.parse::().ok()); + let char_len = r.get(6).and_then(|v| v.parse::().ok()); + ColumnInfo { + is_primary_key: pk_names.contains(&name), + name, + data_type, + is_nullable: r.get(2).map(|v| v == "YES").unwrap_or(false), + column_default: r.get(3).filter(|v| !v.is_empty()).cloned(), + extra: None, + comment: None, + numeric_precision: num_prec, + numeric_scale: num_scale, + character_maximum_length: char_len, + } + }) + .collect()) +} + +pub fn list_indexes(client: &GaussdbClient, schema: &str, table: &str) -> Result, String> { + let s = schema.replace('\'', "''"); + let t = table.replace('\'', "''"); + let sql = format!( + "SELECT indexname, indexdef FROM pg_indexes WHERE schemaname = '{s}' AND tablename = '{t}' ORDER BY indexname" + ); + let rows = client.query_rows(&sql)?; + Ok(rows + .into_iter() + .map(|r| { + let name = r.first().cloned().unwrap_or_default(); + let def = r.get(1).cloned().unwrap_or_default(); + let is_unique = def.to_uppercase().contains("UNIQUE"); + let is_primary = def.to_uppercase().contains("PRIMARY"); + let columns = def + .rsplit_once('(') + .and_then(|(_, rest)| rest.strip_suffix(')')) + .map(|cols| cols.split(',').map(|c| c.trim().trim_matches('"').to_string()).collect()) + .unwrap_or_default(); + IndexInfo { + name, + columns, + is_unique, + is_primary, + filter: None, + index_type: None, + included_columns: None, + comment: None, + } + }) + .collect()) +} + +pub fn list_foreign_keys(client: &GaussdbClient, schema: &str, table: &str) -> Result, String> { + let s = schema.replace('\'', "''"); + let t = table.replace('\'', "''"); + let sql = format!( + "SELECT con.conname, a.attname, cl2.relname, a2.attname \ + FROM pg_catalog.pg_constraint con \ + JOIN pg_catalog.pg_class cl ON cl.oid = con.conrelid \ + JOIN pg_catalog.pg_namespace n ON n.oid = cl.relnamespace \ + JOIN pg_catalog.pg_attribute a ON a.attrelid = con.conrelid AND a.attnum = ANY(con.conkey) \ + JOIN pg_catalog.pg_class cl2 ON cl2.oid = con.confrelid \ + JOIN pg_catalog.pg_attribute a2 ON a2.attrelid = con.confrelid AND a2.attnum = ANY(con.confkey) \ + WHERE con.contype = 'f' AND n.nspname = '{s}' AND cl.relname = '{t}' \ + ORDER BY con.conname" + ); + let rows = client.query_rows(&sql)?; + Ok(rows + .into_iter() + .map(|r| ForeignKeyInfo { + name: r.first().cloned().unwrap_or_default(), + column: r.get(1).cloned().unwrap_or_default(), + ref_table: r.get(2).cloned().unwrap_or_default(), + ref_column: r.get(3).cloned().unwrap_or_default(), + }) + .collect()) +} + +pub fn list_triggers(client: &GaussdbClient, schema: &str, table: &str) -> Result, String> { + let s = schema.replace('\'', "''"); + let t = table.replace('\'', "''"); + let sql = format!( + "SELECT t.tgname, em.event, CASE WHEN t.tgtype & 2 = 2 THEN 'BEFORE' ELSE 'AFTER' END \ + FROM pg_catalog.pg_trigger t \ + JOIN pg_catalog.pg_class c ON c.oid = t.tgrelid \ + JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace \ + CROSS JOIN LATERAL ( \ + SELECT CASE \ + WHEN t.tgtype & 4 = 4 THEN 'INSERT' \ + WHEN t.tgtype & 8 = 8 THEN 'DELETE' \ + WHEN t.tgtype & 16 = 16 THEN 'UPDATE' \ + ELSE 'UNKNOWN' END AS event \ + ) em \ + WHERE NOT t.tgisinternal AND n.nspname = '{s}' AND c.relname = '{t}' \ + ORDER BY t.tgname" + ); + let rows = client.query_rows(&sql)?; + Ok(rows + .into_iter() + .map(|r| TriggerInfo { + name: r.first().cloned().unwrap_or_default(), + event: r.get(1).cloned().unwrap_or_default(), + timing: r.get(2).cloned().unwrap_or_default(), + }) + .collect()) +} + +pub fn execute_query_sync(client: &GaussdbClient, sql: &str) -> Result { + let start = Instant::now(); + let sql = sql.trim().trim_end_matches(';'); + let trimmed = sql.to_uppercase(); + + if trimmed.starts_with("SELECT") + || trimmed.starts_with("WITH") + || trimmed.starts_with("SHOW") + || trimmed.starts_with("DESCRIBE") + || trimmed.starts_with("EXPLAIN") + { + match client.conn.execute(sql, (), None).map_err(|e| e.to_string())? { + Some(mut cursor) => { + let col_count = cursor.num_result_cols().map_err(|e| e.to_string())? as u16; + let columns: Vec = (1..=col_count) + .map(|i| cursor.col_name(i).map_err(|e| e.to_string()).unwrap_or_else(|_| format!("col{i}"))) + .collect(); + + let buffer = TextRowSet::for_cursor(1000, &mut cursor, Some(8192)).map_err(|e| e.to_string())?; + let mut row_cursor = cursor.bind_buffer(buffer).map_err(|e| e.to_string())?; + + let mut rows = Vec::new(); + while let Some(batch) = row_cursor.fetch().map_err(|e| e.to_string())? { + for row_idx in 0..batch.num_rows() { + let vals: Vec = (0..col_count as usize) + .map(|col| { + batch + .at(col, row_idx) + .and_then(|bytes| std::str::from_utf8(bytes).ok()) + .map(|s| serde_json::Value::String(s.to_string())) + .unwrap_or(serde_json::Value::Null) + }) + .collect(); + rows.push(vals); + if rows.len() >= crate::query::MAX_ROWS { + break; + } + } + if rows.len() >= crate::query::MAX_ROWS { + break; + } + } + + let truncated = rows.len() >= crate::query::MAX_ROWS; + Ok(QueryResult { + columns, + rows, + affected_rows: 0, + execution_time_ms: start.elapsed().as_millis(), + truncated, + }) + } + None => Ok(QueryResult { + columns: vec![], + rows: vec![], + affected_rows: 0, + execution_time_ms: start.elapsed().as_millis(), + truncated: false, + }), + } + } else { + match client.conn.execute(sql, (), None) { + Ok(Some(_cursor)) => Ok(QueryResult { + columns: vec![], + rows: vec![], + affected_rows: 0, + execution_time_ms: start.elapsed().as_millis(), + truncated: false, + }), + Ok(None) => Ok(QueryResult { + columns: vec![], + rows: vec![], + affected_rows: 0, + execution_time_ms: start.elapsed().as_millis(), + truncated: false, + }), + Err(e) => Err(e.to_string()), + } + } +} diff --git a/crates/dbx-core/src/db/mod.rs b/crates/dbx-core/src/db/mod.rs index da872f2bd..5b4092cc8 100644 --- a/crates/dbx-core/src/db/mod.rs +++ b/crates/dbx-core/src/db/mod.rs @@ -3,6 +3,7 @@ pub mod dm_driver; pub mod duckdb_driver; pub mod elasticsearch_driver; pub mod file_validator; +pub mod gaussdb_driver; pub mod mongo_driver; pub mod mysql; pub mod oracle_driver; @@ -22,6 +23,19 @@ pub use file_validator::validate_file_path; pub const CONNECTION_TIMEOUT_SECS: u64 = 5; pub const TCP_PROBE_TIMEOUT_SECS: u64 = 3; +pub static ODBC_ENV: std::sync::LazyLock = std::sync::LazyLock::new(|| { + if std::env::var("ODBCSYSINI").is_err() { + for dir in &["/opt/homebrew/etc", "/usr/local/etc", "/etc"] { + let path = std::path::Path::new(dir).join("odbcinst.ini"); + if path.exists() { + std::env::set_var("ODBCSYSINI", dir); + break; + } + } + } + odbc_api::Environment::new().expect("Failed to initialize ODBC environment") +}); + pub fn connection_timeout() -> Duration { Duration::from_secs(CONNECTION_TIMEOUT_SECS) } diff --git a/crates/dbx-core/src/models/connection.rs b/crates/dbx-core/src/models/connection.rs index fd76823ea..4d965791e 100644 --- a/crates/dbx-core/src/models/connection.rs +++ b/crates/dbx-core/src/models/connection.rs @@ -71,6 +71,7 @@ pub enum DatabaseType { StarRocks, Redshift, Dameng, + Gaussdb, } impl ConnectionConfig { @@ -130,6 +131,7 @@ impl ConnectionConfig { DatabaseType::Oracle => format!("oracle://{host}:{port}{db_part}"), DatabaseType::Elasticsearch => format!("http://{host}:{port}"), DatabaseType::Dameng => format!("dm://{host}:{port}{db_part}"), + DatabaseType::Gaussdb => format!("gaussdb://{host}:{port}{db_part}"), } } @@ -191,6 +193,9 @@ impl ConnectionConfig { DatabaseType::Dameng => { format!("dm://{}:{}@{host}:{port}{db_part}", username, password) } + DatabaseType::Gaussdb => { + format!("gaussdb://{}:{}@{host}:{port}{db_part}", username, password) + } } } diff --git a/crates/dbx-core/src/query.rs b/crates/dbx-core/src/query.rs index e56c40979..f4d215668 100644 --- a/crates/dbx-core/src/query.rs +++ b/crates/dbx-core/src/query.rs @@ -228,6 +228,20 @@ pub async fn do_execute( .await .map(truncate_result) } + PoolKind::Gaussdb(client) => { + let client = client.clone(); + let sql = sql.to_string(); + drop(connections); + wait_for_query(cancel_token, async move { + let task = tokio::task::spawn_blocking(move || { + let client = client.lock().map_err(|e| e.to_string())?; + db::gaussdb_driver::execute_query_sync(&client, &sql) + }); + task.await.map_err(|e| e.to_string())? + }) + .await + .map(truncate_result) + } } } diff --git a/crates/dbx-core/src/schema.rs b/crates/dbx-core/src/schema.rs index 5d7581d37..8e0f6d16f 100644 --- a/crates/dbx-core/src/schema.rs +++ b/crates/dbx-core/src/schema.rs @@ -110,6 +110,16 @@ pub fn extract_dameng( } } +pub fn extract_gaussdb( + connections: &HashMap, + key: &str, +) -> Option>> { + match connections.get(key)? { + PoolKind::Gaussdb(client) => Some(client.clone()), + _ => None, + } +} + pub async fn list_databases_core(state: &AppState, connection_id: &str) -> Result, String> { { let connections = state.connections.lock().await; @@ -132,6 +142,11 @@ pub async fn list_databases_core(state: &AppState, connection_id: &str) -> Resul let client = client.lock().map_err(|e| e.to_string())?; return db::dm_driver::list_databases(&client); } + if let Some(client) = extract_gaussdb(&connections, connection_id) { + drop(connections); + let client = client.lock().map_err(|e| e.to_string())?; + return db::gaussdb_driver::list_databases(&client); + } } let connections = state.connections.lock().await; @@ -166,6 +181,11 @@ pub async fn list_schemas_core(state: &AppState, connection_id: &str, database: let client = client.lock().map_err(|e| e.to_string())?; return db::dm_driver::list_schemas(&client); } + if let Some(client) = extract_gaussdb(&connections, &pool_key) { + drop(connections); + let client = client.lock().map_err(|e| e.to_string())?; + return db::gaussdb_driver::list_schemas(&client); + } } let connections = state.connections.lock().await; @@ -211,6 +231,11 @@ pub async fn list_tables_core( let client = client.lock().map_err(|e| e.to_string())?; return db::dm_driver::list_tables(&client, schema); } + if let Some(client) = extract_gaussdb(&connections, &pool_key) { + drop(connections); + let client = client.lock().map_err(|e| e.to_string())?; + return db::gaussdb_driver::list_tables(&client, schema); + } } let connections = state.connections.lock().await; @@ -259,6 +284,11 @@ pub async fn get_columns_core( let client = client.lock().map_err(|e| e.to_string())?; return db::dm_driver::get_columns(&client, schema, table); } + if let Some(client) = extract_gaussdb(&connections, &pool_key) { + drop(connections); + let client = client.lock().map_err(|e| e.to_string())?; + return db::gaussdb_driver::get_columns(&client, schema, table); + } } let connections = state.connections.lock().await; @@ -298,6 +328,11 @@ pub async fn list_indexes_core( let client = client.lock().map_err(|e| e.to_string())?; return db::dm_driver::list_indexes(&client, schema, table); } + if let Some(client) = extract_gaussdb(&connections, &pool_key) { + drop(connections); + let client = client.lock().map_err(|e| e.to_string())?; + return db::gaussdb_driver::list_indexes(&client, schema, table); + } } let connections = state.connections.lock().await; @@ -337,6 +372,11 @@ pub async fn list_foreign_keys_core( let client = client.lock().map_err(|e| e.to_string())?; return db::dm_driver::list_foreign_keys(&client, schema, table); } + if let Some(client) = extract_gaussdb(&connections, &pool_key) { + drop(connections); + let client = client.lock().map_err(|e| e.to_string())?; + return db::gaussdb_driver::list_foreign_keys(&client, schema, table); + } } let connections = state.connections.lock().await; @@ -376,6 +416,11 @@ pub async fn list_triggers_core( let client = client.lock().map_err(|e| e.to_string())?; return db::dm_driver::list_triggers(&client, schema, table); } + if let Some(client) = extract_gaussdb(&connections, &pool_key) { + drop(connections); + let client = client.lock().map_err(|e| e.to_string())?; + return db::gaussdb_driver::list_triggers(&client, schema, table); + } } let connections = state.connections.lock().await; @@ -441,6 +486,11 @@ pub async fn get_table_ddl_core( let client = client.lock().map_err(|e| e.to_string())?; return build_dameng_ddl(&client, schema, table); } + if let Some(client) = extract_gaussdb(&connections, &pool_key) { + drop(connections); + let client = client.lock().map_err(|e| e.to_string())?; + return build_gaussdb_ddl(&client, schema, table); + } } let connections = state.connections.lock().await; @@ -695,3 +745,54 @@ pub fn build_dameng_ddl(client: &db::dm_driver::DmClient, schema: &str, table: & } Ok(ddl) } + +pub fn build_gaussdb_ddl( + client: &db::gaussdb_driver::GaussdbClient, + schema: &str, + table: &str, +) -> Result { + let columns = db::gaussdb_driver::get_columns(client, schema, table)?; + let indexes = db::gaussdb_driver::list_indexes(client, schema, table)?; + let fkeys = db::gaussdb_driver::list_foreign_keys(client, schema, table)?; + + let mut ddl = format!("CREATE TABLE \"{schema}\".\"{table}\" (\n"); + let col_lines: Vec = columns + .iter() + .map(|c| { + let mut line = format!(" \"{}\" {}", c.name, c.data_type); + if !c.is_nullable { + line.push_str(" NOT NULL"); + } + if let Some(ref def) = c.column_default { + line.push_str(&format!(" DEFAULT {def}")); + } + line + }) + .collect(); + ddl.push_str(&col_lines.join(",\n")); + + let pks: Vec<&str> = columns.iter().filter(|c| c.is_primary_key).map(|c| c.name.as_str()).collect(); + if !pks.is_empty() { + ddl.push_str(&format!( + ",\n PRIMARY KEY ({})", + pks.iter().map(|k| format!("\"{k}\"")).collect::>().join(", ") + )); + } + for fk in &fkeys { + ddl.push_str(&format!( + ",\n CONSTRAINT \"{}\" FOREIGN KEY (\"{}\") REFERENCES \"{}\"(\"{}\")", + fk.name, fk.column, fk.ref_table, fk.ref_column + )); + } + ddl.push_str("\n);\n"); + + for idx in &indexes { + if idx.is_primary { + continue; + } + let unique = if idx.is_unique { "UNIQUE " } else { "" }; + let cols = idx.columns.iter().map(|c| format!("\"{c}\"")).collect::>().join(", "); + ddl.push_str(&format!("\nCREATE {unique}INDEX \"{}\" ON \"{schema}\".\"{table}\" ({cols});", idx.name)); + } + Ok(ddl) +} diff --git a/src-tauri/src/commands/connection.rs b/src-tauri/src/commands/connection.rs index 06079305b..8a0deca96 100644 --- a/src-tauri/src/commands/connection.rs +++ b/src-tauri/src/commands/connection.rs @@ -121,6 +121,15 @@ pub async fn test_connection(state: State<'_, Arc>, config: Connection ) .await .map(|_| "Connection successful".to_string()), + DatabaseType::Gaussdb => db::gaussdb_driver::connect( + &host, + port, + config.database.as_deref().unwrap_or(""), + &config.username, + &config.password, + ) + .await + .map(|_| "Connection successful".to_string()), }, }; @@ -199,6 +208,17 @@ pub async fn connect_db(state: State<'_, Arc>, config: ConnectionConfi .await?; PoolKind::Dameng(std::sync::Arc::new(std::sync::Mutex::new(client))) } + DatabaseType::Gaussdb => { + let client = db::gaussdb_driver::connect( + &host, + port, + config.database.as_deref().unwrap_or(""), + &config.username, + &config.password, + ) + .await?; + PoolKind::Gaussdb(std::sync::Arc::new(std::sync::Mutex::new(client))) + } }; state.connections.lock().await.insert(id.clone(), pool); @@ -226,6 +246,7 @@ pub async fn disconnect_db(state: State<'_, Arc>, connection_id: Strin PoolKind::Oracle(_) => {} PoolKind::Elasticsearch(_) => {} PoolKind::Dameng(_) => {} + PoolKind::Gaussdb(_) => {} } } } diff --git a/src/components/connection/ConnectionDialog.vue b/src/components/connection/ConnectionDialog.vue index 4ea7f0fef..7785c8c9c 100644 --- a/src/components/connection/ConnectionDialog.vue +++ b/src/components/connection/ConnectionDialog.vue @@ -138,7 +138,7 @@ const driverProfiles: Record< icon: "opengauss", urlParams: "sslmode=disable", }, - gaussdb: { type: "postgres", port: 5432, user: "gaussdb", label: "GaussDB", icon: "gaussdb" }, + gaussdb: { type: "gaussdb", port: 5432, user: "gaussdb", label: "GaussDB", icon: "gaussdb" }, kingbase: { type: "postgres", port: 54321, user: "system", label: "KingBase", icon: "kingbase" }, vastbase: { type: "postgres", port: 5432, user: "vastbase", label: "Vastbase", icon: "vastbase" }, doris: { type: "mysql", port: 9030, user: "root", label: "Doris", icon: "doris", urlParams: "" }, @@ -323,9 +323,7 @@ const dbOptions = [ { value: "elasticsearch", label: "Elasticsearch" }, { value: "mariadb", label: "MariaDB" }, { value: "dm", label: "DM (Dameng)" }, -]; - -const mysqlCompat = [ + { value: "gaussdb", label: "GaussDB" }, { value: "tidb", label: "TiDB" }, { value: "oceanbase", label: "OceanBase" }, { value: "goldendb", label: "GoldenDB" }, @@ -333,24 +331,16 @@ const mysqlCompat = [ { value: "selectdb", label: "SelectDB" }, { value: "starrocks", label: "StarRocks" }, { value: "tdengine", label: "TDengine" }, - { value: "custom_mysql", label: "Custom" }, -]; - -const pgCompat = [ { value: "opengauss", label: "openGauss" }, - { value: "gaussdb", label: "GaussDB" }, { value: "kingbase", label: "KingBase" }, { value: "vastbase", label: "Vastbase" }, { value: "redshift", label: "Redshift" }, { value: "cockroachdb", label: "CockroachDB" }, - { value: "custom_postgres", label: "Custom" }, + { value: "custom_mysql", label: "Custom (MySQL)" }, + { value: "custom_postgres", label: "Custom (PostgreSQL)" }, ]; -const dbCategories = computed(() => [ - { key: "mainstream", title: t("connection.mainstream"), options: dbOptions }, - { key: "mysql", title: `MySQL ${t("connection.compatible")}`, options: mysqlCompat }, - { key: "postgres", title: `PostgreSQL ${t("connection.compatible")}`, options: pgCompat }, -]); +const dbCategories = computed(() => [{ key: "all", title: "", options: dbOptions }]); const filteredDbCategories = computed(() => { const keyword = dbSearchQuery.value.trim().toLowerCase(); @@ -854,6 +844,20 @@ async function browseDbFilePath() {

+
+ +

+ {{ t("connection.gaussdbOdbcHint") }} + + {{ t("connection.gaussdbDownload") }} + +

+
+