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
This commit is contained in:
t8y2 2026-05-07 02:18:20 +08:00
parent 969f441753
commit b79e5b53bc
21 changed files with 602 additions and 40 deletions

View File

@ -29,6 +29,7 @@ pub enum PoolKind {
Oracle(Arc<tokio::sync::Mutex<db::oracle_driver::OracleClient>>),
Elasticsearch(db::elasticsearch_driver::EsClient),
Dameng(Arc<std::sync::Mutex<db::dm_driver::DmClient>>),
Gaussdb(Arc<std::sync::Mutex<db::gaussdb_driver::GaussdbClient>>),
}
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)
};

View File

@ -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<Environment> =
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). \

View File

@ -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<Vec<Vec<String>>, 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<Vec<String>, String> {
Ok(self.query_rows(sql)?.into_iter().filter_map(|r| r.into_iter().next()).collect())
}
}
fn read_cursor(cursor: impl Cursor) -> Result<Vec<Vec<String>>, 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<String> = (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<GaussdbClient, String> {
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<Vec<DatabaseInfo>, 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<Vec<String>, 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<Vec<TableInfo>, 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<Vec<ColumnInfo>, 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<String> = 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::<i32>().ok());
let num_scale = r.get(5).and_then(|v| v.parse::<i32>().ok());
let char_len = r.get(6).and_then(|v| v.parse::<i32>().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<Vec<IndexInfo>, 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<Vec<ForeignKeyInfo>, 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<Vec<TriggerInfo>, 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<QueryResult, String> {
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<String> = (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<serde_json::Value> = (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()),
}
}
}

View File

@ -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<odbc_api::Environment> = 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)
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -110,6 +110,16 @@ pub fn extract_dameng(
}
}
pub fn extract_gaussdb(
connections: &HashMap<String, PoolKind>,
key: &str,
) -> Option<Arc<std::sync::Mutex<db::gaussdb_driver::GaussdbClient>>> {
match connections.get(key)? {
PoolKind::Gaussdb(client) => Some(client.clone()),
_ => None,
}
}
pub async fn list_databases_core(state: &AppState, connection_id: &str) -> Result<Vec<db::DatabaseInfo>, 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<String, String> {
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<String> = 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::<Vec<_>>().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::<Vec<_>>().join(", ");
ddl.push_str(&format!("\nCREATE {unique}INDEX \"{}\" ON \"{schema}\".\"{table}\" ({cols});", idx.name));
}
Ok(ddl)
}

View File

@ -121,6 +121,15 @@ pub async fn test_connection(state: State<'_, Arc<AppState>>, 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<AppState>>, 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<AppState>>, connection_id: Strin
PoolKind::Oracle(_) => {}
PoolKind::Elasticsearch(_) => {}
PoolKind::Dameng(_) => {}
PoolKind::Gaussdb(_) => {}
}
}
}

View File

