feat: add Highgo (瀚高) database support

Add full support for Highgo database, a PostgreSQL-compatible Chinese
domestic database. Includes database type definition, connection dialog,
agent driver mapping, and database icon.
This commit is contained in:
t8y2
2026-05-15 14:35:19 +08:00
parent f91ccf89c4
commit 2973bb1cd5
12 changed files with 136 additions and 8 deletions
+55 -2
View File
@@ -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<String> = 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;
@@ -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
+6
View File
@@ -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)
}
+1
View File
@@ -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' },
];
+1 -1
View File
@@ -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"),
+1
View File
@@ -17,6 +17,7 @@ static REGISTRY_CACHE: std::sync::LazyLock<Mutex<Option<(std::time::Instant, Age
const AGENT_TYPES: &[(&str, &str)] = &[
("dameng", "达梦 DM8"),
("kingbase", "人大金仓 KingbaseES"),
("highgo", "瀚高 HighGo"),
("vastbase", "Vastbase"),
("goldendb", "GoldenDB"),
("oracle", "Oracle"),
+4 -2
View File
@@ -164,6 +164,9 @@ pub async fn connect_db(state: State<'_, Arc<AppState>>, 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<AppState>>, 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(())
}
+13 -3
View File
@@ -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<string, string> = {
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) {
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">{{ t("connection.type") }}</Label>
<div class="col-span-3 flex items-center gap-2 rounded-md border bg-muted/20 px-3 py-2">
<button
type="button"
class="col-span-3 flex items-center gap-2 rounded-md border bg-muted/20 px-3 py-2 hover:bg-muted/40 cursor-pointer transition"
@click="backToDatabasePicker()"
>
<DatabaseIcon :db-type="selectedDbIcon" class="h-4 w-4 shrink-0" />
<span class="min-w-0 flex-1 truncate text-sm">{{ selectedProfile().label }}</span>
</div>
<span class="min-w-0 flex-1 truncate text-sm text-left">{{ selectedProfile().label }}</span>
<Pencil class="h-3 w-3 text-muted-foreground" />
</button>
</div>
<div v-if="isCustomCompatibleProfile()" class="grid grid-cols-4 items-center gap-4">
@@ -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'
"
+1
View File
@@ -52,6 +52,7 @@ const assetIcons: Record<string, string> = {
const letterIcons: Record<string, { letter: string; color: string }> = {
goldendb: { letter: "G", color: "#F59E0B" },
vastbase: { letter: "V", color: "#6D28D9" },
highgo: { letter: "瀚", color: "#005bac" },
};
const normalizedType = computed(() => props.dbType.toLowerCase().replace(/[\s-]+/g, "_"));
+6
View File
@@ -8,6 +8,7 @@ export const SCHEMA_AWARE_TYPES = new Set<DatabaseType>([
"dameng",
"gaussdb",
"kingbase",
"highgo",
"vastbase",
"jdbc",
"h2",
@@ -28,6 +29,7 @@ export const DIAGRAM_SUPPORTED_TYPES = new Set<DatabaseType>([
"dameng",
"gaussdb",
"kingbase",
"highgo",
"vastbase",
"goldendb",
"h2",
@@ -46,6 +48,7 @@ export const DATABASE_SEARCH_SUPPORTED_TYPES = new Set<DatabaseType>([
"dameng",
"gaussdb",
"kingbase",
"highgo",
"vastbase",
"goldendb",
"h2",
@@ -75,6 +78,7 @@ export const TABLE_IMPORT_SUPPORTED_TYPES = new Set<DatabaseType>([
"dameng",
"gaussdb",
"kingbase",
"highgo",
"vastbase",
"goldendb",
]);
@@ -115,6 +119,7 @@ export const TREE_SCHEMA_TYPES = new Set<DatabaseType>([
"sqlserver",
"gaussdb",
"kingbase",
"highgo",
"vastbase",
"jdbc",
"trino",
@@ -126,6 +131,7 @@ export const PG_LIKE_STRUCTURE_TYPES = new Set<DatabaseType>(["postgres", "redsh
export const AGENT_DRIVER_TYPES = new Set<DatabaseType>([
"dameng",
"kingbase",
"highgo",
"vastbase",
"goldendb",
"oracle",
+45
View File
@@ -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<string, string> = {
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;
}
+1
View File
@@ -15,6 +15,7 @@ export type DatabaseType =
| "dameng"
| "gaussdb"
| "kingbase"
| "highgo"
| "vastbase"
| "goldendb"
| "h2"