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() {
+
+