@ -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<DbCategory[]>(() => [
{ 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<DbCategory[]>(() => [{ key: "all", title: "", options: dbOptions }]);
const filteredDbCategories = computed<DbCategory[]>(() => {
const keyword = dbSearchQuery.value.trim().toLowerCase();
@ -854,6 +844,20 @@ async function browseDbFilePath() {
</p>
</div>
<div v-if="selectedType === 'gaussdb'" class="grid grid-cols-4 items-center gap-4">
<span />
<p class="col-span-3 text-xs text-muted-foreground">
{{ t("connection.gaussdbOdbcHint") }}
<a
href="https://support.huaweicloud.com/mgtg-dws/dws_01_0032.html"
target="_blank"
class="underline text-primary hover:text-primary/80"
>
{{ t("connection.gaussdbDownload") }}
</a>
</p>
</div>
<div v-if="form.db_type === 'oracle'" class="grid grid-cols-4 items-center gap-4">
<Label class="text-right text-xs">SYSDBA</Label>
<label class="col-span-3 flex items-center gap-2 cursor-pointer">

View File

@ -49,8 +49,17 @@ const props = defineProps<{
focusTableName?: string;
}>();
const SQL_TYPES: DatabaseType[] = ["mysql", "postgres", "sqlite", "sqlserver", "oracle", "redshift", "dameng"];
const SCHEMA_AWARE_TYPES: DatabaseType[] = ["postgres", "sqlserver", "oracle", "redshift", "dameng"];
const SQL_TYPES: DatabaseType[] = [
"mysql",
"postgres",
"sqlite",
"sqlserver",
"oracle",
"redshift",
"dameng",
"gaussdb",
];
const SCHEMA_AWARE_TYPES: DatabaseType[] = ["postgres", "sqlserver", "oracle", "redshift", "dameng", "gaussdb"];
const CARD_WIDTH = 270;
const COLUMN_ROW_HEIGHT = 24;
const CARD_HEADER_HEIGHT = 44;

View File

@ -77,7 +77,8 @@ async function resolveSchema(connectionId: string, database: string): Promise<st
config?.db_type === "sqlserver" ||
config?.db_type === "oracle" ||
config?.db_type === "redshift" ||
config?.db_type === "dameng";
config?.db_type === "dameng" ||
config?.db_type === "gaussdb";
if (needsSchema) {
const schemas = await api.listSchemas(connectionId, database);
return schemas.includes("public") ? "public" : (schemas[0] ?? "");

View File

@ -1663,9 +1663,6 @@ function escapeAndHighlightKeywords(s: string): string {
<Button v-if="hasPendingChanges" variant="ghost" size="sm" class="h-5 text-xs" @click="discardChanges">
{{ t("grid.discard") }}
</Button>
<Button variant="ghost" size="sm" class="h-5 text-xs" @click="toggleDdl">
<Code2 class="w-3 h-3 mr-1" /> DDL
</Button>
</template>
<span class="ml-auto flex items-center gap-1">

View File

@ -54,7 +54,7 @@ type SearchTableTask = {
table: TableInfo;
};
const SCHEMA_AWARE_TYPES = new Set<DatabaseType>(["postgres", "sqlserver", "oracle", "redshift", "dameng"]);
const SCHEMA_AWARE_TYPES = new Set<DatabaseType>(["postgres", "sqlserver", "oracle", "redshift", "dameng", "gaussdb"]);
const SYSTEM_SCHEMAS = new Set([
"information_schema",
"pg_catalog",

View File

@ -87,7 +87,16 @@ const emit = defineEmits<{
}>();
const sqlFileUnsupportedTypes = new Set(["redis", "mongodb", "elasticsearch"]);
const diagramSupportedTypes = new Set(["mysql", "postgres", "sqlite", "sqlserver", "oracle", "redshift", "dameng"]);
const diagramSupportedTypes = new Set([
"mysql",
"postgres",
"sqlite",
"sqlserver",
"oracle",
"redshift",
"dameng",
"gaussdb",
]);
const databaseSearchSupportedTypes = new Set([
"mysql",
"postgres",
@ -98,6 +107,7 @@ const databaseSearchSupportedTypes = new Set([
"duckdb",
"clickhouse",
"dameng",
"gaussdb",
]);
const tableImportSupportedTypes = new Set([
"mysql",
@ -111,6 +121,7 @@ const tableImportSupportedTypes = new Set([
"starrocks",
"redshift",
"dameng",
"gaussdb",
]);
const tableStructureSupportedTypes = new Set(["mysql", "postgres", "sqlite", "sqlserver"]);
const fieldLineageSupportedTypes = new Set([
@ -121,6 +132,7 @@ const fieldLineageSupportedTypes = new Set([
"oracle",
"redshift",
"dameng",
"gaussdb",
]);
const isExportingDatabase = ref(false);
@ -133,7 +145,13 @@ function quoteIdent(name: string): string {
}
function isSchemaAwareDbType(dbType?: DatabaseType): boolean {
return dbType === "postgres" || dbType === "oracle" || dbType === "sqlserver" || dbType === "dameng";
return (
dbType === "postgres" ||
dbType === "oracle" ||
dbType === "sqlserver" ||
dbType === "dameng" ||
dbType === "gaussdb"
);
}
function qualifiedTableName(tableName: string, schema?: string): string {
@ -242,7 +260,7 @@ async function toggle() {
queryStore.updateSql(tab, node.label);
} else if (node.type === "database" && node.connectionId && node.database) {
const config = connectionStore.getConfig(node.connectionId);
if (config?.db_type === "postgres" || config?.db_type === "sqlserver") {
if (config?.db_type === "postgres" || config?.db_type === "sqlserver" || config?.db_type === "gaussdb") {
await connectionStore.loadSchemas(node.connectionId, node.database);
} else {
await connectionStore.loadTables(node.connectionId, node.database);
@ -472,7 +490,7 @@ async function confirmDuplicateStructure() {
let sql: string;
if (dbType === "mysql") {
sql = `CREATE TABLE ${target} LIKE ${source};`;
} else if (dbType === "postgres" || dbType === "redshift") {
} else if (dbType === "postgres" || dbType === "redshift" || dbType === "gaussdb") {
sql = `CREATE TABLE ${target} (LIKE ${source} INCLUDING ALL);`;
} else if (dbType === "sqlserver") {
sql = `SELECT TOP 0 * INTO ${target} FROM ${source};`;

View File

@ -34,6 +34,7 @@ const SQL_TYPES: DatabaseType[] = [
"clickhouse",
"duckdb",
"dameng",
"gaussdb",
];
const sqlConnections = computed(() => store.connections.filter((c) => SQL_TYPES.includes(c.db_type)));
@ -134,7 +135,8 @@ async function loadTables() {
config?.db_type === "postgres" ||
config?.db_type === "sqlserver" ||
config?.db_type === "oracle" ||
config?.db_type === "dameng";
config?.db_type === "dameng" ||
config?.db_type === "gaussdb";
if (needsSchema) {
const schemas = await api.listSchemas(sourceConnectionId.value, sourceDatabase.value);
sourceSchema.value = schemas.includes("public") ? "public" : (schemas[0] ?? "");
@ -225,7 +227,8 @@ async function startTransfer() {
targetConfig?.db_type === "postgres" ||
targetConfig?.db_type === "sqlserver" ||
targetConfig?.db_type === "oracle" ||
targetConfig?.db_type === "dameng";
targetConfig?.db_type === "dameng" ||
targetConfig?.db_type === "gaussdb";
if (targetNeedsSchema && !targetSchema.value) {
try {
const schemas = await api.listSchemas(targetConnectionId.value, targetDatabase.value);

View File

@ -14,7 +14,13 @@ export function useSchemaOptions() {
function isSchemaAware(connectionId: string): boolean {
const dbType = connectionStore.getConfig(connectionId)?.db_type;
return dbType === "postgres" || dbType === "sqlserver" || dbType === "oracle" || dbType === "dameng";
return (
dbType === "postgres" ||
dbType === "sqlserver" ||
dbType === "oracle" ||
dbType === "dameng" ||
dbType === "gaussdb"
);
}
async function loadSchemaOptions(connectionId: string, database: string) {

View File

@ -113,6 +113,8 @@ export default {
sshExposeLan: "Expose tunnel to LAN",
dmCompatHint: "Requires DM8 ODBC driver installed on your system.",
dmDownload: "Download from Dameng",
gaussdbOdbcHint: "Requires GaussDB ODBC driver installed on your system.",
gaussdbDownload: "Download from Huawei Cloud",
mongoLegacyHint:
"For older MongoDB servers, enter authSource=admin&authMechanism=SCRAM-SHA-1 here; add directConnection=true only when needed for direct standalone connections.",
compatible: "Compatible",

View File

@ -112,6 +112,8 @@ export default {
sshExposeLan: "允许局域网访问隧道",
dmCompatHint: "需要在系统上安装达梦 DM8 ODBC 驱动程序。",
dmDownload: "前往达梦官网下载",
gaussdbOdbcHint: "需要在系统上安装 GaussDB ODBC 驱动程序。",
gaussdbDownload: "前往华为云下载",
mongoLegacyHint:
"连接旧版 MongoDB 时,可在此填写 authSource=admin&authMechanism=SCRAM-SHA-1直连单节点时可按需追加 directConnection=true。",
compatible: "兼容",

View File

@ -26,7 +26,8 @@ export function qualifiedTableName(
(databaseType === "postgres" ||
databaseType === "oracle" ||
databaseType === "sqlserver" ||
databaseType === "dameng") &&
databaseType === "dameng" ||
databaseType === "gaussdb") &&
schema
) {
return `${quoteTableIdentifier(databaseType, schema)}.${quoteTableIdentifier(databaseType, tableName)}`;

View File

@ -129,6 +129,7 @@ export const useConnectionStore = defineStore("connection", () => {
starrocks: "StarRocks",
redshift: "Redshift",
dameng: "DM (Dameng)",
gaussdb: "GaussDB",
};
return {
...config,
@ -653,7 +654,13 @@ export const useConnectionStore = defineStore("connection", () => {
function isSchemaAwareDatabase(connectionId: string): boolean {
const dbType = getConfig(connectionId)?.db_type;
return dbType === "postgres" || dbType === "sqlserver" || dbType === "oracle" || dbType === "dameng";
return (
dbType === "postgres" ||
dbType === "sqlserver" ||
dbType === "oracle" ||
dbType === "dameng" ||
dbType === "gaussdb"
);
}
async function listCompletionTables(connectionId: string, database: string): Promise<SqlCompletionTable[]> {

View File

@ -12,7 +12,8 @@ export type DatabaseType =
| "doris"
| "starrocks"
| "redshift"
| "dameng";
| "dameng"
| "gaussdb";
export interface ConnectionConfig {
id: string;