From 2973bb1cd5edc3805e3a4ee1b6540bc896560381 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Fri, 15 May 2026 14:35:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20add=20Highgo=20(=E7=80=9A=E9=AB=98)=20d?= =?UTF-8?q?atabase=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add full support for Highgo database, a PostgreSQL-compatible Chinese domestic database. Includes database type definition, connection dialog, agent driver mapping, and database icon. --- crates/dbx-core/src/connection.rs | 57 ++++++++++++++++++- crates/dbx-core/src/database_capabilities.rs | 2 + crates/dbx-core/src/models/connection.rs | 6 ++ docs/app/[lang]/page.tsx | 1 + mcp/src/index.ts | 2 +- src-tauri/src/commands/agents.rs | 1 + src-tauri/src/commands/connection.rs | 6 +- .../connection/ConnectionDialog.vue | 16 +++++- src/components/icons/DatabaseIcon.vue | 1 + src/lib/databaseCapabilitySets.ts | 6 ++ src/stores/connectionStore.ts | 45 +++++++++++++++ src/types/database.ts | 1 + 12 files changed, 136 insertions(+), 8 deletions(-) diff --git a/crates/dbx-core/src/connection.rs b/crates/dbx-core/src/connection.rs index daceb00d53..f1a506dee6 100644 --- a/crates/dbx-core/src/connection.rs +++ b/crates/dbx-core/src/connection.rs @@ -223,6 +223,7 @@ impl AppState { } DatabaseType::Dameng | DatabaseType::Kingbase + | DatabaseType::Highgo | DatabaseType::Vastbase | DatabaseType::Goldendb | DatabaseType::Oracle @@ -371,9 +372,39 @@ impl AppState { None => connection_id.to_string(), } }; - self.connections.write().await.remove(&pool_key); + if self.uses_forwarded_transport(connection_id).await { + self.remove_connection_pools(connection_id).await; + self.reset_connection_transport(connection_id).await; + } else { + self.connections.write().await.remove(&pool_key); + } self.get_or_create_pool(connection_id, database).await } + + pub async fn reset_connection_transport(&self, connection_id: &str) { + self.tunnels.stop_tunnel(connection_id).await; + self.proxy_tunnels.stop_tunnel(connection_id).await; + } + + pub async fn remove_connection_pools(&self, connection_id: &str) { + let mut conns = self.connections.write().await; + let keys_to_remove: Vec = conns + .keys() + .filter(|k| *k == connection_id || k.starts_with(&format!("{connection_id}:"))) + .cloned() + .collect(); + for key in keys_to_remove { + conns.remove(&key); + } + } + + async fn uses_forwarded_transport(&self, connection_id: &str) -> bool { + let configs = self.configs.read().await; + configs.get(connection_id).is_some_and(|config| { + (config.ssh_enabled && !config.ssh_host.is_empty()) + || (config.proxy_enabled && !config.proxy_host.is_empty()) + }) + } } fn default_plugin_dir() -> PathBuf { @@ -436,7 +467,7 @@ async fn detect_ob_oracle_mode(config: &ConnectionConfig, pool: &sqlx::mysql::My #[cfg(test)] mod tests { - use super::{agent_connect_params, database_connection_config, metadata_connection_config, AppState}; + use super::{agent_connect_params, database_connection_config, metadata_connection_config, AppState, PoolKind}; use crate::models::connection::{ConnectionConfig, DatabaseType, ProxyType}; use crate::schema; use crate::storage::Storage; @@ -566,6 +597,28 @@ mod tests { let _ = std::fs::remove_dir_all(dir); } + #[tokio::test] + async fn remove_connection_pools_clears_base_and_database_scoped_pools() { + let (state, dir) = test_app_state().await; + let pool = sqlx::sqlite::SqlitePoolOptions::new().max_connections(1).connect(":memory:").await.unwrap(); + + { + let mut conns = state.connections.write().await; + conns.insert("conn".to_string(), PoolKind::Sqlite(pool.clone())); + conns.insert("conn:analytics".to_string(), PoolKind::Sqlite(pool.clone())); + conns.insert("other".to_string(), PoolKind::Sqlite(pool)); + } + + state.remove_connection_pools("conn").await; + + let conns = state.connections.read().await; + assert!(!conns.contains_key("conn")); + assert!(!conns.contains_key("conn:analytics")); + assert!(conns.contains_key("other")); + + let _ = std::fs::remove_dir_all(dir); + } + #[tokio::test] async fn proxy_connection_uses_local_forward_endpoint() { let (state, dir) = test_app_state().await; diff --git a/crates/dbx-core/src/database_capabilities.rs b/crates/dbx-core/src/database_capabilities.rs index df10795f71..f8973568e7 100644 --- a/crates/dbx-core/src/database_capabilities.rs +++ b/crates/dbx-core/src/database_capabilities.rs @@ -4,6 +4,7 @@ pub fn agent_key(db_type: &DatabaseType, driver_profile: Option<&str>) -> Option match db_type { DatabaseType::Dameng => Some("dameng"), DatabaseType::Kingbase => Some("kingbase"), + DatabaseType::Highgo => Some("highgo"), DatabaseType::Vastbase => Some("vastbase"), DatabaseType::Goldendb => Some("goldendb"), DatabaseType::Oracle => match driver_profile { @@ -39,6 +40,7 @@ pub fn is_single_connection_pool(db_type: &DatabaseType) -> bool { | DatabaseType::Oracle | DatabaseType::Dameng | DatabaseType::Kingbase + | DatabaseType::Highgo | DatabaseType::Vastbase | DatabaseType::Goldendb | DatabaseType::Jdbc diff --git a/crates/dbx-core/src/models/connection.rs b/crates/dbx-core/src/models/connection.rs index 696ef6b346..08838aec75 100644 --- a/crates/dbx-core/src/models/connection.rs +++ b/crates/dbx-core/src/models/connection.rs @@ -114,6 +114,7 @@ pub enum DatabaseType { Redshift, Dameng, Kingbase, + Highgo, Vastbase, Goldendb, Gaussdb, @@ -161,6 +162,7 @@ impl ConnectionConfig { DatabaseType::Redshift => Some("dev"), DatabaseType::Gaussdb => Some("postgres"), DatabaseType::Kingbase | DatabaseType::Vastbase => Some("postgres"), + DatabaseType::Highgo => Some("highgo"), _ => None, } } @@ -231,6 +233,7 @@ impl ConnectionConfig { } DatabaseType::Dameng => format!("dm://{host}:{port}{db_part}"), DatabaseType::Kingbase => format!("kingbase://{host}:{port}{db_part}"), + DatabaseType::Highgo => format!("highgo://{host}:{port}{db_part}"), DatabaseType::Vastbase => format!("vastbase://{host}:{port}{db_part}"), DatabaseType::Goldendb => format!("goldendb://{host}:{port}{db_part}"), DatabaseType::Gaussdb => format!("gaussdb://{host}:{port}{db_part}"), @@ -319,6 +322,9 @@ impl ConnectionConfig { DatabaseType::Kingbase => { format!("kingbase://{}:{}@{host}:{port}{db_part}", username, password) } + DatabaseType::Highgo => { + format!("highgo://{}:{}@{host}:{port}{db_part}", username, password) + } DatabaseType::Vastbase => { format!("vastbase://{}:{}@{host}:{port}{db_part}", username, password) } diff --git a/docs/app/[lang]/page.tsx b/docs/app/[lang]/page.tsx index 9aee24ffc1..4a9c0557c7 100644 --- a/docs/app/[lang]/page.tsx +++ b/docs/app/[lang]/page.tsx @@ -86,6 +86,7 @@ const databaseSupport = [ { name: 'TDengine', icon: '/icons/database/tdengine.svg', tone: '#2f6fff' }, { name: 'openGauss', icon: '/icons/database/opengauss.svg', tone: '#1488c9' }, { name: 'KingBase', icon: '/icons/database/kingbase.svg', tone: '#e1212d' }, + { name: 'HighGo', initials: 'HG', tone: '#005bac' }, { name: 'CockroachDB', icon: '/icons/database/cockroachdb.svg', tone: '#6933ff' }, { name: 'More', initials: '···', tone: '#6ea8ff' }, ]; diff --git a/mcp/src/index.ts b/mcp/src/index.ts index 7e71d786bb..eef02281e9 100644 --- a/mcp/src/index.ts +++ b/mcp/src/index.ts @@ -144,7 +144,7 @@ server.tool( "Add a new database connection to DBX", { name: z.string().describe("Connection name"), - db_type: z.string().describe("Database type: postgres, mysql, sqlite, redis, duckdb, clickhouse, sqlserver, mongodb, oracle, elasticsearch, doris, starrocks, redshift, dameng, kingbase, vastbase, goldendb, gaussdb, h2, snowflake, trino, hive, db2, informix, neo4j, cassandra, bigquery, kylin, sundb, jdbc"), + db_type: z.string().describe("Database type: postgres, mysql, sqlite, redis, duckdb, clickhouse, sqlserver, mongodb, oracle, elasticsearch, doris, starrocks, redshift, dameng, kingbase, highgo, vastbase, goldendb, gaussdb, h2, snowflake, trino, hive, db2, informix, neo4j, cassandra, bigquery, kylin, sundb, jdbc"), host: z.string().describe("Database host"), port: z.number().describe("Database port"), username: z.string().default("").describe("Username"), diff --git a/src-tauri/src/commands/agents.rs b/src-tauri/src/commands/agents.rs index 4211f95ca0..66a0e7cbeb 100644 --- a/src-tauri/src/commands/agents.rs +++ b/src-tauri/src/commands/agents.rs @@ -17,6 +17,7 @@ static REGISTRY_CACHE: std::sync::LazyLock>, config: ConnectionConfi let id = config.id.clone(); let db_config = metadata_connection_config(&config); + state.remove_connection_pools(&id).await; + state.reset_connection_transport(&id).await; + let (host, port) = state.connection_host_port(&id, &db_config).await?; probe_connection_endpoint(&db_config, &host, port).await?; let url = connection_url_for_endpoint(&db_config, &host, port); @@ -287,7 +290,6 @@ pub async fn disconnect_db(state: State<'_, Arc>, connection_id: Strin } drop(conns); state.configs.write().await.remove(&connection_id); - state.tunnels.stop_tunnel(&connection_id).await; - state.proxy_tunnels.stop_tunnel(&connection_id).await; + state.reset_connection_transport(&connection_id).await; Ok(()) } diff --git a/src/components/connection/ConnectionDialog.vue b/src/components/connection/ConnectionDialog.vue index 52111148ba..9a519bc71b 100644 --- a/src/components/connection/ConnectionDialog.vue +++ b/src/components/connection/ConnectionDialog.vue @@ -161,6 +161,7 @@ const driverProfiles: Record< }, gaussdb: { type: "gaussdb", port: 5432, user: "gaussdb", label: "GaussDB", icon: "gaussdb" }, kingbase: { type: "kingbase", port: 54321, user: "system", label: "KingBase", icon: "kingbase" }, + highgo: { type: "highgo", port: 5866, user: "highgo", label: "瀚高 HighGo", icon: "highgo" }, vastbase: { type: "vastbase", port: 5432, user: "vastbase", label: "Vastbase", icon: "vastbase" }, doris: { type: "mysql", port: 9030, user: "root", label: "Doris", icon: "doris", urlParams: "" }, selectdb: { @@ -338,6 +339,7 @@ function defaultDatabaseForProfile() { if (form.value.db_type === "redshift") return "dev"; if (form.value.db_type === "gaussdb") return "postgres"; if (selectedType.value === "cockroachdb") return "defaultdb"; + if (form.value.db_type === "highgo") return "highgo"; if (form.value.db_type === "postgres" || form.value.db_type === "kingbase" || form.value.db_type === "vastbase") return "postgres"; if (form.value.db_type === "sqlserver") return "master"; @@ -370,6 +372,7 @@ const iconTypeMap: Record = { opengauss: "opengauss", gaussdb: "gaussdb", kingbase: "kingbase", + highgo: "highgo", vastbase: "vastbase", doris: "doris", selectdb: "selectdb", @@ -417,6 +420,7 @@ const dbOptions = [ { value: "tdengine", label: "TDengine" }, { value: "opengauss", label: "openGauss" }, { value: "kingbase", label: "KingBase" }, + { value: "highgo", label: "瀚高 HighGo" }, { value: "vastbase", label: "Vastbase" }, { value: "redshift", label: "Redshift" }, { value: "cockroachdb", label: "CockroachDB" }, @@ -906,10 +910,15 @@ function openExternalUrl(url: string) {
-
+
+ {{ selectedProfile().label }} + +
@@ -1195,6 +1204,7 @@ function openExternalUrl(url: string) { form.db_type === 'redshift' || form.db_type === 'informix' || form.db_type === 'kingbase' || + form.db_type === 'highgo' || form.db_type === 'vastbase' || form.db_type === 'goldendb' " diff --git a/src/components/icons/DatabaseIcon.vue b/src/components/icons/DatabaseIcon.vue index 9d5900fcbd..22bf4cd0bc 100644 --- a/src/components/icons/DatabaseIcon.vue +++ b/src/components/icons/DatabaseIcon.vue @@ -52,6 +52,7 @@ const assetIcons: Record = { const letterIcons: Record = { goldendb: { letter: "G", color: "#F59E0B" }, vastbase: { letter: "V", color: "#6D28D9" }, + highgo: { letter: "瀚", color: "#005bac" }, }; const normalizedType = computed(() => props.dbType.toLowerCase().replace(/[\s-]+/g, "_")); diff --git a/src/lib/databaseCapabilitySets.ts b/src/lib/databaseCapabilitySets.ts index 5f16415b2f..0ef638dae8 100644 --- a/src/lib/databaseCapabilitySets.ts +++ b/src/lib/databaseCapabilitySets.ts @@ -8,6 +8,7 @@ export const SCHEMA_AWARE_TYPES = new Set([ "dameng", "gaussdb", "kingbase", + "highgo", "vastbase", "jdbc", "h2", @@ -28,6 +29,7 @@ export const DIAGRAM_SUPPORTED_TYPES = new Set([ "dameng", "gaussdb", "kingbase", + "highgo", "vastbase", "goldendb", "h2", @@ -46,6 +48,7 @@ export const DATABASE_SEARCH_SUPPORTED_TYPES = new Set([ "dameng", "gaussdb", "kingbase", + "highgo", "vastbase", "goldendb", "h2", @@ -75,6 +78,7 @@ export const TABLE_IMPORT_SUPPORTED_TYPES = new Set([ "dameng", "gaussdb", "kingbase", + "highgo", "vastbase", "goldendb", ]); @@ -115,6 +119,7 @@ export const TREE_SCHEMA_TYPES = new Set([ "sqlserver", "gaussdb", "kingbase", + "highgo", "vastbase", "jdbc", "trino", @@ -126,6 +131,7 @@ export const PG_LIKE_STRUCTURE_TYPES = new Set(["postgres", "redsh export const AGENT_DRIVER_TYPES = new Set([ "dameng", "kingbase", + "highgo", "vastbase", "goldendb", "oracle", diff --git a/src/stores/connectionStore.ts b/src/stores/connectionStore.ts index 280f196bc2..5e3adb6a69 100644 --- a/src/stores/connectionStore.ts +++ b/src/stores/connectionStore.ts @@ -23,6 +23,7 @@ import { isTauriRuntime } from "@/lib/tauriRuntime"; import { isSchemaAware, usesTreeSchemaMode } from "@/lib/databaseCapabilities"; import { buildDatabaseTreeNodes } from "@/lib/databaseTree"; import { buildSqlServerDatabaseTreeNodes, SQLSERVER_DEFAULT_SCHEMA } from "@/lib/sqlServerTree"; +import { shouldMarkDisconnected } from "@/lib/connectionHealth"; import { buildGroupedObjectTreeNodes, buildTableTreeNodes, @@ -153,6 +154,14 @@ export const useConnectionStore = defineStore("connection", () => { return message; } + function recordMetadataLoadError(connectionId: string, error: unknown) { + if (shouldMarkDisconnected(error)) { + connectedIds.value.delete(connectionId); + if (activeConnectionId.value === connectionId) activeConnectionId.value = null; + } + recordConnectionError(connectionId, error); + } + function normalizeConnection(config: ConnectionConfig): ConnectionConfig { const labelMap: Record = { mysql: "MySQL", @@ -171,6 +180,7 @@ export const useConnectionStore = defineStore("connection", () => { dameng: "DM (Dameng)", gaussdb: "GaussDB", kingbase: "KingBase", + highgo: "瀚高 HighGo", vastbase: "Vastbase", goldendb: "GoldenDB", h2: "H2", @@ -194,6 +204,8 @@ export const useConnectionStore = defineStore("connection", () => { dbType = "redshift" as ConnectionConfig["db_type"]; } else if (profile === "kingbase" && dbType === "postgres") { dbType = "kingbase" as ConnectionConfig["db_type"]; + } else if (profile === "highgo" && dbType === "postgres") { + dbType = "highgo" as ConnectionConfig["db_type"]; } else if (profile === "vastbase" && dbType === "postgres") { dbType = "vastbase" as ConnectionConfig["db_type"]; } else if (profile === "goldendb" && dbType === "mysql") { @@ -588,6 +600,9 @@ export const useConnectionStore = defineStore("connection", () => { await savePersistedTreeChildren(cacheKey, children); } node.isExpanded = true; + } catch (e) { + recordMetadataLoadError(connectionId, e); + throw e; } finally { node.isLoading = false; } @@ -620,6 +635,9 @@ export const useConnectionStore = defineStore("connection", () => { ), ); node.isExpanded = true; + } catch (e) { + recordMetadataLoadError(connectionId, e); + throw e; } finally { node.isLoading = false; } @@ -665,6 +683,9 @@ export const useConnectionStore = defineStore("connection", () => { ), ); node.isExpanded = true; + } catch (e) { + recordMetadataLoadError(connectionId, e); + throw e; } finally { node.isLoading = false; } @@ -690,6 +711,9 @@ export const useConnectionStore = defineStore("connection", () => { })), ); node.isExpanded = true; + } catch (e) { + recordMetadataLoadError(connectionId, e); + throw e; } finally { node.isLoading = false; } @@ -720,6 +744,9 @@ export const useConnectionStore = defineStore("connection", () => { setChildren(node, children); await savePersistedTreeChildren(cacheKey, children); node.isExpanded = true; + } catch (e) { + recordMetadataLoadError(connectionId, e); + throw e; } finally { node.isLoading = false; } @@ -744,6 +771,9 @@ export const useConnectionStore = defineStore("connection", () => { setChildren(node, children); await savePersistedTreeChildren(cacheKey, children); node.isExpanded = true; + } catch (e) { + recordMetadataLoadError(connectionId, e); + throw e; } finally { node.isLoading = false; } @@ -774,6 +804,9 @@ export const useConnectionStore = defineStore("connection", () => { setChildren(node, children); await savePersistedTreeChildren(cacheKey, children); node.isExpanded = true; + } catch (e) { + recordMetadataLoadError(connectionId, e); + throw e; } finally { node.isLoading = false; } @@ -865,6 +898,9 @@ export const useConnectionStore = defineStore("connection", () => { })), ); node.isExpanded = true; + } catch (e) { + recordMetadataLoadError(connectionId, e); + throw e; } finally { node.isLoading = false; } @@ -894,6 +930,9 @@ export const useConnectionStore = defineStore("connection", () => { })), ); node.isExpanded = true; + } catch (e) { + recordMetadataLoadError(connectionId, e); + throw e; } finally { node.isLoading = false; } @@ -923,6 +962,9 @@ export const useConnectionStore = defineStore("connection", () => { })), ); node.isExpanded = true; + } catch (e) { + recordMetadataLoadError(connectionId, e); + throw e; } finally { node.isLoading = false; } @@ -952,6 +994,9 @@ export const useConnectionStore = defineStore("connection", () => { })), ); node.isExpanded = true; + } catch (e) { + recordMetadataLoadError(connectionId, e); + throw e; } finally { node.isLoading = false; } diff --git a/src/types/database.ts b/src/types/database.ts index 04d45fa245..561539ce84 100644 --- a/src/types/database.ts +++ b/src/types/database.ts @@ -15,6 +15,7 @@ export type DatabaseType = | "dameng" | "gaussdb" | "kingbase" + | "highgo" | "vastbase" | "goldendb" | "h2"