diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index 2032217ea6..4a8bddab79 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -26,6 +26,12 @@ import { isTauriRuntime } from "@/lib/tauriRuntime"; import { applyParsedConnectionUrl, normalizeMongoConnectionString, parseConnectionUrl } from "@/lib/connectionUrl"; import type { ConnectionDeepLinkDraft } from "@/lib/connectionDeepLink"; import { connectionUrlPlaceholder as getUrlPlaceholder } from "@/lib/connectionPresentation"; +import { + h2ConnectionModeForConfig, + h2FileJdbcUrl, + h2FilePathFromJdbcUrl, + type H2ConnectionMode, +} from "@/lib/h2Connection"; import { mongodbAuthFailureHint, mongoUrlParam, setMongoUrlParam } from "@/lib/mongoConnectionOptions"; import { copyToClipboard } from "@/lib/clipboard"; import { showAgentDriverInstallHint, type AgentDriverInstallState } from "@/lib/agentDriverInstallHint"; @@ -264,6 +270,7 @@ const agentDrivers = ref([]); const selectedJdbcDriverPath = ref(""); const connectionUrlInput = ref(""); const oceanbaseSubMode = ref<"mysql" | "oracle">("mysql"); +const h2ConnectionMode = ref("file"); const dialogStep = ref("select"); const dbPickerView = ref("icon"); const dbSearchQuery = ref(""); @@ -467,6 +474,12 @@ function applyProfile(val: string, preserveConnectionFields = false) { if (profile.type === "sqlite" || profile.type === "duckdb" || profile.type === "access") { form.value.host = ""; } + if (profile.type === "h2") { + h2ConnectionMode.value = "file"; + form.value.host = ""; + form.value.port = 0; + form.value.connection_string = undefined; + } if (profile.type === "jdbc") { form.value.host = ""; form.value.connection_string = ""; @@ -508,7 +521,7 @@ watch( driver_profile: profile, driver_label: config.driver_label || driverProfiles[profile]?.label || config.db_type, url_params: config.url_params || "", - host: config.host, + host: config.db_type === "h2" ? config.host || h2FilePathFromJdbcUrl(config.connection_string) : config.host, port: profile === "tdengine" && (config.port === 0 || config.port === 6030) ? 6041 : config.port, username: config.username, password: config.password, @@ -535,6 +548,7 @@ watch( redis_cluster_nodes: config.redis_cluster_nodes || "", etcd_endpoints: config.etcd_endpoints || "", }; + h2ConnectionMode.value = h2ConnectionModeForConfig(config); selectedTransportLayerId.value = form.value.transport_layers?.[0]?.id || null; selectedType.value = profile; if (profile === "oceanbase") { @@ -555,6 +569,7 @@ watch( selectedType.value = "mysql"; customDriverName.value = ""; oceanbaseSubMode.value = "mysql"; + h2ConnectionMode.value = "file"; dialogStep.value = "select"; configTab.value = "connection"; } @@ -616,6 +631,7 @@ function defaultDatabaseForProfile() { return "postgres"; if (form.value.db_type === "sqlserver") return "master"; if (form.value.db_type === "oracle") return "ORCL"; + if (form.value.db_type === "h2" && h2ConnectionMode.value === "tcp") return "test"; return ""; } @@ -625,6 +641,26 @@ function onDbTypeChange(val: string) { resetTestState(); } +function switchH2ConnectionMode(mode: H2ConnectionMode) { + h2ConnectionMode.value = mode; + if (mode === "file") { + form.value.host = h2FilePathFromJdbcUrl(form.value.connection_string) || ""; + form.value.port = 0; + } else { + form.value.host = + form.value.host.trim() && !isH2FileJdbcUrlLikePath(form.value.host) ? form.value.host : "127.0.0.1"; + form.value.port = form.value.port || 9092; + if (form.value.connection_string && h2FilePathFromJdbcUrl(form.value.connection_string)) { + form.value.connection_string = undefined; + } + } + resetTestState(); +} + +function isH2FileJdbcUrlLikePath(value: string): boolean { + return /\.(mv|h2)\.db$/i.test(value.trim()) || value.includes("/") || value.includes("\\"); +} + const iconTypeMap: Record = { mysql: "mysql", postgres: "postgres", @@ -776,11 +812,20 @@ const filteredDbCategories = computed(() => { const hasDbPickerResults = computed(() => filteredDbCategories.value.some((category) => category.options.length > 0)); const selectedDbIcon = computed(() => iconTypeMap[selectedType.value] || selectedProfile().icon || selectedType.value); const isJdbcConnection = computed(() => form.value.db_type === "jdbc"); +const isH2FileMode = computed(() => form.value.db_type === "h2" && h2ConnectionMode.value === "file"); +const usesLocalFilePathInput = computed( + () => + form.value.db_type === "sqlite" || + form.value.db_type === "duckdb" || + form.value.db_type === "access" || + isH2FileMode.value, +); const connectionUrlPlaceholder = computed(() => getUrlPlaceholder(form.value.db_type)); const filePathPlaceholder = computed(() => { if (form.value.db_type === "duckdb") return "/path/to/database.duckdb or :memory:"; if (form.value.db_type === "access") return "/path/to/database.accdb"; + if (form.value.db_type === "h2") return "/path/to/database.mv.db"; return "/path/to/database.db or :memory:"; }); const supportsMemoryDatabasePath = computed(() => form.value.db_type === "sqlite" || form.value.db_type === "duckdb"); @@ -872,7 +917,9 @@ const etcdEndpointsLines = computed({ form.value.etcd_endpoints = normalizeEndpointLines(value); }, }); -const canUseTransportLayers = computed(() => form.value.db_type !== "sqlite" && form.value.db_type !== "access"); +const canUseTransportLayers = computed( + () => form.value.db_type !== "sqlite" && form.value.db_type !== "access" && !isH2FileMode.value, +); const shouldShowAgentDriverInstallHint = computed(() => showAgentDriverInstallHint(form.value.db_type, agentDrivers.value, form.value.driver_profile), ); @@ -880,6 +927,15 @@ const testResultMessage = computed(() => { if (!testResult.value) return ""; return testResult.value.ok ? t("connection.testSuccess") : testResult.value.message; }); +const hasRequiredConnectionTarget = computed(() => { + if (isH2FileMode.value) return !!(form.value.host.trim() || h2FilePathFromJdbcUrl(form.value.connection_string)); + return !!( + form.value.host || + (mongoUseUrl.value && form.value.connection_string) || + (form.value.db_type === "jdbc" && form.value.connection_string) || + connectionUrlInput.value.trim() + ); +}); const mongoAuthDatabase = computed({ get: () => mongoUrlParam(form.value.url_params, "authSource"), set: (value: string) => { @@ -1068,6 +1124,26 @@ function connectionConfigForSubmit(id: string): ConnectionConfig { .map((path) => path.trim()) .filter(Boolean); } + if (config.db_type === "h2") { + if (h2ConnectionMode.value === "file") { + const filePath = config.host?.trim() || h2FilePathFromJdbcUrl(config.connection_string); + if (!filePath) { + throw new Error(t("connection.h2FilePathRequired")); + } + config.host = filePath; + config.port = 0; + config.connection_string = h2FileJdbcUrl(filePath); + config.transport_layers = []; + } else { + config.host = config.host?.trim() || "127.0.0.1"; + config.port = Number(config.port) || 9092; + if (h2FilePathFromJdbcUrl(config.connection_string)) { + config.connection_string = undefined; + } else { + config.connection_string = config.connection_string?.trim() || undefined; + } + } + } const legacy = config as LegacyConnectionConfig; delete legacy.ssh_enabled; delete legacy.ssh_host; @@ -1704,7 +1780,9 @@ async function browseDbFilePath() { ? [{ name: "DuckDB", extensions: ["duckdb", "db"] }] : form.value.db_type === "access" ? [{ name: "Microsoft Access", extensions: ["accdb", "mdb"] }] - : [{ name: "SQLite", extensions: ["db", "sqlite", "sqlite3"] }]; + : form.value.db_type === "h2" + ? [{ name: "H2", extensions: ["db"] }] + : [{ name: "SQLite", extensions: ["db", "sqlite", "sqlite3"] }]; const selected = await open({ title: "Select Database File", multiple: false, @@ -2074,6 +2152,26 @@ function openExternalUrl(url: string) { +
+ +
+ + +
+
+ -