From 7ce2cf21637856064ae9bb9f50fdaedadd0a054e Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Wed, 13 May 2026 09:47:22 +0800 Subject: [PATCH] feat: add 12 new agent-driven databases, migrate GaussDB to agent Add H2, Snowflake, Trino, Hive, DB2, Informix, Neo4j, Cassandra, BigQuery, Apache Kylin, SunDB as new agent-driven database types. Migrate GaussDB from native rust-gaussdb to JDBC agent. Remove rust-gaussdb dependency. Total agent-driven databases: 17. --- Cargo.lock | 121 +------ crates/dbx-core/Cargo.toml | 1 - crates/dbx-core/src/agent_manager.rs | 12 + crates/dbx-core/src/connection.rs | 40 ++- crates/dbx-core/src/db/gaussdb_driver.rs | 304 ------------------ crates/dbx-core/src/db/mod.rs | 1 - crates/dbx-core/src/models/connection.rs | 59 ++++ crates/dbx-core/src/query.rs | 17 +- crates/dbx-core/src/schema.rs | 108 ------- src-tauri/src/commands/agents.rs | 13 + src-tauri/src/commands/connection.rs | 49 +-- .../connection/ConnectionDialog.vue | 33 ++ src/components/icons/DatabaseIcon.vue | 2 + src/lib/databaseCapabilities.ts | 37 ++- src/stores/connectionStore.ts | 11 + src/types/database.ts | 11 + 16 files changed, 245 insertions(+), 574 deletions(-) delete mode 100644 crates/dbx-core/src/db/gaussdb_driver.rs diff --git a/Cargo.lock b/Cargo.lock index 9df21d42df..6de79cb563 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1398,7 +1398,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.13.1", + "phf", "smallvec", ] @@ -1613,7 +1613,6 @@ dependencies = [ "redis", "reqwest 0.12.28", "russh", - "rust-gaussdb", "rust_decimal", "rustls 0.23.40", "serde", @@ -1917,7 +1916,7 @@ dependencies = [ "arrow", "cast", "comfy-table", - "fallible-iterator 0.3.0", + "fallible-iterator", "fallible-streaming-iterator", "hashlink", "libduckdb-sys", @@ -2143,12 +2142,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "fallible-iterator" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" - [[package]] name = "fallible-iterator" version = "0.3.0" @@ -3864,16 +3857,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "md-5" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" -dependencies = [ - "cfg-if", - "digest 0.11.3", -] - [[package]] name = "md5" version = "0.7.0" @@ -4016,7 +3999,7 @@ dependencies = [ "hickory-resolver", "hmac 0.12.1", "macro_magic", - "md-5 0.10.6", + "md-5", "mongocrypt", "mongodb-internal-macros", "pbkdf2 0.12.2", @@ -4754,15 +4737,6 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_shared 0.11.3", -] - [[package]] name = "phf" version = "0.13.1" @@ -4770,7 +4744,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" dependencies = [ "phf_macros", - "phf_shared 0.13.1", + "phf_shared", "serde", ] @@ -4781,7 +4755,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" dependencies = [ "phf_generator", - "phf_shared 0.13.1", + "phf_shared", ] [[package]] @@ -4791,7 +4765,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" dependencies = [ "fastrand", - "phf_shared 0.13.1", + "phf_shared", ] [[package]] @@ -4801,21 +4775,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" dependencies = [ "phf_generator", - "phf_shared 0.13.1", + "phf_shared", "proc-macro2", "quote", "syn 2.0.117", ] -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher", -] - [[package]] name = "phf_shared" version = "0.13.1" @@ -4991,38 +4956,6 @@ dependencies = [ "rand 0.8.6", ] -[[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", - "bytes", - "fallible-iterator 0.2.0", - "hmac 0.13.0", - "md-5 0.11.0", - "memchr", - "rand 0.10.1", - "sha2 0.11.0", - "stringprep", -] - -[[package]] -name = "postgres-types" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dc729a129e682e8d24170cd30ae1aa01b336b096cbb56df6d534ffec133d186" -dependencies = [ - "bytes", - "chrono", - "fallible-iterator 0.2.0", - "postgres-protocol", - "serde_core", - "serde_json", -] - [[package]] name = "potential_utf" version = "0.1.5" @@ -5790,32 +5723,6 @@ dependencies = [ "wasm-bindgen-futures", ] -[[package]] -name = "rust-gaussdb" -version = "0.1.0" -source = "git+https://github.com/t8y2/rust-gaussdb?branch=main#8ad53d7301f19b5fc0856e98649b866118b8d8de" -dependencies = [ - "base64 0.22.1", - "byteorder", - "bytes", - "fallible-iterator 0.2.0", - "futures-channel", - "futures-util", - "hex", - "hmac 0.12.1", - "log", - "md-5 0.10.6", - "pbkdf2 0.12.2", - "phf 0.11.3", - "postgres-protocol", - "postgres-types", - "rand 0.9.4", - "sha1 0.10.6", - "sha2 0.10.9", - "tokio", - "tokio-util", -] - [[package]] name = "rust_decimal" version = "1.42.0" @@ -6211,7 +6118,7 @@ dependencies = [ "derive_more", "log", "new_debug_unreachable", - "phf 0.13.1", + "phf", "phf_codegen", "precomputed-hash", "rustc-hash", @@ -6814,7 +6721,7 @@ dependencies = [ "hmac 0.12.1", "itoa", "log", - "md-5 0.10.6", + "md-5", "memchr", "once_cell", "percent-encoding", @@ -6856,7 +6763,7 @@ dependencies = [ "home", "itoa", "log", - "md-5 0.10.6", + "md-5", "memchr", "once_cell", "rand 0.8.6", @@ -6942,7 +6849,7 @@ checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" dependencies = [ "new_debug_unreachable", "parking_lot", - "phf_shared 0.13.1", + "phf_shared", "precomputed-hash", ] @@ -6953,7 +6860,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" dependencies = [ "phf_generator", - "phf_shared 0.13.1", + "phf_shared", "proc-macro2", "quote", ] @@ -7516,7 +7423,7 @@ dependencies = [ "json-patch", "log", "memchr", - "phf 0.13.1", + "phf", "plist", "proc-macro2", "quote", @@ -8517,7 +8424,7 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" dependencies = [ - "phf 0.13.1", + "phf", "phf_codegen", "string_cache", "string_cache_codegen", diff --git a/crates/dbx-core/Cargo.toml b/crates/dbx-core/Cargo.toml index 9050a1d461..c1e6fd8f4e 100644 --- a/crates/dbx-core/Cargo.toml +++ b/crates/dbx-core/Cargo.toml @@ -30,6 +30,5 @@ russh = "0.60" portpicker = "0.1.1" csv = "1" calamine = "0.30.1" -rust-gaussdb = { git = "https://github.com/t8y2/rust-gaussdb", branch = "main" } base64 = "0.22" async-trait = "0.1" diff --git a/crates/dbx-core/src/agent_manager.rs b/crates/dbx-core/src/agent_manager.rs index a5b975fd0d..809f6173cc 100644 --- a/crates/dbx-core/src/agent_manager.rs +++ b/crates/dbx-core/src/agent_manager.rs @@ -122,6 +122,18 @@ impl AgentManager { DatabaseType::Vastbase => Some("vastbase"), DatabaseType::Goldendb => Some("goldendb"), DatabaseType::Oracle => Some("oracle"), + DatabaseType::H2 => Some("h2"), + DatabaseType::Snowflake => Some("snowflake"), + DatabaseType::Trino => Some("trino"), + DatabaseType::Hive => Some("hive"), + DatabaseType::Db2 => Some("db2"), + DatabaseType::Informix => Some("informix"), + DatabaseType::Neo4j => Some("neo4j"), + DatabaseType::Cassandra => Some("cassandra"), + DatabaseType::Bigquery => Some("bigquery"), + DatabaseType::Kylin => Some("kylin"), + DatabaseType::Sundb => Some("sundb"), + DatabaseType::Gaussdb => Some("gaussdb"), _ => None, } } diff --git a/crates/dbx-core/src/connection.rs b/crates/dbx-core/src/connection.rs index 532382d5d7..d8be2197f9 100644 --- a/crates/dbx-core/src/connection.rs +++ b/crates/dbx-core/src/connection.rs @@ -44,7 +44,6 @@ pub enum PoolKind { SqlServer(Arc>), Elasticsearch(db::elasticsearch_driver::EsClient), Agent(Arc>), - Gaussdb(Arc>), ExternalTabular(Arc), ExternalDriver { driver_id: String, config: ConnectionConfig, session: Arc }, } @@ -214,7 +213,19 @@ impl AppState { | DatabaseType::Kingbase | DatabaseType::Vastbase | DatabaseType::Goldendb - | DatabaseType::Oracle => { + | DatabaseType::Oracle + | DatabaseType::H2 + | DatabaseType::Snowflake + | DatabaseType::Trino + | DatabaseType::Hive + | DatabaseType::Db2 + | DatabaseType::Informix + | DatabaseType::Neo4j + | DatabaseType::Cassandra + | DatabaseType::Bigquery + | DatabaseType::Kylin + | DatabaseType::Sundb + | DatabaseType::Gaussdb => { let mut client = self.agent_manager.spawn(&db_config.db_type).await?; client .call::( @@ -230,17 +241,6 @@ impl AppState { .await?; PoolKind::Agent(Arc::new(tokio::sync::Mutex::new(client))) } - DatabaseType::Gaussdb => { - let client = db::gaussdb_driver::connect( - &host, - port, - db_config.effective_database().unwrap_or("postgres"), - &db_config.username, - &db_config.password, - ) - .await?; - PoolKind::Gaussdb(Arc::new(tokio::sync::Mutex::new(client))) - } DatabaseType::Jdbc => { let mut jdbc_config = db_config.clone(); if host != config.host || port != config.port { @@ -406,7 +406,19 @@ pub async fn probe_connection_endpoint(config: &ConnectionConfig, host: &str, po | DatabaseType::Kingbase | DatabaseType::Vastbase | DatabaseType::Goldendb - | DatabaseType::Oracle => Ok(()), + | DatabaseType::Oracle + | DatabaseType::H2 + | DatabaseType::Snowflake + | DatabaseType::Trino + | DatabaseType::Hive + | DatabaseType::Db2 + | DatabaseType::Informix + | DatabaseType::Neo4j + | DatabaseType::Cassandra + | DatabaseType::Bigquery + | DatabaseType::Kylin + | DatabaseType::Sundb + | DatabaseType::Gaussdb => Ok(()), _ => db::probe_tcp_endpoint(&format!("{:?}", config.db_type), host, port).await, } } diff --git a/crates/dbx-core/src/db/gaussdb_driver.rs b/crates/dbx-core/src/db/gaussdb_driver.rs deleted file mode 100644 index b44004995e..0000000000 --- a/crates/dbx-core/src/db/gaussdb_driver.rs +++ /dev/null @@ -1,304 +0,0 @@ -use std::time::Instant; - -use crate::sql::starts_with_executable_sql_keyword; -use crate::types::{ColumnInfo, DatabaseInfo, ForeignKeyInfo, IndexInfo, QueryResult, TableInfo, TriggerInfo}; - -use super::CONNECTION_TIMEOUT_SECS; - -pub struct GaussdbClient { - client: rust_gaussdb::Client, -} - -unsafe impl Send for GaussdbClient {} - -impl GaussdbClient { - async fn query_rows(&mut self, sql: &str) -> Result>>, String> { - let rows = self.client.query(sql, &[]).await.map_err(|e| e.to_string())?; - let mut result = Vec::new(); - for row in &rows { - let mut vals = Vec::new(); - for i in 0..row.columns().len() { - vals.push(row.try_get::(i)); - } - result.push(vals); - } - Ok(result) - } - - async fn query_single_column(&mut self, sql: &str) -> Result, String> { - Ok(self.query_rows(sql).await?.into_iter().filter_map(|r| r.into_iter().next().flatten()).collect()) - } -} - -pub async fn connect(host: &str, port: u16, database: &str, user: &str, pass: &str) -> Result { - let database = normalize_database(database); - let dsn = format!("host={host} port={port} user={user} password={pass} dbname={database}"); - - let result = tokio::time::timeout(std::time::Duration::from_secs(CONNECTION_TIMEOUT_SECS), async { - rust_gaussdb::Client::connect(&dsn).await.map_err(|e| format!("GaussDB connection failed: {e}")) - }) - .await - .map_err(|_| format!("GaussDB connection timed out ({CONNECTION_TIMEOUT_SECS}s)"))?; - - result.map(|client| GaussdbClient { client }) -} - -fn normalize_database(database: &str) -> &str { - let database = database.trim(); - if database.is_empty() { - "postgres" - } else { - database - } -} - -pub async fn list_databases(client: &mut GaussdbClient) -> Result, String> { - let rows = client - .query_single_column( - "SELECT datname FROM pg_database \ - WHERE datistemplate = false AND datallowconn = true \ - ORDER BY datname", - ) - .await?; - Ok(rows.into_iter().map(|name| DatabaseInfo { name }).collect()) -} - -pub async fn list_schemas(client: &mut 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') \ - ORDER BY nspname", - ) - .await -} - -pub async fn list_tables(client: &mut 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).await?; - Ok(rows - .into_iter() - .map(|r| { - let raw_type = r.get(1).and_then(|v| v.clone()).unwrap_or_default(); - TableInfo { - name: r.first().and_then(|v| v.clone()).unwrap_or_default(), - table_type: if raw_type.contains("VIEW") { "VIEW".to_string() } else { "TABLE".to_string() }, - comment: None, - } - }) - .collect()) -} - -pub async fn get_columns(client: &mut 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" - )) - .await?; - 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" - )) - .await?; - - Ok(col_rows - .into_iter() - .map(|r| { - let name = r.first().and_then(|v| v.clone()).unwrap_or_default(); - let data_type = r.get(1).and_then(|v| v.clone()).unwrap_or_default(); - let num_prec = r.get(4).and_then(|v| v.as_ref()?.parse::().ok()); - let num_scale = r.get(5).and_then(|v| v.as_ref()?.parse::().ok()); - let char_len = r.get(6).and_then(|v| v.as_ref()?.parse::().ok()); - ColumnInfo { - is_primary_key: pk_names.contains(&name), - name, - data_type, - is_nullable: r.get(2).and_then(|v| v.as_ref()).map(|v| v == "YES").unwrap_or(false), - column_default: r.get(3).and_then(|v| v.clone()).filter(|v| !v.is_empty()), - extra: None, - comment: None, - numeric_precision: num_prec, - numeric_scale: num_scale, - character_maximum_length: char_len, - } - }) - .collect()) -} - -pub async fn list_indexes(client: &mut 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).await?; - Ok(rows - .into_iter() - .map(|r| { - let name = r.first().and_then(|v| v.clone()).unwrap_or_default(); - let def = r.get(1).and_then(|v| v.clone()).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 async fn list_foreign_keys( - client: &mut 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).await?; - Ok(rows - .into_iter() - .map(|r| ForeignKeyInfo { - name: r.first().and_then(|v| v.clone()).unwrap_or_default(), - column: r.get(1).and_then(|v| v.clone()).unwrap_or_default(), - ref_table: r.get(2).and_then(|v| v.clone()).unwrap_or_default(), - ref_column: r.get(3).and_then(|v| v.clone()).unwrap_or_default(), - }) - .collect()) -} - -pub async fn list_triggers(client: &mut GaussdbClient, schema: &str, table: &str) -> Result, String> { - let s = schema.replace('\'', "''"); - let t = table.replace('\'', "''"); - let sql = format!( - "SELECT t.tgname, \ - 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, \ - 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 \ - WHERE NOT t.tgisinternal AND n.nspname = '{s}' AND c.relname = '{t}' \ - ORDER BY t.tgname" - ); - let rows = client.query_rows(&sql).await?; - Ok(rows - .into_iter() - .map(|r| TriggerInfo { - name: r.first().and_then(|v| v.clone()).unwrap_or_default(), - event: r.get(1).and_then(|v| v.clone()).unwrap_or_default(), - timing: r.get(2).and_then(|v| v.clone()).unwrap_or_default(), - }) - .collect()) -} - -pub async fn execute_query(client: &mut GaussdbClient, sql: &str) -> Result { - let start = Instant::now(); - let sql = sql.trim().trim_end_matches(';'); - - if starts_with_executable_sql_keyword(sql, &["SELECT", "WITH", "SHOW", "EXPLAIN"]) { - let rows = client.client.query(sql, &[]).await.map_err(|e| e.to_string())?; - - let columns: Vec = if let Some(first) = rows.first() { - first.columns().iter().map(|c| c.name.clone()).collect() - } else { - Vec::new() - }; - - let mut result_rows = Vec::new(); - for row in &rows { - let vals: Vec = (0..row.columns().len()) - .map(|i| row.try_get::(i).map(serde_json::Value::String).unwrap_or(serde_json::Value::Null)) - .collect(); - result_rows.push(vals); - if result_rows.len() >= crate::query::MAX_ROWS { - break; - } - } - - let truncated = result_rows.len() >= crate::query::MAX_ROWS; - Ok(QueryResult { - columns, - rows: result_rows, - affected_rows: 0, - execution_time_ms: start.elapsed().as_millis(), - truncated, - }) - } else { - let affected = client.client.execute(sql).await.map_err(|e| e.to_string())?; - Ok(QueryResult { - columns: vec![], - rows: vec![], - affected_rows: affected, - execution_time_ms: start.elapsed().as_millis(), - truncated: false, - }) - } -} - -#[cfg(test)] -mod tests { - use super::normalize_database; - - #[test] - fn normalize_database_defaults_blank_to_postgres() { - assert_eq!(normalize_database(""), "postgres"); - assert_eq!(normalize_database(" "), "postgres"); - } - - #[test] - fn normalize_database_keeps_explicit_database() { - assert_eq!(normalize_database("app"), "app"); - } -} diff --git a/crates/dbx-core/src/db/mod.rs b/crates/dbx-core/src/db/mod.rs index fd66cc7528..46b46cb6b6 100644 --- a/crates/dbx-core/src/db/mod.rs +++ b/crates/dbx-core/src/db/mod.rs @@ -3,7 +3,6 @@ pub mod clickhouse_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 ob_oracle; diff --git a/crates/dbx-core/src/models/connection.rs b/crates/dbx-core/src/models/connection.rs index af74426598..ab5efb08a0 100644 --- a/crates/dbx-core/src/models/connection.rs +++ b/crates/dbx-core/src/models/connection.rs @@ -117,6 +117,21 @@ pub enum DatabaseType { Vastbase, Goldendb, Gaussdb, + #[serde(rename = "h2")] + H2, + Snowflake, + Trino, + Hive, + #[serde(rename = "db2")] + Db2, + Informix, + #[serde(rename = "neo4j")] + Neo4j, + Cassandra, + #[serde(rename = "bigquery")] + Bigquery, + Kylin, + Sundb, Jdbc, } @@ -216,6 +231,17 @@ impl ConnectionConfig { DatabaseType::Vastbase => format!("vastbase://{host}:{port}{db_part}"), DatabaseType::Goldendb => format!("goldendb://{host}:{port}{db_part}"), DatabaseType::Gaussdb => format!("gaussdb://{host}:{port}{db_part}"), + DatabaseType::H2 => format!("h2://{host}:{port}{db_part}"), + DatabaseType::Snowflake => format!("snowflake://{host}/{db_part}"), + DatabaseType::Trino => format!("trino://{host}:{port}{db_part}"), + DatabaseType::Hive => format!("hive://{host}:{port}{db_part}"), + DatabaseType::Db2 => format!("db2://{host}:{port}{db_part}"), + DatabaseType::Informix => format!("informix://{host}:{port}{db_part}"), + DatabaseType::Neo4j => format!("neo4j://{host}:{port}{db_part}"), + DatabaseType::Cassandra => format!("cassandra://{host}:{port}{db_part}"), + DatabaseType::Bigquery => format!("bigquery://{host}/{db_part}"), + DatabaseType::Kylin => format!("kylin://{host}:{port}{db_part}"), + DatabaseType::Sundb => format!("sundb://{host}:{port}{db_part}"), DatabaseType::Jdbc => "jdbc:".to_string(), } } @@ -296,6 +322,39 @@ impl ConnectionConfig { DatabaseType::Gaussdb => { format!("gaussdb://{}:{}@{host}:{port}{db_part}", username, password) } + DatabaseType::H2 => { + format!("h2://{}:{}@{host}:{port}{db_part}", username, password) + } + DatabaseType::Snowflake => { + format!("snowflake://{}:{}@{host}/{db_part}", username, password) + } + DatabaseType::Trino => { + format!("trino://{}:{}@{host}:{port}{db_part}", username, password) + } + DatabaseType::Hive => { + format!("hive://{}:{}@{host}:{port}{db_part}", username, password) + } + DatabaseType::Db2 => { + format!("db2://{}:{}@{host}:{port}{db_part}", username, password) + } + DatabaseType::Informix => { + format!("informix://{}:{}@{host}:{port}{db_part}", username, password) + } + DatabaseType::Neo4j => { + format!("neo4j://{}:{}@{host}:{port}{db_part}", username, password) + } + DatabaseType::Cassandra => { + format!("cassandra://{}:{}@{host}:{port}{db_part}", username, password) + } + DatabaseType::Bigquery => { + format!("bigquery://{}:{}@{host}/{db_part}", username, password) + } + DatabaseType::Kylin => { + format!("kylin://{}:{}@{host}:{port}{db_part}", username, password) + } + DatabaseType::Sundb => { + format!("sundb://{}:{}@{host}:{port}{db_part}", username, password) + } DatabaseType::Jdbc => { self.connection_string.as_deref().filter(|value| !value.is_empty()).unwrap_or("jdbc:").to_string() } diff --git a/crates/dbx-core/src/query.rs b/crates/dbx-core/src/query.rs index d22467f46f..2197d92b0a 100644 --- a/crates/dbx-core/src/query.rs +++ b/crates/dbx-core/src/query.rs @@ -227,17 +227,6 @@ 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 mut client = client.lock().await; - db::gaussdb_driver::execute_query(&mut client, &sql).await - }) - .await - .map(truncate_result) - } PoolKind::ExternalTabular(ext_pool) => { if !starts_with_executable_sql_keyword(sql, &["SELECT", "WITH", "SHOW", "DESCRIBE", "EXPLAIN", "PRAGMA"]) { return Err("External data sources are read-only. Only SELECT queries are supported.".to_string()); @@ -476,7 +465,7 @@ pub async fn execute_statements( /// Execute multiple SQL statements within a single transaction. /// For sqlx-based pools (Postgres/MySQL/SQLite), uses the Transaction API to /// guarantee all statements run on the same physical connection. -/// For custom drivers (ClickHouse/SqlServer/Agent/Gaussdb), uses explicit +/// For custom drivers (ClickHouse/SqlServer/Agent), uses explicit /// BEGIN/COMMIT/ROLLBACK on the already-single-connection client. /// For databases that don't support explicit transactions (Redis, MongoDB, Oracle), /// executes statements sequentially without transaction. @@ -503,9 +492,7 @@ pub async fn execute_statements_in_transaction( PoolKind::Postgres(pg) => TxPath::Pg(pg.clone()), PoolKind::Mysql(mp, _mode) => TxPath::Mysql(mp.clone(), false), PoolKind::Sqlite(sq) => TxPath::Sqlite(sq.clone()), - PoolKind::ClickHouse(_) | PoolKind::SqlServer(_) | PoolKind::Agent(_) | PoolKind::Gaussdb(_) => { - TxPath::Explicit - } + PoolKind::ClickHouse(_) | PoolKind::SqlServer(_) | PoolKind::Agent(_) => TxPath::Explicit, PoolKind::DuckDb(_) | PoolKind::Redis(_) | PoolKind::MongoDb(_) diff --git a/crates/dbx-core/src/schema.rs b/crates/dbx-core/src/schema.rs index f2c1553ae9..3d0e4a8b4c 100644 --- a/crates/dbx-core/src/schema.rs +++ b/crates/dbx-core/src/schema.rs @@ -112,16 +112,6 @@ pub fn extract_agent( } } -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.read().await; @@ -150,11 +140,6 @@ pub async fn list_databases_core(state: &AppState, connection_id: &str) -> Resul let mut client = client.lock().await; return client.call("list_databases", serde_json::json!({})).await; } - if let Some(client) = extract_gaussdb(&connections, connection_id) { - drop(connections); - let mut client = client.lock().await; - return db::gaussdb_driver::list_databases(&mut client).await; - } } let connections = state.connections.read().await; @@ -198,11 +183,6 @@ pub async fn list_schemas_core(state: &AppState, connection_id: &str, database: let mut client = client.lock().await; return client.call("list_schemas", serde_json::json!({"database": database})).await; } - if let Some(client) = extract_gaussdb(&connections, &pool_key) { - drop(connections); - let mut client = client.lock().await; - return db::gaussdb_driver::list_schemas(&mut client).await; - } } let connections = state.connections.read().await; @@ -264,11 +244,6 @@ pub async fn list_tables_core( let mut client = client.lock().await; return client.call("list_tables", serde_json::json!({"schema": schema})).await; } - if let Some(client) = extract_gaussdb(&connections, &pool_key) { - drop(connections); - let mut client = client.lock().await; - return db::gaussdb_driver::list_tables(&mut client, schema).await; - } } let connections = state.connections.read().await; @@ -392,11 +367,6 @@ pub async fn get_columns_core( let mut client = client.lock().await; return client.call("get_columns", serde_json::json!({"schema": schema, "table": table})).await; } - if let Some(client) = extract_gaussdb(&connections, &pool_key) { - drop(connections); - let mut client = client.lock().await; - return db::gaussdb_driver::get_columns(&mut client, schema, table).await; - } } let connections = state.connections.read().await; @@ -437,11 +407,6 @@ pub async fn list_indexes_core( let mut client = client.lock().await; return client.call("list_indexes", serde_json::json!({"schema": schema, "table": table})).await; } - if let Some(client) = extract_gaussdb(&connections, &pool_key) { - drop(connections); - let mut client = client.lock().await; - return db::gaussdb_driver::list_indexes(&mut client, schema, table).await; - } } let connections = state.connections.read().await; @@ -482,11 +447,6 @@ pub async fn list_foreign_keys_core( let mut client = client.lock().await; return client.call("list_foreign_keys", serde_json::json!({"schema": schema, "table": table})).await; } - if let Some(client) = extract_gaussdb(&connections, &pool_key) { - drop(connections); - let mut client = client.lock().await; - return db::gaussdb_driver::list_foreign_keys(&mut client, schema, table).await; - } } let connections = state.connections.read().await; @@ -527,11 +487,6 @@ pub async fn list_triggers_core( let mut client = client.lock().await; return client.call("list_triggers", serde_json::json!({"schema": schema, "table": table})).await; } - if let Some(client) = extract_gaussdb(&connections, &pool_key) { - drop(connections); - let mut client = client.lock().await; - return db::gaussdb_driver::list_triggers(&mut client, schema, table).await; - } } let connections = state.connections.read().await; @@ -598,11 +553,6 @@ pub async fn get_table_ddl_core( let mut client = client.lock().await; return client.call("get_table_ddl", serde_json::json!({"schema": schema, "table": table})).await; } - if let Some(client) = extract_gaussdb(&connections, &pool_key) { - drop(connections); - let mut client = client.lock().await; - return build_gaussdb_ddl(&mut client, schema, table).await; - } } let connections = state.connections.read().await; @@ -747,13 +697,6 @@ pub async fn get_object_source_core( db::sqlserver::execute_query(&mut client, &sqlserver_object_source_sql(schema, name, &object_type)) .await?, )? - } else if let Some(client) = extract_gaussdb(&connections, &pool_key) { - drop(connections); - let mut client = client.lock().await; - first_string_cell( - db::gaussdb_driver::execute_query(&mut client, &postgres_object_source_sql(schema, name, &object_type)) - .await?, - )? } else { match connections.get(&pool_key).ok_or("Pool not found")? { PoolKind::Mysql(pool, _) => mysql_object_source(pool, name, &object_type).await?, @@ -964,54 +907,3 @@ pub async fn build_sqlserver_ddl( } Ok(ddl) } - -pub async fn build_gaussdb_ddl( - client: &mut db::gaussdb_driver::GaussdbClient, - schema: &str, - table: &str, -) -> Result { - let columns = db::gaussdb_driver::get_columns(client, schema, table).await?; - let indexes = db::gaussdb_driver::list_indexes(client, schema, table).await?; - let fkeys = db::gaussdb_driver::list_foreign_keys(client, schema, table).await?; - - 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/agents.rs b/src-tauri/src/commands/agents.rs index df5244924a..1335fc1cd1 100644 --- a/src-tauri/src/commands/agents.rs +++ b/src-tauri/src/commands/agents.rs @@ -18,6 +18,19 @@ pub async fn list_installed_agents(state: State<'_, Arc>) -> Result>, config: Connection | DatabaseType::Kingbase | DatabaseType::Vastbase | DatabaseType::Goldendb - | DatabaseType::Oracle => { + | DatabaseType::Oracle + | DatabaseType::H2 + | DatabaseType::Snowflake + | DatabaseType::Trino + | DatabaseType::Hive + | DatabaseType::Db2 + | DatabaseType::Informix + | DatabaseType::Neo4j + | DatabaseType::Cassandra + | DatabaseType::Bigquery + | DatabaseType::Kylin + | DatabaseType::Sundb + | DatabaseType::Gaussdb => { state .agent_manager .call_daemon::( @@ -123,15 +135,6 @@ pub async fn test_connection(state: State<'_, Arc>, config: Connection .await?; Ok("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()), DatabaseType::Jdbc => { let mut jdbc_config = config.clone(); if host != config.host || port != config.port { @@ -214,7 +217,19 @@ pub async fn connect_db(state: State<'_, Arc>, config: ConnectionConfi | DatabaseType::Kingbase | DatabaseType::Vastbase | DatabaseType::Goldendb - | DatabaseType::Oracle => { + | DatabaseType::Oracle + | DatabaseType::H2 + | DatabaseType::Snowflake + | DatabaseType::Trino + | DatabaseType::Hive + | DatabaseType::Db2 + | DatabaseType::Informix + | DatabaseType::Neo4j + | DatabaseType::Cassandra + | DatabaseType::Bigquery + | DatabaseType::Kylin + | DatabaseType::Sundb + | DatabaseType::Gaussdb => { let mut client = state.agent_manager.spawn(&db_config.db_type).await?; client .call::( @@ -230,17 +245,6 @@ pub async fn connect_db(state: State<'_, Arc>, config: ConnectionConfi .await?; PoolKind::Agent(std::sync::Arc::new(tokio::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(std::sync::Arc::new(tokio::sync::Mutex::new(client))) - } DatabaseType::Jdbc => state.external_driver_pool("jdbc", &db_config).await?, }; @@ -268,7 +272,6 @@ pub async fn disconnect_db(state: State<'_, Arc>, connection_id: Strin PoolKind::SqlServer(_) => {} PoolKind::Elasticsearch(_) => {} PoolKind::Agent(_) => {} - PoolKind::Gaussdb(_) => {} PoolKind::ExternalTabular(_) => {} PoolKind::ExternalDriver { .. } => {} } diff --git a/src/components/connection/ConnectionDialog.vue b/src/components/connection/ConnectionDialog.vue index 885d83a4f0..ec7cb36fa5 100644 --- a/src/components/connection/ConnectionDialog.vue +++ b/src/components/connection/ConnectionDialog.vue @@ -184,6 +184,17 @@ const driverProfiles: Record< icon: "cockroachdb", }, dm: { type: "dameng", port: 5236, user: "SYSDBA", label: "DM (Dameng)", icon: "dm" }, + h2: { type: "h2", port: 9092, user: "sa", label: "H2", icon: "h2" }, + snowflake: { type: "snowflake", port: 443, user: "", label: "Snowflake", icon: "snowflake" }, + trino: { type: "trino", port: 8080, user: "", label: "Trino", icon: "trino" }, + hive: { type: "hive", port: 10000, user: "", label: "Apache Hive", icon: "hive" }, + db2: { type: "db2", port: 50000, user: "db2inst1", label: "IBM DB2", icon: "db2" }, + informix: { type: "informix", port: 9088, user: "informix", label: "Informix", icon: "informix" }, + neo4j: { type: "neo4j", port: 7687, user: "neo4j", label: "Neo4j", icon: "neo4j" }, + cassandra: { type: "cassandra", port: 9042, user: "cassandra", label: "Cassandra", icon: "cassandra" }, + bigquery: { type: "bigquery", port: 443, user: "", label: "BigQuery", icon: "bigquery" }, + kylin: { type: "kylin", port: 7070, user: "ADMIN", label: "Apache Kylin", icon: "kylin" }, + sundb: { type: "sundb", port: 22000, user: "root", label: "SunDB", icon: "sundb" }, jdbc: { type: "jdbc", port: 0, user: "", label: "JDBC", icon: "jdbc" }, tdengine: { type: "mysql", port: 6030, user: "root", label: "TDengine", icon: "tdengine" }, custom_mysql: { @@ -362,6 +373,17 @@ const iconTypeMap: Record = { cockroachdb: "cockroachdb", tdengine: "tdengine", dm: "dm", + h2: "h2", + snowflake: "snowflake", + trino: "trino", + hive: "hive", + db2: "db2", + informix: "informix", + neo4j: "neo4j", + cassandra: "cassandra", + bigquery: "bigquery", + kylin: "kylin", + sundb: "sundb", jdbc: "jdbc", custom_mysql: "mysql", custom_postgres: "postgres", @@ -393,6 +415,17 @@ const dbOptions = [ { value: "vastbase", label: "Vastbase" }, { value: "redshift", label: "Redshift" }, { value: "cockroachdb", label: "CockroachDB" }, + { value: "h2", label: "H2" }, + { value: "snowflake", label: "Snowflake" }, + { value: "trino", label: "Trino" }, + { value: "hive", label: "Hive" }, + { value: "db2", label: "DB2" }, + { value: "informix", label: "Informix" }, + { value: "neo4j", label: "Neo4j" }, + { value: "cassandra", label: "Cassandra" }, + { value: "bigquery", label: "BigQuery" }, + { value: "kylin", label: "Kylin" }, + { value: "sundb", label: "SunDB" }, { value: "jdbc", label: "JDBC" }, { value: "custom_mysql", label: "Custom (MySQL)" }, { value: "custom_postgres", label: "Custom (PostgreSQL)" }, diff --git a/src/components/icons/DatabaseIcon.vue b/src/components/icons/DatabaseIcon.vue index fa04dc7ba9..2e1fc99236 100644 --- a/src/components/icons/DatabaseIcon.vue +++ b/src/components/icons/DatabaseIcon.vue @@ -31,6 +31,8 @@ const assetIcons: Record = { hive: "hive", apache_kylin: "apache_kylin", sundb: "sundb", + trino: "presto", + kylin: "apache_kylin", cockroachdb: "cockroachdb", db2: "db2", bigquery: "bigquery", diff --git a/src/lib/databaseCapabilities.ts b/src/lib/databaseCapabilities.ts index 25f3991cc5..aabf498825 100644 --- a/src/lib/databaseCapabilities.ts +++ b/src/lib/databaseCapabilities.ts @@ -10,6 +10,10 @@ export const SCHEMA_AWARE_TYPES = new Set([ "kingbase", "vastbase", "jdbc", + "h2", + "snowflake", + "db2", + "informix", ]); export const SQL_FILE_UNSUPPORTED_TYPES = new Set(["redis", "mongodb", "elasticsearch"]); @@ -26,6 +30,8 @@ export const DIAGRAM_SUPPORTED_TYPES = new Set([ "kingbase", "vastbase", "goldendb", + "h2", + "db2", ]); export const DATABASE_SEARCH_SUPPORTED_TYPES = new Set([ @@ -42,6 +48,17 @@ export const DATABASE_SEARCH_SUPPORTED_TYPES = new Set([ "kingbase", "vastbase", "goldendb", + "h2", + "snowflake", + "trino", + "hive", + "db2", + "informix", + "neo4j", + "cassandra", + "bigquery", + "kylin", + "sundb", ]); export const TABLE_IMPORT_SUPPORTED_TYPES = new Set([ @@ -96,7 +113,25 @@ export const TREE_SCHEMA_TYPES = new Set(["postgres", "redshift", export const PG_LIKE_STRUCTURE_TYPES = new Set(["postgres", "redshift", "gaussdb"]); -export const AGENT_DRIVER_TYPES = new Set(["dameng", "kingbase", "vastbase", "goldendb", "oracle"]); +export const AGENT_DRIVER_TYPES = new Set([ + "dameng", + "kingbase", + "vastbase", + "goldendb", + "oracle", + "h2", + "snowflake", + "trino", + "hive", + "db2", + "informix", + "neo4j", + "cassandra", + "bigquery", + "kylin", + "sundb", + "gaussdb", +]); export const TRANSFER_SQL_TYPES = new Set([ "mysql", diff --git a/src/stores/connectionStore.ts b/src/stores/connectionStore.ts index 3ec27afa30..4d7dfb2c49 100644 --- a/src/stores/connectionStore.ts +++ b/src/stores/connectionStore.ts @@ -162,6 +162,17 @@ export const useConnectionStore = defineStore("connection", () => { kingbase: "KingBase", vastbase: "Vastbase", goldendb: "GoldenDB", + h2: "H2", + snowflake: "Snowflake", + trino: "Trino", + hive: "Hive", + db2: "DB2", + informix: "Informix", + neo4j: "Neo4j", + cassandra: "Cassandra", + bigquery: "BigQuery", + kylin: "Kylin", + sundb: "SunDB", }; const profile = config.driver_profile || config.db_type; diff --git a/src/types/database.ts b/src/types/database.ts index 6da9b8f801..83817023cd 100644 --- a/src/types/database.ts +++ b/src/types/database.ts @@ -17,6 +17,17 @@ export type DatabaseType = | "kingbase" | "vastbase" | "goldendb" + | "h2" + | "snowflake" + | "trino" + | "hive" + | "db2" + | "informix" + | "neo4j" + | "cassandra" + | "bigquery" + | "kylin" + | "sundb" | "jdbc"; export interface ConnectionConfig {