From cf9ea23be8db5cc9cbd5702c48c41384afdaf930 Mon Sep 17 00:00:00 2001 From: blankll Date: Mon, 22 Jun 2026 11:35:09 +0800 Subject: [PATCH 01/17] feat(jdbc-bridge): add driver configs for YashanDB, KingbaseES, GBase 8a, XuguDB Add download_url and credentials_in_url fields to DatabaseDriverConfig. Register YashanDB (com.yashandb:yashandb-jdbc), KingbaseES (cn.com.kingbase:kingbase8), GBase 8a (direct download from gbase8.cn), and fix XuguDB artifact/class/URL. Implement credentials_in_url in ConnectionManager for XuguDB. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../java/sqlkit/bridge/ConnectionManager.java | 22 +++++++--- .../java/sqlkit/bridge/DriverResolver.java | 37 ++++++++++++++++- .../java/sqlkit/bridge/ProtocolHandler.java | 10 +++-- .../src/database/jdbc_bridge/drivers.toml | 40 +++++++++++++++++-- .../src/database/jdbc_bridge/fallback.rs | 2 + .../src/database/jdbc_bridge/protocol.rs | 4 ++ .../src/database/jdbc_bridge/registry.rs | 31 ++++++++++++++ 7 files changed, 133 insertions(+), 13 deletions(-) diff --git a/jdbc-bridge/src/main/java/sqlkit/bridge/ConnectionManager.java b/jdbc-bridge/src/main/java/sqlkit/bridge/ConnectionManager.java index 88c09eef..0a1281ad 100644 --- a/jdbc-bridge/src/main/java/sqlkit/bridge/ConnectionManager.java +++ b/jdbc-bridge/src/main/java/sqlkit/bridge/ConnectionManager.java @@ -29,11 +29,23 @@ public class ConnectionManager { public void connect(String connId, String url, String username, String password, String driverClass, List driverJars, - int minPool, int maxPool) throws ClassifiedException, Exception { + int minPool, int maxPool, + boolean credentialsInUrl) throws ClassifiedException, Exception { if (pools.containsKey(connId)) { throw new Exception("Connection already exists: " + connId); } + if (credentialsInUrl) { + StringBuilder sb = new StringBuilder(url); + sb.append(url.contains("?") ? "&" : "?"); + sb.append("user=").append(username); + if (password != null && !password.isEmpty()) { + sb.append("&password=").append(password); + } + url = sb.toString(); + } + final String jdbcUrl = url; + DriverClassLoader loader = new DriverClassLoader(driverJars); Class driverCls = Class.forName(driverClass, true, loader); if (!java.sql.Driver.class.isAssignableFrom(driverCls)) { @@ -48,13 +60,13 @@ public java.sql.Connection getConnection() throws java.sql.SQLException { java.util.Properties info = new java.util.Properties(); if (username != null) info.setProperty("user", username); if (password != null) info.setProperty("password", password); - return driver.connect(url, info); + return driver.connect(jdbcUrl, info); } public java.sql.Connection getConnection(String u, String p) throws java.sql.SQLException { java.util.Properties info = new java.util.Properties(); - info.setProperty("user", u); - info.setProperty("password", p); - return driver.connect(url, info); + if (u != null) info.setProperty("user", u); + if (p != null) info.setProperty("password", p); + return driver.connect(jdbcUrl, info); } public java.io.PrintWriter getLogWriter() { return null; } public void setLogWriter(java.io.PrintWriter out) {} diff --git a/jdbc-bridge/src/main/java/sqlkit/bridge/DriverResolver.java b/jdbc-bridge/src/main/java/sqlkit/bridge/DriverResolver.java index 65eb8ba5..dacd9e08 100644 --- a/jdbc-bridge/src/main/java/sqlkit/bridge/DriverResolver.java +++ b/jdbc-bridge/src/main/java/sqlkit/bridge/DriverResolver.java @@ -40,10 +40,43 @@ private static String getDriversCacheDir() { * @param mavenArtifact e.g. "h2" * @param versionCap Optional max version to cap against. Null means resolve LATEST. * @param classifier Optional Maven classifier (e.g. "standalone"). Null means no classifier. + * @param downloadUrl Direct download URL for drivers NOT on Maven Central. Null means use Maven. * @return DriverResult with path to the cached JAR and resolved version */ - public static DriverResult resolve(String mavenGroup, String mavenArtifact, - String versionCap, String classifier) throws Exception { + public static DriverResult resolve(String mavenGroup, String mavenArtifact, + String versionCap, String classifier, + String downloadUrl) throws Exception { + if (downloadUrl != null && !downloadUrl.isEmpty()) { + return resolveDirect(mavenArtifact, versionCap, downloadUrl); + } + return resolveFromMaven(mavenGroup, mavenArtifact, versionCap, classifier); + } + + private static DriverResult resolveDirect(String mavenArtifact, String versionCap, String downloadUrl) throws Exception { + String version = (versionCap != null && !versionCap.isEmpty()) ? versionCap : "1.0.0"; + String jarFilename = mavenArtifact + "-" + version + ".jar"; + Path destPath = Paths.get(DRIVERS_CACHE, mavenArtifact, jarFilename); + + if (!Files.exists(destPath)) { + Files.createDirectories(destPath.getParent()); + Request request = new Request.Builder() + .url(downloadUrl) + .addHeader("User-Agent", "SQLKit/1.0") + .build(); + Response response = HTTP_CLIENT.newCall(request).execute(); + if (!response.isSuccessful()) { + throw new Exception("Failed to download JAR: HTTP " + response.code() + " for " + downloadUrl); + } + byte[] jarBytes = response.body() != null ? response.body().bytes() : new byte[0]; + response.close(); + Files.write(destPath, jarBytes); + } + + return new DriverResult(destPath.toAbsolutePath().toString(), version); + } + + private static DriverResult resolveFromMaven(String mavenGroup, String mavenArtifact, + String versionCap, String classifier) throws Exception { // 1. Fetch maven-metadata.xml String metadataUrl = String.format("%s/%s/%s/maven-metadata.xml", MAVEN_CENTRAL, mavenGroup.replace('.', '/'), mavenArtifact); diff --git a/jdbc-bridge/src/main/java/sqlkit/bridge/ProtocolHandler.java b/jdbc-bridge/src/main/java/sqlkit/bridge/ProtocolHandler.java index c202c2a8..2a60bcaa 100644 --- a/jdbc-bridge/src/main/java/sqlkit/bridge/ProtocolHandler.java +++ b/jdbc-bridge/src/main/java/sqlkit/bridge/ProtocolHandler.java @@ -121,6 +121,8 @@ private void handleConnect(JsonNode params, ObjectNode response) throws Exceptio String driverClass = requiredString(params, "driver_class", null); int poolMin = params.has("pool_min") ? params.get("pool_min").asInt(1) : 1; int poolMax = params.has("pool_max") ? params.get("pool_max").asInt(5) : 5; + boolean credentialsInUrl = params.has("credentials_in_url") && !params.get("credentials_in_url").isNull() + && params.get("credentials_in_url").asBoolean(false); List driverJars = new ArrayList<>(); if (params.has("driver_jars") && params.get("driver_jars").isArray()) { @@ -132,7 +134,7 @@ private void handleConnect(JsonNode params, ObjectNode response) throws Exceptio // Extract Oracle-specific connection options String tnsAdminDir = null; String walletPassword = null; - connectionManager.connect(connId, url, username, password, driverClass, driverJars, poolMin, poolMax); + connectionManager.connect(connId, url, username, password, driverClass, driverJars, poolMin, poolMax, credentialsInUrl); response.put("result", connId); } @@ -214,8 +216,10 @@ private void handleResolveDriver(JsonNode params, ObjectNode response) throws Ex ? params.get("version_cap").asText() : null; String classifier = params.has("maven_classifier") && !params.get("maven_classifier").isNull() ? params.get("maven_classifier").asText() : null; - - DriverResolver.DriverResult result = DriverResolver.resolve(mavenGroup, mavenArtifact, versionCap, classifier); + String downloadUrl = params.has("download_url") && !params.get("download_url").isNull() + ? params.get("download_url").asText() : null; + + DriverResolver.DriverResult result = DriverResolver.resolve(mavenGroup, mavenArtifact, versionCap, classifier, downloadUrl); ObjectNode resultNode = MAPPER.createObjectNode(); resultNode.put("jar_path", result.getJarPath()); diff --git a/src-tauri/src/database/jdbc_bridge/drivers.toml b/src-tauri/src/database/jdbc_bridge/drivers.toml index ddb84795..dbe5713d 100644 --- a/src-tauri/src/database/jdbc_bridge/drivers.toml +++ b/src-tauri/src/database/jdbc_bridge/drivers.toml @@ -162,12 +162,13 @@ version_error_signatures = [ [databases.xugudb] name = "虚谷 XuguDB" -class_name = "com.xugudb.jdbc.Driver" +class_name = "com.xugu.cloudjdbc.Driver" maven_group = "com.xugudb" -maven_artifact = "xugudb-jdbc" -jdbc_url_template = "jdbc:xugudb://{host}:{port}/{database}" +maven_artifact = "xugu-jdbc" +jdbc_url_template = "jdbc:xugu://{host}:{port}/{database}" default_port = 5138 min_jre_version = "11" +credentials_in_url = true version_error_signatures = [ "driver version not compatible", ] # ── 南大通用 GBase 8a ── @@ -180,6 +181,8 @@ maven_artifact = "gbase-connector-java" jdbc_url_template = "jdbc:gbase://{host}:{port}/{database}" default_port = 5258 min_jre_version = "11" +version_cap = "8.3.81.53" +download_url = "https://www.gbase8.cn/wp-content/uploads/2020/10/gbase-connector-java-8.3.81.53-build55.5.7-bin_min_mix.jar" version_error_signatures = [ "driver version not compatible", ] # ── Apache Hive ── @@ -332,3 +335,34 @@ jdbc_url_template = "jdbc:ucanaccess://{host}/{database}" default_port = 0 min_jre_version = "11" version_error_signatures = [ "driver version not compatible", ] + +# ── YashanDB (崖山数据库) ── +# +# Official JDBC driver from Yashan Technologies. Published on Maven Central. +# Uses `jdbc:yasdb:` protocol — NOT PG-wire compatible. + +[databases.yashandb] +name = "YashanDB (崖山数据库)" +class_name = "com.yashandb.jdbc.Driver" +maven_group = "com.yashandb" +maven_artifact = "yashandb-jdbc" +jdbc_url_template = "jdbc:yasdb://{host}:{port}/{database}" +default_port = 1688 +min_jre_version = "11" +version_error_signatures = [ "driver version not compatible", ] + +# ── KingbaseES (人大金仓) ── +# +# Official JDBC driver from 电科金仓. Published on Maven Central. +# Uses `jdbc:kingbase8:` protocol — PG-wire compatible but JDBC bridge +# gives access to advanced features (read-write splitting, Oracle-compat mode). + +[databases.kingbase] +name = "KingbaseES (人大金仓)" +class_name = "com.kingbase8.Driver" +maven_group = "cn.com.kingbase" +maven_artifact = "kingbase8" +jdbc_url_template = "jdbc:kingbase8://{host}:{port}/{database}" +default_port = 54321 +min_jre_version = "11" +version_error_signatures = [ "driver version not compatible", ] diff --git a/src-tauri/src/database/jdbc_bridge/fallback.rs b/src-tauri/src/database/jdbc_bridge/fallback.rs index c420d6be..631d13ae 100644 --- a/src-tauri/src/database/jdbc_bridge/fallback.rs +++ b/src-tauri/src/database/jdbc_bridge/fallback.rs @@ -123,6 +123,7 @@ pub async fn try_driver( "maven_artifact": config.maven_artifact, "version_cap": effective_cap, "maven_classifier": config.maven_classifier, + "download_url": config.download_url, }); let resolve_req = JdbcRequest::new(JdbcMethod::ResolveDriver, resolve_params); @@ -182,6 +183,7 @@ pub async fn try_driver( pool_min: 1, pool_max: 5, oracle_options: oracle_options.cloned(), + credentials_in_url: config.credentials_in_url, }) { Ok(v) => v, Err(e) => { diff --git a/src-tauri/src/database/jdbc_bridge/protocol.rs b/src-tauri/src/database/jdbc_bridge/protocol.rs index 5275734b..0a032573 100644 --- a/src-tauri/src/database/jdbc_bridge/protocol.rs +++ b/src-tauri/src/database/jdbc_bridge/protocol.rs @@ -78,6 +78,8 @@ pub struct ConnectParams { pub pool_max: u32, #[serde(default, skip_serializing_if = "Option::is_none")] pub oracle_options: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub credentials_in_url: Option, } fn default_pool_min() -> u32 { @@ -112,6 +114,8 @@ pub struct ResolveDriverParams { pub version_cap: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub maven_classifier: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub download_url: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src-tauri/src/database/jdbc_bridge/registry.rs b/src-tauri/src/database/jdbc_bridge/registry.rs index 46196e12..1a694051 100644 --- a/src-tauri/src/database/jdbc_bridge/registry.rs +++ b/src-tauri/src/database/jdbc_bridge/registry.rs @@ -55,6 +55,15 @@ pub struct DatabaseDriverConfig { /// When set, the download URL becomes: {artifact}-{version}-{classifier}.jar #[serde(default)] pub maven_classifier: Option, + /// Direct download URL for drivers NOT on Maven Central (e.g. GBase 8a). + /// When present, the bridge downloads the JAR directly from this URL + /// instead of resolving via Maven metadata. + #[serde(default)] + pub download_url: Option, + /// When true, append `?user=...&password=...` to the JDBC URL instead of + /// passing credentials via Properties. Some drivers (e.g. XuguDB) require this. + #[serde(default)] + pub credentials_in_url: Option, } // --------------------------------------------------------------------------- @@ -190,6 +199,8 @@ fn db_type_to_registry_key(db: DatabaseType) -> Option<&'static str> { DatabaseType::Cassandra => Some("cassandra"), DatabaseType::Iris => Some("iris"), DatabaseType::Access => Some("access"), + DatabaseType::YashanDB => Some("yashandb"), + DatabaseType::KingbaseES => Some("kingbase"), _ => None, } } @@ -275,6 +286,8 @@ mod tests { version_error_signatures: vec![], version_cap: None, maven_classifier: None, + download_url: None, + credentials_in_url: None, }; let url = build_jdbc_url(&config, "localhost", 9092, Some("testdb")); assert_eq!(url, "jdbc:h2:tcp://localhost:9092/testdb"); @@ -294,6 +307,8 @@ mod tests { version_error_signatures: vec![], version_cap: None, maven_classifier: None, + download_url: None, + credentials_in_url: None, }; let url = build_jdbc_url(&config, "localhost", 1521, Some("XEPDB1")); assert_eq!(url, "jdbc:oracle:thin:@localhost:1521:XEPDB1"); @@ -314,6 +329,8 @@ mod tests { version_error_signatures: vec![], version_cap: None, maven_classifier: None, + download_url: None, + credentials_in_url: None, }; let url = build_jdbc_url(&config, "localhost", 9092, None); assert_eq!(url, "jdbc:h2:tcp://localhost:9092/"); @@ -334,6 +351,8 @@ mod tests { version_error_signatures: vec![], version_cap: None, maven_classifier: None, + download_url: None, + credentials_in_url: None, }; let url = build_jdbc_url(&config, "10.0.0.1", 5236, None); assert_eq!(url, "jdbc:dm://10.0.0.1:5236"); @@ -397,6 +416,8 @@ mod tests { version_error_signatures: vec![], version_cap: None, maven_classifier: None, + download_url: None, + credentials_in_url: None, }; let url = build_jdbc_url(&config, "localhost", port, None); assert_eq!( @@ -473,6 +494,14 @@ mod tests { DriverRegistry::registry_key(DatabaseType::Access), Some("access") ); + assert_eq!( + DriverRegistry::registry_key(DatabaseType::YashanDB), + Some("yashandb") + ); + assert_eq!( + DriverRegistry::registry_key(DatabaseType::KingbaseES), + Some("kingbase") + ); } #[test] @@ -490,6 +519,8 @@ mod tests { assert!(keys.contains(&"oracle"), "oracle should be in the list"); assert!(keys.contains(&"db2"), "db2 should be in the list"); assert!(keys.contains(&"h2"), "h2 should be in the list"); + assert!(keys.contains(&"yashandb"), "yashandb should be in the list"); + assert!(keys.contains(&"kingbase"), "kingbase should be in the list"); } #[test] From f2a93d999c489b8981b703708f3dbbf4fd7a4687 Mon Sep 17 00:00:00 2001 From: blankll Date: Mon, 22 Jun 2026 11:35:25 +0800 Subject: [PATCH 02/17] feat(database): migrate YashanDB/KingbaseES to JDBC bridge, fix defaults Move YashanDB and KingbaseES from PG-wire compatibility to JDBC bridge in strategy.rs. Fix KingbaseES default_port from 5432 to 54321. Add username-as-dbname fallback in postgres.rs build_connection_string for PG-wire compat engines. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src-tauri/src/database/postgres.rs | 75 ++++++++++++++++++++++++++++-- src-tauri/src/database/strategy.rs | 14 ++++-- 2 files changed, 81 insertions(+), 8 deletions(-) diff --git a/src-tauri/src/database/postgres.rs b/src-tauri/src/database/postgres.rs index 8cb81039..5cd47b78 100644 --- a/src-tauri/src/database/postgres.rs +++ b/src-tauri/src/database/postgres.rs @@ -5,7 +5,7 @@ use crate::database::{ adapter::DatabaseAdapter, - config::{ConnectionConfig, SslMode}, + config::{ConnectionConfig, DatabaseType, SslMode}, error::{DbError, DbResult}, pool::ConnectionPool, types::{ @@ -138,6 +138,36 @@ fn deadpool_pool_error_to_db_error(error: deadpool_postgres::PoolError) -> DbErr } } +/// Enrich deadpool error with connection context (host:port, database) so even +/// generic messages like "error communicating with the server" include the target. +fn enrich_pool_error(error: deadpool_postgres::PoolError, config: &ConnectionConfig) -> DbError { + let target = format!("{}:{}", config.host, config.port); + let db_hint = config + .database + .as_ref() + .map(|d| format!(" database={}", d)) + .unwrap_or_default(); + let ctx = format!(" (connecting to {}{})", target, db_hint); + let err = deadpool_pool_error_to_db_error(error); + match err { + DbError::Connection(msg) => { + if msg.contains(&target) { + DbError::Connection(msg) + } else { + DbError::Connection(format!("{}{}", msg, ctx)) + } + } + DbError::Timeout(msg) => { + if msg.contains(&target) { + DbError::Timeout(msg) + } else { + DbError::Timeout(format!("{}{}", msg, ctx)) + } + } + other => DbError::Connection(format!("{}{}", other, ctx)), + } +} + /// PostgreSQL connection pool wrapper. pub struct PostgresPool { pool: Pool, @@ -227,6 +257,17 @@ impl PostgresAdapter { if let Some(ref database) = self.config.database { parts.push(format!("dbname={}", database)); + } else if matches!(self.config.db_type, DatabaseType::PostgreSQL) { + // Real PostgreSQL servers always have a 'postgres' maintenance DB; + // use it as the default when the user leaves the field empty. + parts.push("dbname=postgres".to_string()); + } else { + // PG-wire-compat engines (KingbaseES, OpenGauss, HighGo, etc.) + // typically don't ship a 'postgres' database. The libpq convention + // is to fall back to the username-matching database, but + // deadpool-postgres does not implement this fallback — it requires + // an explicit dbname. Use the username as the fallback here. + parts.push(format!("dbname={}", self.config.username)); } // Add SSL mode @@ -239,6 +280,9 @@ impl PostgresAdapter { }; parts.push(format!("sslmode={}", ssl_mode)); + // Add connection timeout + parts.push(format!("connect_timeout={}", self.config.connect_timeout_secs)); + // Add additional options for (key, value) in &self.config.options { parts.push(format!("{}={}", key, value)); @@ -821,7 +865,10 @@ impl DatabaseAdapter for PostgresAdapter { } }; - let _client = pool.get().await.map_err(deadpool_pool_error_to_db_error)?; + let _client = pool + .get() + .await + .map_err(|e| enrich_pool_error(e, &self.config))?; self.pool = Some(Arc::new(PostgresPool { pool })); @@ -2172,7 +2219,29 @@ mod tests { assert!(conn_str.contains("port=5432")); assert!(conn_str.contains("user=postgres")); assert!(conn_str.contains("password=password")); - assert!(!conn_str.contains("dbname=")); + assert!(conn_str.contains("dbname=postgres")); + } + + #[test] + fn test_pg_compat_without_database_falls_back_to_username() { + let config = + ConnectionConfig::new(DatabaseType::OpenGauss, "10.84.1.213", 5432, "SYSTEM") + .with_password("kingbase@123"); + + let adapter = PostgresAdapter::new(config); + let conn_str = adapter.build_connection_string(); + + assert!(conn_str.contains("host=10.84.1.213")); + assert!(conn_str.contains("port=5432")); + assert!(conn_str.contains("user=SYSTEM")); + assert!( + conn_str.contains("dbname=SYSTEM"), + "PG-wire-compat engines must fall back to username as dbname (libpq convention)" + ); + assert!( + !conn_str.contains("dbname=postgres"), + "PG-wire-compat engines must not force dbname=postgres" + ); } #[test] diff --git a/src-tauri/src/database/strategy.rs b/src-tauri/src/database/strategy.rs index 68631f1f..59175ac4 100644 --- a/src-tauri/src/database/strategy.rs +++ b/src-tauri/src/database/strategy.rs @@ -43,8 +43,8 @@ pub fn resolve_effective_type(db: DatabaseType) -> ConnectionStrategy { // Native PG adapter PostgreSQL => ConnectionStrategy::Native(CoreDatabaseType::PostgreSQL), // PG wire protocol compat - CockroachDB | Redshift | YugabyteDB | TimescaleDB | KingbaseES | GaussDB | HighGo - | UXDB | OpenGauss | GBase8c | QuestDB | Vastbase | YashanDB + CockroachDB | Redshift | YugabyteDB | TimescaleDB | GaussDB | HighGo + | UXDB | OpenGauss | GBase8c | QuestDB | Vastbase | Greenplum | EnterpriseDB | CrateDB | Materialize | AlloyDB | CloudSQLPG | FujitsuPG => { ConnectionStrategy::Native(CoreDatabaseType::PostgreSQL) @@ -88,6 +88,8 @@ pub fn resolve_effective_type(db: DatabaseType) -> ConnectionStrategy { Cassandra => ConnectionStrategy::JdbcBridge, Iris => ConnectionStrategy::JdbcBridge, Access => ConnectionStrategy::JdbcBridge, + YashanDB => ConnectionStrategy::JdbcBridge, + KingbaseES => ConnectionStrategy::JdbcBridge, // HTTP SQL bridge Trino | Presto => ConnectionStrategy::Http, @@ -117,12 +119,13 @@ pub fn is_pg_family(db: DatabaseType) -> bool { pub fn default_port(db: DatabaseType) -> Option { use DatabaseType::*; match db { - PostgreSQL | CockroachDB | Redshift | YugabyteDB | TimescaleDB | KingbaseES | GaussDB + PostgreSQL | CockroachDB | Redshift | YugabyteDB | TimescaleDB | GaussDB | HighGo | UXDB | OpenGauss | GBase8c | Vastbase | Greenplum | EnterpriseDB | CrateDB | Materialize | AlloyDB | CloudSQLPG | FujitsuPG => Some(5432), QuestDB => Some(8812), YashanDB => Some(1688), + KingbaseES => Some(54321), MySQL | MariaDB | TiDB | OceanBase | TDSQL | PolarDB | GoldenDB | SingleStoreMemSQL | CloudSQLMySQL => Some(3306), Doris | SelectDB | StarRocks => Some(9030), @@ -171,12 +174,10 @@ mod tests { DatabaseType::PostgreSQL, DatabaseType::CockroachDB, DatabaseType::Redshift, - DatabaseType::KingbaseES, DatabaseType::GaussDB, DatabaseType::HighGo, DatabaseType::QuestDB, DatabaseType::Vastbase, - DatabaseType::YashanDB, DatabaseType::Greenplum, DatabaseType::EnterpriseDB, DatabaseType::CrateDB, @@ -272,6 +273,8 @@ mod tests { DatabaseType::Cassandra, DatabaseType::Iris, DatabaseType::Access, + DatabaseType::YashanDB, + DatabaseType::KingbaseES, ] { assert_eq!( resolve_effective_type(db), @@ -315,6 +318,7 @@ mod tests { assert_eq!(default_port(DatabaseType::QuestDB), Some(8812)); assert_eq!(default_port(DatabaseType::Vastbase), Some(5432)); assert_eq!(default_port(DatabaseType::YashanDB), Some(1688)); + assert_eq!(default_port(DatabaseType::KingbaseES), Some(54321)); assert_eq!(default_port(DatabaseType::Hive), Some(10000)); assert_eq!(default_port(DatabaseType::Databricks), Some(443)); assert_eq!(default_port(DatabaseType::Hana), Some(30015)); From d94fc344ab35d351cc6d85f1ad8af07b3dd5d745 Mon Sep 17 00:00:00 2001 From: blankll Date: Mon, 22 Jun 2026 11:35:40 +0800 Subject: [PATCH 03/17] feat(ui): add SearchableSelect component and integrate in connection form Create SearchableSelect.vue with search/filter, keyboard navigation, and slot-based icon support. Replace 80+ static SelectItem entries in ServerFormDialog with the searchable version. Dropdown width matches trigger width via CSS var(--radix-popover-trigger-width). Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../connections/ServerFormDialog.vue | 537 +++++------------- .../ui/combobox/SearchableSelect.vue | 310 ++++++++++ src/components/ui/combobox/index.ts | 2 + src/components/ui/combobox/types.ts | 5 + 4 files changed, 447 insertions(+), 407 deletions(-) create mode 100644 src/components/ui/combobox/SearchableSelect.vue create mode 100644 src/components/ui/combobox/index.ts create mode 100644 src/components/ui/combobox/types.ts diff --git a/src/components/connections/ServerFormDialog.vue b/src/components/connections/ServerFormDialog.vue index feded3fc..4ddcc789 100644 --- a/src/components/connections/ServerFormDialog.vue +++ b/src/components/connections/ServerFormDialog.vue @@ -6,6 +6,7 @@ import { open as openDialog } from '@tauri-apps/plugin-dialog' import { computed, onUnmounted, ref, watch } from 'vue' import { useI18n } from 'vue-i18n' import { Button } from '@/components/ui/button' +import { SearchableSelect } from '@/components/ui/combobox' import { Dialog, DialogContent, DialogDescription, DialogTitle } from '@/components/ui/dialog' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' @@ -21,7 +22,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { useDatabaseIcon } from '@/composables/useDatabaseIcon' import { toast } from '@/composables/useNotifications' import { jdbcApi } from '@/datasources/jdbcApi' -import { buildOracleOptions, buildTransportLayers, DatabaseType, dbTypeToBackend, resolveDatabase } from '@/store' +import { buildOracleOptions, buildTransportLayers, databasePlaceholderFor, DatabaseType, dbTypeToBackend, isDatabaseRequired, isJdbcDatabase, resolveDatabase } from '@/store' import { DEFAULT_SSL_MODE, sslModeToBackend, validateSslConfig } from '@/types/connection' import SslConfigSection from './ssl/SslConfigSection.vue' @@ -337,6 +338,21 @@ const isFileBased = computed(() => || formData.value.type === DatabaseType.DUCKDB, ) +const isJdbcDb = computed(() => isJdbcDatabase(formData.value.type)) + +const dbTypeI18nKeyMap: Record = { + MANTICORESEARCH: 'manticore', +} + +const databaseTypeOptions = computed(() => + Object.values(DatabaseType).map((value) => { + const i18nKey = dbTypeI18nKeyMap[value] || value.toLowerCase() + const fullKey = `components.serverForm.databaseTypes.${i18nKey}` + const label = t(fullKey) + return { label: label === fullKey ? value : label, value } + }), +) + // ── Oracle-specific state ── const isOracle = computed(() => formData.value.type === DatabaseType.ORACLE) const oracleMethod = ref<'basic' | 'tns' | 'cloud_wallet'>('basic') @@ -371,6 +387,12 @@ const databaseLabel = computed(() => { return t('components.serverForm.labels.database') }) +const isDbNameRequired = computed(() => isDatabaseRequired(formData.value.type)) + +const databasePlaceholder = computed(() => + databasePlaceholderFor[formData.value.type] || t('components.serverForm.placeholders.database'), +) + // Reset Oracle options when toggling method function resetOracleOptions() { oracleMethod.value = 'basic' @@ -551,6 +573,10 @@ function validateForm(): boolean { if (isOracle.value && !formData.value.database?.trim()) { errors.database = t('components.serverForm.errors.databaseRequired') } + // For engines with mode-dependent or no default database (EnterpriseDB, TimescaleDB, Redshift) + if (isDbNameRequired.value && !formData.value.database?.trim()) { + errors.database = t('components.serverForm.errors.databaseNameRequired') + } const dbTypeBackend = mapDatabaseTypeToBackend(formData.value.type) const sslErrors = validateSslConfig(formData.value.ssl, dbTypeBackend) @@ -582,53 +608,56 @@ async function handleTestConnection() { await startProgressListener() try { - // Run all three downloads in parallel — none depend on each other - const jreStep = setupSteps.value.find(s => s.id === 'jre')! - const bridgeStep = setupSteps.value.find(s => s.id === 'bridge')! - const driverStep = setupSteps.value.find(s => s.id === 'driver')! - const dbType = mapDatabaseTypeToBackend(formData.value.type) - const results = await Promise.allSettled([ - // JRE download (skips if already installed) - invoke('check_jre_status').then(async (status: any) => { - if (!status.installed) { - const ok = await runStep(jreStep) - if (!ok) - throw new Error('JRE download failed') - } - else { jreStep.status = 'done' } - }), - // Bridge download (skips if already installed) - invoke('check_bridge_status').then(async (status: any) => { - if (!status.installed) { - const ok = await runStep(bridgeStep) - if (!ok) - throw new Error('Bridge download failed') - } - else { bridgeStep.status = 'done' } - }), - // JDBC driver download directly from Maven Central (no Java needed) - (async () => { - try { - await invoke('download_jdbc_driver_direct', { dbType }) - driverStep.status = 'done' - } - catch { - driverStep.status = 'done' // Non-fatal - } - })(), - ]) - - // If JRE or Bridge failed, stop (keep steps visible with error state) - if (results[0].status === 'rejected' || results[1].status === 'rejected') { - const firstError = setupSteps.value.find(s => s.status === 'error') - if (firstError?.error) - testError.value = firstError.error - return + // For JDBC bridge databases, ensure JRE/bridge/driver are ready first + if (isJdbcDb.value) { + const jreStep = setupSteps.value.find(s => s.id === 'jre')! + const bridgeStep = setupSteps.value.find(s => s.id === 'bridge')! + const driverStep = setupSteps.value.find(s => s.id === 'driver')! + + const results = await Promise.allSettled([ + // JRE download (skips if already installed) + invoke('check_jre_status').then(async (status: any) => { + if (!status.installed) { + const ok = await runStep(jreStep) + if (!ok) + throw new Error('JRE download failed') + } + else { jreStep.status = 'done' } + }), + // Bridge download (skips if already installed) + invoke('check_bridge_status').then(async (status: any) => { + if (!status.installed) { + const ok = await runStep(bridgeStep) + if (!ok) + throw new Error('Bridge download failed') + } + else { bridgeStep.status = 'done' } + }), + // JDBC driver download directly from Maven Central (no Java needed) + (async () => { + try { + await invoke('download_jdbc_driver_direct', { dbType }) + driverStep.status = 'done' + } + catch { + driverStep.status = 'done' // Non-fatal + } + })(), + ]) + + // If JRE or Bridge failed, stop (keep steps visible with error state) + if (results[0].status === 'rejected' || results[1].status === 'rejected') { + const firstError = setupSteps.value.find(s => s.status === 'error') + if (firstError?.error) + testError.value = firstError.error + return + } } + const driverStep = isJdbcDb.value ? setupSteps.value.find(s => s.id === 'driver') : undefined - // Step 4: Test the actual connection + // Test the actual connection const config = { id: formData.value.id || crypto.randomUUID(), name: formData.value.name, @@ -653,13 +682,19 @@ async function handleTestConnection() { const result = await invoke<{ is_connected: boolean, server_version?: string }>('test_connection', { config }) if (result.is_connected) { - driverStep.status = 'done' + if (result.server_version) { + formData.value.serverVersion = result.server_version + } + if (driverStep) + driverStep.status = 'done' testStatus.value = 'success' } else { - driverStep.status = 'error' + if (driverStep) + driverStep.status = 'error' const msg = 'Connection returned not connected' - driverStep.error = msg + if (driverStep) + driverStep.error = msg testError.value = msg testStatus.value = 'error' } @@ -670,10 +705,10 @@ async function handleTestConnection() { testError.value = msg // If the error is Java-related, also mark JRE as failed so user can retry it if (msg.includes('Unable to locate a Java Runtime') || msg.includes('Java not found')) { - const jreStep = setupSteps.value.find(s => s.id === 'jre') - if (jreStep) { - jreStep.status = 'error' - jreStep.error = 'JRE is missing or broken' + const jreStepFound = setupSteps.value.find(s => s.id === 'jre') + if (jreStepFound) { + jreStepFound.status = 'error' + jreStepFound.error = 'JRE is missing or broken' } } const running = setupSteps.value.find(s => s.status === 'running') @@ -776,354 +811,33 @@ function handleSave() {
- + + + +
@@ -1155,12 +869,19 @@ function handleSave() {
- + +

+ {{ formErrors.database }} +

@@ -1607,14 +1328,14 @@ function handleSave() {
s.status !== 'pending'))" class="p-3 rounded-md" :class="{ 'bg-blue-50 dark:bg-blue-900/10': testStatus === 'testing', 'bg-green-50 dark:bg-green-900/10': testStatus === 'success', 'bg-red-50 dark:bg-red-900/10': testStatus === 'error', }" > - -
+ +
+
+
-
+
{{ t('common.status.testing') }}
-
+
{{ t('common.status.success') }}
-
+
{{ t('common.status.failed') }} diff --git a/src/components/ui/combobox/SearchableSelect.vue b/src/components/ui/combobox/SearchableSelect.vue new file mode 100644 index 00000000..b1593109 --- /dev/null +++ b/src/components/ui/combobox/SearchableSelect.vue @@ -0,0 +1,310 @@ + + + diff --git a/src/components/ui/combobox/index.ts b/src/components/ui/combobox/index.ts new file mode 100644 index 00000000..50a9edfa --- /dev/null +++ b/src/components/ui/combobox/index.ts @@ -0,0 +1,2 @@ +export { default as SearchableSelect } from './SearchableSelect.vue' +export type { ComboboxOption } from './types' diff --git a/src/components/ui/combobox/types.ts b/src/components/ui/combobox/types.ts new file mode 100644 index 00000000..983fc113 --- /dev/null +++ b/src/components/ui/combobox/types.ts @@ -0,0 +1,5 @@ +export type ComboboxOption = { + label: string + value: string + disabled?: boolean +} From d6e1af0354408236d65224396a96c583844e3cc9 Mon Sep 17 00:00:00 2001 From: blankll Date: Mon, 22 Jun 2026 11:35:54 +0800 Subject: [PATCH 04/17] fix(ui): limit ConnectionsPage scroll to card list only Restructure layout so header and stats are fixed (shrink-0), only the connection card list scrolls (flex-1 min-h-0 overflow-y-auto). Prevents the whole page from scrolling. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/pages/ConnectionsPage.vue | 249 +++++++++++++++++----------------- 1 file changed, 124 insertions(+), 125 deletions(-) diff --git a/src/pages/ConnectionsPage.vue b/src/pages/ConnectionsPage.vue index 8a4465bd..9a018295 100644 --- a/src/pages/ConnectionsPage.vue +++ b/src/pages/ConnectionsPage.vue @@ -203,8 +203,8 @@ function getConnectionStatus(connectionId: string | undefined): ConnectionStatus