mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 12:41:39 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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"),
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
"
|
||||
|
||||
@@ -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, "_"));
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export type DatabaseType =
|
||||
| "dameng"
|
||||
| "gaussdb"
|
||||
| "kingbase"
|
||||
| "highgo"
|
||||
| "vastbase"
|
||||
| "goldendb"
|
||||
| "h2"
|
||||
|
||||
Reference in New Issue
Block a user