From bab94cc52b58a4485096338e042a759bda6a1777 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Fri, 10 Jul 2026 21:25:17 +0800 Subject: [PATCH] fix(jdbc): support reusable multi-JAR driver bundles --- .github/workflows/agents-release.yml | 2 +- .../components/config/DriverStoreDialog.vue | 41 +++- .../connection/ConnectionDialog.vue | 14 +- apps/desktop/src/lib/backend/api.ts | 2 + apps/desktop/src/lib/backend/http.ts | 9 + apps/desktop/src/lib/backend/tauri.ts | 9 + apps/desktop/src/types/database.ts | 15 ++ crates/dbx-core/src/jdbc.rs | 177 +++++++++++++++++- crates/dbx-web/src/main.rs | 2 + crates/dbx-web/src/routes/jdbc.rs | 35 +++- .../main/java/app/dbx/jdbc/DbxJdbcPlugin.java | 13 +- .../app/dbx/jdbc/maven/DbxMavenResolver.java | 13 +- .../java/app/dbx/jdbc/DbxJdbcPluginTest.java | 22 +++ src-tauri/src/commands/plugins.rs | 23 ++- src-tauri/src/lib.rs | 2 + 15 files changed, 355 insertions(+), 24 deletions(-) diff --git a/.github/workflows/agents-release.yml b/.github/workflows/agents-release.yml index 469b9155d9..33cc9746e7 100644 --- a/.github/workflows/agents-release.yml +++ b/.github/workflows/agents-release.yml @@ -174,7 +174,7 @@ jobs: include: - jre-key: "21" java-version: "21" - modules: "java.base,java.sql,java.sql.rowset,java.naming,java.management,java.desktop,java.security.jgss,java.security.sasl,jdk.charsets,java.scripting" + modules: "java.base,java.sql,java.sql.rowset,java.naming,java.management,java.desktop,java.security.jgss,java.security.sasl,jdk.charsets,java.scripting,java.compiler" steps: - uses: actions/setup-java@v4 with: diff --git a/apps/desktop/src/components/config/DriverStoreDialog.vue b/apps/desktop/src/components/config/DriverStoreDialog.vue index 4e3d8f44b9..56d14a5c20 100644 --- a/apps/desktop/src/components/config/DriverStoreDialog.vue +++ b/apps/desktop/src/components/config/DriverStoreDialog.vue @@ -14,7 +14,7 @@ import DatabaseIcon from "@/components/icons/DatabaseIcon.vue"; import { useToast } from "@/composables/useToast"; import { isTauriRuntime } from "@/lib/backend/tauriRuntime"; import { countAvailableDriverUpdates } from "@/lib/connection/agentDriverUpdateBadge"; -import type { JdbcDriverInfo, JdbcMavenBundleInfo, JdbcPluginStatus } from "@/types/database"; +import type { JdbcDriverInfo, JdbcLocalBundleInfo, JdbcMavenBundleInfo, JdbcPluginStatus } from "@/types/database"; import * as api from "@/lib/backend/api"; import type { AgentDriverInfo, DriverRuntimeInfo, DriverRuntimeSummary, DriverStoreUsage, JavaRuntimeConfig } from "@/lib/backend/api"; import { formatRuntimeBytes, formatRuntimeCpu, formatRuntimeUptime, runtimeHealthClass, runtimeStatusClass, runtimeStatusDotClass } from "@/lib/connection/driverRuntimePresentation"; @@ -602,6 +602,7 @@ function formatSize(bytes: number): string { const jdbcDrivers = ref([]); const jdbcMavenBundles = ref([]); +const jdbcLocalBundles = ref([]); const jdbcDriverSearch = ref(""); const isLoadingJdbcDrivers = ref(false); const jdbcPluginStatus = ref(null); @@ -631,6 +632,15 @@ type JdbcDriverListItem = size: number; driver: JdbcDriverInfo; } + | { + kind: "local"; + id: string; + title: string; + subtitle: string; + source: string; + size: number; + bundle: JdbcLocalBundleInfo; + } | { kind: "maven"; id: string; @@ -648,6 +658,15 @@ const filteredAgentDrivers = computed(() => { }); const jdbcDriverListItems = computed(() => { + const localBundleItems = jdbcLocalBundles.value.map((bundle) => ({ + kind: "local" as const, + id: `local:${bundle.id}`, + title: bundle.name, + subtitle: `${bundle.artifacts.length} JARs - ${bundle.artifacts.map((artifact) => artifact.file_name).join(", ")}`, + source: t("driverStore.jdbcSourceManual"), + size: bundle.artifacts.reduce((total, artifact) => total + Number(artifact.size || 0), 0), + bundle, + })); const bundleItems = jdbcMavenBundles.value.map((bundle) => ({ kind: "maven" as const, id: `maven:${bundle.id}`, @@ -668,7 +687,7 @@ const jdbcDriverListItems = computed(() => { size: driver.size, driver, })); - return [...bundleItems, ...manualItems].sort((a, b) => a.title.localeCompare(b.title)); + return [...localBundleItems, ...bundleItems, ...manualItems].sort((a, b) => a.title.localeCompare(b.title)); }); const filteredJdbcDrivers = computed(() => { @@ -792,9 +811,10 @@ function jreUsageLabel(key: string) { async function loadJdbcDrivers() { isLoadingJdbcDrivers.value = true; try { - const [drivers, bundles] = await Promise.all([api.listJdbcDrivers(), api.listJdbcMavenBundles()]); + const [drivers, bundles, localBundles] = await Promise.all([api.listJdbcDrivers(), api.listJdbcMavenBundles(), api.listJdbcLocalBundles()]); jdbcDrivers.value = drivers; jdbcMavenBundles.value = bundles; + jdbcLocalBundles.value = localBundles; } catch (e: any) { toast(String(e?.message || e), 5000); } finally { @@ -898,6 +918,7 @@ async function importJdbcDriverPaths(paths: string[]) { if (!paths.length) return; try { jdbcDrivers.value = await api.importJdbcDrivers(paths); + jdbcLocalBundles.value = await api.listJdbcLocalBundles(); jdbcDriverPathInput.value = ""; void loadDriverStoreUsage(); toast(t("settings.jdbcImportSuccess", { count: paths.length })); @@ -912,6 +933,7 @@ async function importJdbcDrivers() { if (!files || !files.length) return; try { jdbcDrivers.value = await api.importJdbcDrivers(files); + jdbcLocalBundles.value = await api.listJdbcLocalBundles(); void loadDriverStoreUsage(); toast(t("settings.jdbcImportSuccess", { count: files.length })); } catch (e: any) { @@ -978,6 +1000,17 @@ async function deleteJdbcMavenBundle(bundleId: string) { } } +async function deleteJdbcLocalBundle(bundleId: string) { + try { + jdbcDrivers.value = await api.deleteJdbcLocalBundle(bundleId); + jdbcLocalBundles.value = await api.listJdbcLocalBundles(); + void loadDriverStoreUsage(); + toast(t("settings.jdbcDeleteSuccess")); + } catch (e: any) { + toast(String(e?.message || e), 5000); + } +} + // ──────────── Lifecycle ──────────── onMounted(async () => { @@ -1300,7 +1333,7 @@ watch(driverStoreTab, (tab) => {
{{ item.subtitle }}
{{ formatBytes(item.size) }}
- diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index ce2a7e6678..6529a35ace 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -13,7 +13,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Switch } from "@/components/ui/switch"; -import type { ConnectionConfig, DatabaseType, HttpTunnelConfig, JdbcDriverInfo, JdbcMavenBundleInfo, ProxyTunnelConfig, SshConfigHostEntry, SshTunnelConfig, TransportLayerConfig } from "@/types/database"; +import type { ConnectionConfig, DatabaseType, HttpTunnelConfig, JdbcDriverInfo, JdbcLocalBundleInfo, JdbcMavenBundleInfo, ProxyTunnelConfig, SshConfigHostEntry, SshTunnelConfig, TransportLayerConfig } from "@/types/database"; import type { InfluxDbExternalConfig, InfluxDbVersion } from "@/types/influxdb"; import type { MqAdminConfig, MqAuth, MqSystemKind } from "@/types/mq"; import type { NacosAdminConfig, NacosAuthConfig } from "@/types/nacos"; @@ -369,6 +369,7 @@ const mongoUseUrl = ref(false); const jdbcDriverPathsInput = ref(""); const jdbcDrivers = ref([]); const jdbcMavenBundles = ref([]); +const jdbcLocalBundles = ref([]); const sshConfigHosts = ref([]); const agentDrivers = ref([]); const selectedJdbcDriverPath = ref(""); @@ -457,6 +458,11 @@ const customColorInput = ref(""); const customColorOpen = ref(false); const jdbcDriverSelectItems = computed(() => { + const localBundles = jdbcLocalBundles.value.map((bundle) => ({ + id: `local:${bundle.id}`, + label: bundle.name, + paths: bundle.artifacts.map((artifact) => artifact.path), + })); const bundles = jdbcMavenBundles.value.map((bundle) => ({ id: `maven:${bundle.id}`, label: bundle.coordinate, @@ -469,7 +475,7 @@ const jdbcDriverSelectItems = computed(() => { label: driver.name, paths: [driver.path], })); - return [...bundles, ...manual].sort((left, right) => left.label.localeCompare(right.label)); + return [...localBundles, ...bundles, ...manual].sort((left, right) => left.label.localeCompare(right.label)); }); const jdbcDriverSelectItemById = computed(() => new Map(jdbcDriverSelectItems.value.map((item) => [item.id, item]))); @@ -3479,13 +3485,15 @@ async function browseJdbcDriverPaths() { async function loadJdbcDrivers() { if (!isDesktop) return; try { - const [drivers, bundles] = await Promise.all([api.listJdbcDrivers(), api.listJdbcMavenBundles()]); + const [drivers, bundles, localBundles] = await Promise.all([api.listJdbcDrivers(), api.listJdbcMavenBundles(), api.listJdbcLocalBundles()]); jdbcDrivers.value = drivers; jdbcMavenBundles.value = bundles; + jdbcLocalBundles.value = localBundles; applyPrestoSqlBuiltinDriverPathsIfAvailable(); } catch { jdbcDrivers.value = []; jdbcMavenBundles.value = []; + jdbcLocalBundles.value = []; } } diff --git a/apps/desktop/src/lib/backend/api.ts b/apps/desktop/src/lib/backend/api.ts index db30955db0..a1822d6944 100644 --- a/apps/desktop/src/lib/backend/api.ts +++ b/apps/desktop/src/lib/backend/api.ts @@ -64,11 +64,13 @@ export const decryptConfig = forward("decryptConfig"); export const listPlugins = forward("listPlugins"); export const listJdbcDrivers = forward("listJdbcDrivers"); export const listJdbcMavenBundles = forward("listJdbcMavenBundles"); +export const listJdbcLocalBundles = forward("listJdbcLocalBundles"); export const importJdbcDrivers = forward("importJdbcDrivers"); export const installJdbcDriverFromMaven = forward("installJdbcDriverFromMaven"); export const installPrestoSqlJdbcDriver = forward("installPrestoSqlJdbcDriver"); export const deleteJdbcDriver = forward("deleteJdbcDriver"); export const deleteJdbcMavenBundle = forward("deleteJdbcMavenBundle"); +export const deleteJdbcLocalBundle = forward("deleteJdbcLocalBundle"); export const jdbcPluginStatus = forward("jdbcPluginStatus"); export const installJdbcPlugin = forward("installJdbcPlugin"); export const installJdbcPluginLocal = forward("installJdbcPluginLocal"); diff --git a/apps/desktop/src/lib/backend/http.ts b/apps/desktop/src/lib/backend/http.ts index 582513088c..0cdb3dcfe1 100644 --- a/apps/desktop/src/lib/backend/http.ts +++ b/apps/desktop/src/lib/backend/http.ts @@ -25,6 +25,7 @@ import type { DatabaseType, InstalledPlugin, JdbcDriverInfo, + JdbcLocalBundleInfo, JdbcMavenBundleInfo, JdbcPluginStatus, SidebarLayout, @@ -267,6 +268,10 @@ export async function listJdbcMavenBundles(): Promise { return get("/api/jdbc/drivers/maven"); } +export async function listJdbcLocalBundles(): Promise { + return get("/api/jdbc/drivers/local"); +} + export async function importJdbcDrivers(pathsOrFiles: (string | File)[]): Promise { const formData = new FormData(); for (const item of pathsOrFiles) { @@ -300,6 +305,10 @@ export async function deleteJdbcMavenBundle(bundleId: string): Promise { + return del(`/api/jdbc/drivers/local/${encodeURIComponent(bundleId)}`); +} + export async function jdbcPluginStatus(): Promise { return get("/api/jdbc/plugin/status"); } diff --git a/apps/desktop/src/lib/backend/tauri.ts b/apps/desktop/src/lib/backend/tauri.ts index 542dfd93cf..34293bf0f6 100644 --- a/apps/desktop/src/lib/backend/tauri.ts +++ b/apps/desktop/src/lib/backend/tauri.ts @@ -27,6 +27,7 @@ import type { DatabaseType, InstalledPlugin, JdbcDriverInfo, + JdbcLocalBundleInfo, JdbcMavenBundleInfo, JdbcPluginStatus, SavedSqlFile, @@ -1131,6 +1132,10 @@ export async function listJdbcMavenBundles(): Promise { return invoke("list_jdbc_maven_bundles"); } +export async function listJdbcLocalBundles(): Promise { + return invoke("list_jdbc_local_bundles"); +} + export async function importJdbcDrivers(paths: (string | File)[]): Promise { if (paths.some((path) => typeof path !== "string")) { throw new Error("Desktop JDBC driver import requires local file paths"); @@ -1154,6 +1159,10 @@ export async function deleteJdbcMavenBundle(bundleId: string): Promise { + return invoke("delete_jdbc_local_bundle", { bundleId }); +} + export async function jdbcPluginStatus(): Promise { return invoke("jdbc_plugin_status"); } diff --git a/apps/desktop/src/types/database.ts b/apps/desktop/src/types/database.ts index 4226c12538..241b5c79d5 100644 --- a/apps/desktop/src/types/database.ts +++ b/apps/desktop/src/types/database.ts @@ -272,6 +272,21 @@ export interface JdbcMavenBundleInfo { artifacts: JdbcMavenArtifactInfo[]; } +export interface JdbcLocalArtifactInfo { + file_name: string; + path: string; + size: number; + sha256: string; +} + +export interface JdbcLocalBundleInfo { + id: string; + name: string; + installed_at: string; + path: string; + artifacts: JdbcLocalArtifactInfo[]; +} + export interface JdbcPluginStatus { installed: boolean; version?: string | null; diff --git a/crates/dbx-core/src/jdbc.rs b/crates/dbx-core/src/jdbc.rs index a63403de1d..a5778c0a27 100644 --- a/crates/dbx-core/src/jdbc.rs +++ b/crates/dbx-core/src/jdbc.rs @@ -54,6 +54,23 @@ pub struct JdbcMavenArtifactInfo { pub sha256: String, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct JdbcLocalBundleInfo { + pub id: String, + pub name: String, + pub installed_at: String, + pub path: String, + pub artifacts: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct JdbcLocalArtifactInfo { + pub file_name: String, + pub path: String, + pub size: u64, + pub sha256: String, +} + #[derive(Debug, Clone, Serialize)] pub struct JdbcPluginStatus { pub installed: bool, @@ -76,7 +93,15 @@ pub fn list_jdbc_maven_bundles(plugins_root: &Path) -> Result Result, String> { + list_jdbc_local_bundles_from_dir(&jdbc_local_drivers_dir(plugins_root)) +} + pub fn import_jdbc_drivers(plugins_root: &Path, paths: &[String]) -> Result, String> { + if paths.len() > 1 { + install_jdbc_local_bundle(plugins_root, paths)?; + return list_jdbc_drivers(plugins_root); + } let drivers_dir = jdbc_drivers_dir(plugins_root); std::fs::create_dir_all(&drivers_dir).map_err(|err| err.to_string())?; @@ -126,6 +151,17 @@ pub fn delete_jdbc_maven_bundle(plugins_root: &Path, bundle_id: &str) -> Result< list_jdbc_drivers(plugins_root) } +pub fn delete_jdbc_local_bundle(plugins_root: &Path, bundle_id: &str) -> Result, String> { + if !is_safe_bundle_id(bundle_id) { + return Err("Invalid JDBC local bundle id".to_string()); + } + let target = jdbc_local_drivers_dir(plugins_root).join(bundle_id); + if target.exists() { + std::fs::remove_dir_all(&target).map_err(|err| err.to_string())?; + } + list_jdbc_drivers(plugins_root) +} + pub async fn install_jdbc_driver_from_maven( plugins_root: &Path, request: JdbcMavenInstallRequest, @@ -309,6 +345,10 @@ fn jdbc_maven_drivers_dir(plugins_root: &Path) -> PathBuf { jdbc_drivers_dir(plugins_root).join("maven") } +fn jdbc_local_drivers_dir(plugins_root: &Path) -> PathBuf { + jdbc_drivers_dir(plugins_root).join("local") +} + fn jdbc_maven_resolver_executable(plugin_dir: &Path) -> PathBuf { #[cfg(windows)] { @@ -504,9 +544,14 @@ fn list_jdbc_drivers_from_dir(drivers_dir: &Path) -> Result, for entry in entries { let entry = entry.map_err(|err| err.to_string())?; let path = entry.path(); - if path.file_name().and_then(|name| name.to_str()) == Some("maven") && path.is_dir() { - append_jdbc_maven_driver_jars(&mut drivers, &path)?; - continue; + if path.is_dir() { + match path.file_name().and_then(|name| name.to_str()) { + Some("maven") | Some("local") => { + append_jdbc_bundle_jars(&mut drivers, &path)?; + continue; + } + _ => {} + } } if path.extension().and_then(|ext| ext.to_str()).map(|ext| ext.eq_ignore_ascii_case("jar")) != Some(true) { continue; @@ -523,8 +568,8 @@ fn list_jdbc_drivers_from_dir(drivers_dir: &Path) -> Result, Ok(drivers) } -fn append_jdbc_maven_driver_jars(drivers: &mut Vec, maven_dir: &Path) -> Result<(), String> { - let entries = match std::fs::read_dir(maven_dir) { +fn append_jdbc_bundle_jars(drivers: &mut Vec, bundles_dir: &Path) -> Result<(), String> { + let entries = match std::fs::read_dir(bundles_dir) { Ok(entries) => entries, Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()), Err(err) => return Err(err.to_string()), @@ -560,6 +605,86 @@ fn append_jdbc_maven_driver_jars(drivers: &mut Vec, maven_dir: & Ok(()) } +fn list_jdbc_local_bundles_from_dir(local_dir: &Path) -> Result, String> { + let entries = match std::fs::read_dir(local_dir) { + Ok(entries) => entries, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(vec![]), + Err(err) => return Err(err.to_string()), + }; + let mut bundles = Vec::new(); + for entry in entries { + let entry = entry.map_err(|err| err.to_string())?; + let manifest_path = entry.path().join("manifest.json"); + if !manifest_path.exists() { + continue; + } + let raw = std::fs::read_to_string(&manifest_path).map_err(|err| err.to_string())?; + bundles.push(serde_json::from_str::(&raw).map_err(|err| err.to_string())?); + } + bundles.sort_by(|a, b| a.name.cmp(&b.name)); + Ok(bundles) +} + +fn install_jdbc_local_bundle(plugins_root: &Path, paths: &[String]) -> Result<(), String> { + let sources = paths + .iter() + .map(PathBuf::from) + .map(|source| { + if !source.exists() { + return Err(format!("Driver JAR does not exist: {}", source.display())); + } + if source.extension().and_then(|ext| ext.to_str()).map(|ext| ext.eq_ignore_ascii_case("jar")) != Some(true) + { + return Err(format!("Only .jar files can be imported: {}", source.display())); + } + Ok(source) + }) + .collect::, String>>()?; + let name = sources + .first() + .and_then(|path| path.file_stem()) + .and_then(|name| name.to_str()) + .filter(|name| !name.is_empty()) + .unwrap_or("JDBC driver") + .to_string(); + let bundle_id = format!("local-{}", uuid::Uuid::new_v4().simple()); + let bundle_dir = jdbc_local_drivers_dir(plugins_root).join(&bundle_id); + let temp_dir = bundle_dir.with_extension("tmp"); + std::fs::create_dir_all(temp_dir.join("jars")).map_err(|err| err.to_string())?; + + let mut artifacts = Vec::with_capacity(sources.len()); + for source in sources { + let file_name = source + .file_name() + .and_then(|name| name.to_str()) + .ok_or_else(|| format!("Invalid driver file path: {}", source.display()))?; + let target = unique_target_path(&temp_dir.join("jars"), file_name); + std::fs::copy(&source, &target) + .map_err(|err| format!("Failed to import {} to {}: {err}", source.display(), target.display()))?; + let metadata = std::fs::metadata(&target).map_err(|err| err.to_string())?; + let final_path = bundle_dir.join("jars").join(target.file_name().unwrap_or_default()); + artifacts.push(JdbcLocalArtifactInfo { + file_name: target.file_name().and_then(|value| value.to_str()).unwrap_or(file_name).to_string(), + path: final_path.to_string_lossy().to_string(), + size: metadata.len(), + sha256: file_sha256(&target)?, + }); + } + let manifest = JdbcLocalBundleInfo { + id: bundle_id, + name, + installed_at: chrono::Utc::now().to_rfc3339(), + path: bundle_dir.to_string_lossy().to_string(), + artifacts, + }; + std::fs::write( + temp_dir.join("manifest.json"), + serde_json::to_string_pretty(&manifest).map_err(|err| err.to_string())?, + ) + .map_err(|err| err.to_string())?; + std::fs::rename(&temp_dir, &bundle_dir).map_err(|err| err.to_string()) +} + fn list_jdbc_maven_bundles_from_dir(maven_dir: &Path) -> Result, String> { let entries = match std::fs::read_dir(maven_dir) { Ok(entries) => entries, @@ -818,6 +943,48 @@ mod tests { assert!(!is_safe_bundle_id("../hive")); } + #[test] + fn multiple_local_jars_are_imported_as_one_bundle() { + let root = std::env::temp_dir().join(format!("dbx-jdbc-local-bundle-test-{}", uuid::Uuid::new_v4())); + let source_dir = root.join("source"); + std::fs::create_dir_all(&source_dir).unwrap(); + let driver = source_dir.join("vendor-driver.jar"); + let dependency = source_dir.join("vendor-codec.jar"); + std::fs::write(&driver, b"driver").unwrap(); + std::fs::write(&dependency, b"dependency").unwrap(); + + let paths = vec![driver.to_string_lossy().to_string(), dependency.to_string_lossy().to_string()]; + let drivers = import_jdbc_drivers(&root, &paths).unwrap(); + let bundles = list_jdbc_local_bundles(&root).unwrap(); + + assert_eq!(bundles.len(), 1); + assert_eq!(bundles[0].name, "vendor-driver"); + assert_eq!(bundles[0].artifacts.len(), 2); + assert_eq!(drivers.len(), 2); + assert!(drivers.iter().all(|item| item.bundle_id.as_deref() == Some(bundles[0].id.as_str()))); + + let remaining = delete_jdbc_local_bundle(&root, &bundles[0].id).unwrap(); + assert!(remaining.is_empty()); + assert!(list_jdbc_local_bundles(&root).unwrap().is_empty()); + let _ = std::fs::remove_dir_all(root); + } + + #[test] + fn single_local_jar_keeps_legacy_standalone_behavior() { + let root = std::env::temp_dir().join(format!("dbx-jdbc-single-jar-test-{}", uuid::Uuid::new_v4())); + let source = root.join("source").join("standalone.jar"); + std::fs::create_dir_all(source.parent().unwrap()).unwrap(); + std::fs::write(&source, b"driver").unwrap(); + + let drivers = import_jdbc_drivers(&root, &[source.to_string_lossy().to_string()]).unwrap(); + + assert_eq!(drivers.len(), 1); + assert_eq!(drivers[0].name, "standalone.jar"); + assert_eq!(drivers[0].bundle_id, None); + assert!(list_jdbc_local_bundles(&root).unwrap().is_empty()); + let _ = std::fs::remove_dir_all(root); + } + #[test] fn installs_prestosql_direct_driver_as_builtin_bundle() { let root = std::env::temp_dir().join(format!("dbx-prestosql-direct-driver-test-{}", uuid::Uuid::new_v4())); diff --git a/crates/dbx-web/src/main.rs b/crates/dbx-web/src/main.rs index 5cb84a51f6..c1b134b6e0 100644 --- a/crates/dbx-web/src/main.rs +++ b/crates/dbx-web/src/main.rs @@ -237,8 +237,10 @@ async fn main() { "/jdbc/drivers/maven", get(routes::jdbc::list_jdbc_maven_bundles).post(routes::jdbc::install_jdbc_driver_from_maven), ) + .route("/jdbc/drivers/local", get(routes::jdbc::list_jdbc_local_bundles)) .route("/jdbc/drivers/prestosql", post(routes::jdbc::install_prestosql_jdbc_driver)) .route("/jdbc/drivers/maven/{bundle_id}", delete(routes::jdbc::delete_jdbc_maven_bundle)) + .route("/jdbc/drivers/local/{bundle_id}", delete(routes::jdbc::delete_jdbc_local_bundle)) .route("/jdbc/drivers/{name}", delete(routes::jdbc::delete_jdbc_driver)) .route("/jdbc/plugin/status", get(routes::jdbc::get_jdbc_plugin_status)) .route("/jdbc/plugin/install", post(routes::jdbc::install_jdbc_plugin)) diff --git a/crates/dbx-web/src/routes/jdbc.rs b/crates/dbx-web/src/routes/jdbc.rs index 6d629384ab..183daa185b 100644 --- a/crates/dbx-web/src/routes/jdbc.rs +++ b/crates/dbx-web/src/routes/jdbc.rs @@ -3,7 +3,9 @@ use std::sync::Arc; use axum::extract::{Multipart, Path, State}; use axum::Json; use dbx_core::agent_service::AgentProgressEvent; -use dbx_core::jdbc::{self, JdbcDriverInfo, JdbcMavenBundleInfo, JdbcMavenInstallRequest, JdbcPluginStatus}; +use dbx_core::jdbc::{ + self, JdbcDriverInfo, JdbcLocalBundleInfo, JdbcMavenBundleInfo, JdbcMavenInstallRequest, JdbcPluginStatus, +}; use dbx_core::plugins::PluginRuntimeEnv; use tokio::sync::broadcast; @@ -24,6 +26,13 @@ pub async fn list_jdbc_maven_bundles( Ok(Json(jdbc::list_jdbc_maven_bundles(root).map_err(AppError::internal)?)) } +pub async fn list_jdbc_local_bundles( + State(state): State>, +) -> Result>, AppError> { + let root = state.app.plugins.root_dir(); + Ok(Json(jdbc::list_jdbc_local_bundles(root).map_err(AppError::internal)?)) +} + pub async fn install_jdbc_driver_from_maven( State(state): State>, Json(req): Json, @@ -48,8 +57,8 @@ pub async fn import_jdbc_drivers( mut multipart: Multipart, ) -> Result>, AppError> { let root = state.app.plugins.root_dir(); - let drivers_dir = root.join("jdbc").join("drivers"); - std::fs::create_dir_all(&drivers_dir).map_err(|e| AppError::internal(e.to_string()))?; + let upload_dir = root.join("jdbc").join("upload_tmp").join(uuid::Uuid::new_v4().simple().to_string()); + std::fs::create_dir_all(&upload_dir).map_err(|e| AppError::internal(e.to_string()))?; let mut imported = Vec::new(); while let Ok(Some(field)) = multipart.next_field().await { @@ -58,12 +67,18 @@ pub async fn import_jdbc_drivers( return Err(AppError::bad_request("Only .jar files can be imported")); } let data = field.bytes().await.map_err(|e| AppError::internal(e.to_string()))?; - let target = jdbc::unique_target_path(&drivers_dir, &file_name); + let target = jdbc::unique_target_path(&upload_dir, &file_name); std::fs::write(&target, &data).map_err(|e| AppError::internal(e.to_string()))?; imported.push(target); } - - jdbc::list_jdbc_drivers(root).map(Json).map_err(AppError::internal) + if imported.is_empty() { + let _ = std::fs::remove_dir_all(&upload_dir); + return Err(AppError::bad_request("No file uploaded")); + } + let paths = imported.iter().map(|path| path.to_string_lossy().to_string()).collect::>(); + let result = jdbc::import_jdbc_drivers(root, &paths).map(Json).map_err(AppError::internal); + let _ = std::fs::remove_dir_all(&upload_dir); + result } pub async fn delete_jdbc_driver( @@ -87,6 +102,14 @@ pub async fn delete_jdbc_maven_bundle( Ok(Json(jdbc::delete_jdbc_maven_bundle(root, &bundle_id).map_err(AppError::internal)?)) } +pub async fn delete_jdbc_local_bundle( + State(state): State>, + Path(bundle_id): Path, +) -> Result>, AppError> { + let root = state.app.plugins.root_dir(); + Ok(Json(jdbc::delete_jdbc_local_bundle(root, &bundle_id).map_err(AppError::internal)?)) +} + // ---- JDBC Plugin ---- pub async fn get_jdbc_plugin_status(State(state): State>) -> Result, AppError> { diff --git a/plugins/jdbc/src/main/java/app/dbx/jdbc/DbxJdbcPlugin.java b/plugins/jdbc/src/main/java/app/dbx/jdbc/DbxJdbcPlugin.java index 53316816b8..7aa12c1783 100644 --- a/plugins/jdbc/src/main/java/app/dbx/jdbc/DbxJdbcPlugin.java +++ b/plugins/jdbc/src/main/java/app/dbx/jdbc/DbxJdbcPlugin.java @@ -216,14 +216,23 @@ public final class DbxJdbcPlugin { } registerDrivers(connection); response.set("result", handle(method, params, connection)); - } catch (Exception error) { + } catch (Throwable error) { + // The plugin protocol boundary must report linkage errors from vendor drivers instead of exiting silently. ObjectNode errorNode = MAPPER.createObjectNode(); - errorNode.put("message", error.getMessage() == null ? error.toString() : error.getMessage()); + errorNode.put("message", throwableMessage(error)); response.set("error", errorNode); } return response; } + private static String throwableMessage(Throwable error) { + Throwable cause = error; + while (cause.getCause() != null && cause.getCause() != cause) { + cause = cause.getCause(); + } + return cause.getMessage() == null ? cause.toString() : cause.getMessage(); + } + private static JsonNode handle(String method, JsonNode params, JsonNode connection) throws Exception { return switch (method) { case "testConnection", "connect" -> { diff --git a/plugins/jdbc/src/main/java/app/dbx/jdbc/maven/DbxMavenResolver.java b/plugins/jdbc/src/main/java/app/dbx/jdbc/maven/DbxMavenResolver.java index 7f041dd5f3..c6e59c3259 100644 --- a/plugins/jdbc/src/main/java/app/dbx/jdbc/maven/DbxMavenResolver.java +++ b/plugins/jdbc/src/main/java/app/dbx/jdbc/maven/DbxMavenResolver.java @@ -39,15 +39,24 @@ public final class DbxMavenResolver { ResolveResult result = resolve(options); JSON.writeValue(System.out, result); System.out.println(); - } catch (Exception err) { + } catch (Throwable err) { + // Resolver dependencies may fail with linkage errors when a runtime image is missing a required module. Map error = new LinkedHashMap<>(); - error.put("error", err.getMessage() == null ? err.getClass().getSimpleName() : err.getMessage()); + error.put("error", throwableMessage(err)); JSON.writeValue(System.err, error); System.err.println(); System.exit(1); } } + private static String throwableMessage(Throwable error) { + Throwable cause = error; + while (cause.getCause() != null && cause.getCause() != cause) { + cause = cause.getCause(); + } + return cause.getMessage() == null ? cause.toString() : cause.getMessage(); + } + private static ResolveResult resolve(ResolverOptions options) throws Exception { RepositorySystem system = new RepositorySystemSupplier().get(); RepositorySystemSession session = new SessionBuilderSupplier(system) diff --git a/plugins/jdbc/src/test/java/app/dbx/jdbc/DbxJdbcPluginTest.java b/plugins/jdbc/src/test/java/app/dbx/jdbc/DbxJdbcPluginTest.java index f08284d39e..f9d883996a 100644 --- a/plugins/jdbc/src/test/java/app/dbx/jdbc/DbxJdbcPluginTest.java +++ b/plugins/jdbc/src/test/java/app/dbx/jdbc/DbxJdbcPluginTest.java @@ -67,6 +67,20 @@ final class DbxJdbcPluginTest { assertEquals("APP", response.path("result").path("rows").path(0).path(0).asText()); } + @Test + void reportsDriverLinkageErrorsWithoutTerminatingThePlugin() throws Exception { + JsonNode response = request("testConnection", """ + { + "connection": { + "connection_string": "jdbc:broken:test", + "jdbc_driver_class": "app.dbx.jdbc.DbxJdbcPluginTest$ErrorOnLoad" + } + } + """); + + assertEquals("linkage boom", response.path("error").path("message").asText()); + } + @Test void executeQueryTrimsSingleTrailingSemicolon() throws Exception { JsonNode response = request("executeQuery", """ @@ -1766,6 +1780,14 @@ final class DbxJdbcPluginTest { return null; } + public static final class ErrorOnLoad { + private static final Object FAILURE = fail(); + + private static Object fail() { + throw new AssertionError("linkage boom"); + } + } + private static JsonNode request(String method, String params) throws Exception { Method handleLine = DbxJdbcPlugin.class.getDeclaredMethod("handleLine", String.class); handleLine.setAccessible(true); diff --git a/src-tauri/src/commands/plugins.rs b/src-tauri/src/commands/plugins.rs index 8f67b27497..b9fb457b35 100644 --- a/src-tauri/src/commands/plugins.rs +++ b/src-tauri/src/commands/plugins.rs @@ -2,7 +2,9 @@ use std::sync::Arc; use tauri::{Emitter, State}; use dbx_core::agent_service::AgentProgressEvent; -use dbx_core::jdbc::{self, JdbcDriverInfo, JdbcMavenBundleInfo, JdbcMavenInstallRequest, JdbcPluginStatus}; +use dbx_core::jdbc::{ + self, JdbcDriverInfo, JdbcLocalBundleInfo, JdbcMavenBundleInfo, JdbcMavenInstallRequest, JdbcPluginStatus, +}; use dbx_core::plugins::InstalledPlugin; use super::connection::AppState; @@ -67,6 +69,14 @@ pub async fn list_jdbc_maven_bundles(state: State<'_, Arc>) -> Result< .map_err(|err| err.to_string())? } +#[tauri::command] +pub async fn list_jdbc_local_bundles(state: State<'_, Arc>) -> Result, String> { + let root_dir = state.plugins.root_dir().to_path_buf(); + tauri::async_runtime::spawn_blocking(move || jdbc::list_jdbc_local_bundles(&root_dir)) + .await + .map_err(|err| err.to_string())? +} + #[tauri::command] pub async fn install_jdbc_driver_from_maven( state: State<'_, Arc>, @@ -111,6 +121,17 @@ pub async fn delete_jdbc_maven_bundle( .map_err(|err| err.to_string())? } +#[tauri::command] +pub async fn delete_jdbc_local_bundle( + state: State<'_, Arc>, + bundle_id: String, +) -> Result, String> { + let root_dir = state.plugins.root_dir().to_path_buf(); + tauri::async_runtime::spawn_blocking(move || jdbc::delete_jdbc_local_bundle(&root_dir, &bundle_id)) + .await + .map_err(|err| err.to_string())? +} + fn emit_agent_progress(app: &tauri::AppHandle, event: AgentProgressEvent) { let _ = app.emit("agent-install-progress", event); } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c04f41ecac..28ffb5b5c6 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -889,11 +889,13 @@ pub fn run() { commands::plugins::list_plugins, commands::plugins::list_jdbc_drivers, commands::plugins::list_jdbc_maven_bundles, + commands::plugins::list_jdbc_local_bundles, commands::plugins::import_jdbc_drivers, commands::plugins::install_jdbc_driver_from_maven, commands::plugins::install_prestosql_jdbc_driver, commands::plugins::delete_jdbc_driver, commands::plugins::delete_jdbc_maven_bundle, + commands::plugins::delete_jdbc_local_bundle, commands::plugins::jdbc_plugin_status, commands::plugins::install_jdbc_plugin, commands::plugins::install_jdbc_plugin_local,