From 29d1092b5a513c84b04dbb069023f9fa813cc12a Mon Sep 17 00:00:00 2001 From: Andrea Debernardi Date: Mon, 22 Jun 2026 09:24:04 +0200 Subject: [PATCH] fix: scope multi-database operations to the selected database On a single MySQL/MariaDB connection that exposes several databases, the ER diagram, the database dump and the CSV/JSON export all ran against the connection's primary database instead of the one the user had selected. The backend resolves the target database from the request, but the frontend never forwarded the selected database, so it kept falling back to the first one. ER diagram: - Forward the selected database to get_schema_snapshot (the MySQL driver treats the schema argument as the database name), so each database renders its own tables. - Give every diagram window a unique label per connection/database/schema and focus an existing one instead of silently reusing the first window. Dump: - Pass the selected database to dump_database so the pool connects to it and unqualified statements (SHOW CREATE TABLE, SELECT * FROM ...) hit the right database. - Reload the target database's tables when the dialog opens so the table list reflects that database rather than a stale or wrong one. Export: - Pass the selected database to export_query_to_file, resolving it from the tab schema and falling back to the active database (matching how execute_query already resolves it). --- src-tauri/src/commands.rs | 31 +++++- src-tauri/src/dump_commands.rs | 10 +- src-tauri/src/export.rs | 9 +- src/components/modals/DumpDatabaseModal.tsx | 115 ++++++++++++++------ src/pages/Editor.tsx | 12 ++ src/pages/SchemaDiagramPage.tsx | 8 +- src/utils/schemaDiagram.ts | 24 ++++ tests/utils/schemaDiagram.test.ts | 27 +++++ 8 files changed, 198 insertions(+), 38 deletions(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index cc353745..7651b4dc 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -3394,7 +3394,36 @@ pub async fn open_er_diagram_window( url.push_str(&format!("&schema={}", encode(s))); } - let _webview = WebviewWindowBuilder::new(&app, "er-diagram", WebviewUrl::App(url.into())) + // Derive a unique window label per (connection, database, schema) so that + // diagrams for different databases on the same connection do not collide on a + // shared label (which previously kept showing the first database's diagram). + // Tauri window labels only allow a limited character set, so sanitize anything + // else to '_'. + let raw_label = format!( + "er-diagram:{}:{}:{}", + connection_id, + database_name, + schema.as_deref().unwrap_or("") + ); + let label: String = raw_label + .chars() + .map(|c| { + if c.is_ascii_alphanumeric() || c == '-' || c == '_' { + c + } else { + '_' + } + }) + .collect(); + + // If a diagram window for this exact database already exists, just focus it + // instead of failing to build a second window with the same label. + if let Some(existing) = app.get_webview_window(&label) { + let _ = existing.set_focus(); + return Ok(()); + } + + let _webview = WebviewWindowBuilder::new(&app, &label, WebviewUrl::App(url.into())) .title(&title) .inner_size(1200.0, 800.0) .center() diff --git a/src-tauri/src/dump_commands.rs b/src-tauri/src/dump_commands.rs index ee731903..12b1f164 100644 --- a/src-tauri/src/dump_commands.rs +++ b/src-tauri/src/dump_commands.rs @@ -61,11 +61,19 @@ pub async fn dump_database( file_path: String, options: DumpOptions, schema: Option, + database: Option, ) -> Result<(), String> { let saved_conn = find_connection_by_id(&app, &connection_id)?; let expanded_params = expand_ssh_connection_params(&app, &saved_conn.params).await?; let expanded_params = expand_k8s_connection_params(&app, &expanded_params).await?; - let params = resolve_connection_params_with_id(&expanded_params, &connection_id)?; + let mut params = resolve_connection_params_with_id(&expanded_params, &connection_id)?; + // Scope the dump to the selected database on connections that expose multiple + // databases (e.g. MySQL/MariaDB). Without this the connection pool stays bound + // to the primary database, so unqualified statements such as `SHOW CREATE TABLE` + // and `SELECT * FROM table` run against the wrong database. + if let Some(db) = database { + params.database = crate::models::DatabaseSelection::Single(db); + } let driver = saved_conn.params.driver.clone(); let schema = schema.unwrap_or_else(|| "public".to_string()); diff --git a/src-tauri/src/export.rs b/src-tauri/src/export.rs index cc17950a..e76ebf2d 100644 --- a/src-tauri/src/export.rs +++ b/src-tauri/src/export.rs @@ -73,12 +73,19 @@ pub async fn export_query_to_file( file_path: String, format: String, csv_delimiter: Option, + database: Option, ) -> Result<(), String> { let sanitized_query = sanitize_query(&query); let saved_conn = find_connection_by_id(&app, &connection_id)?; let expanded_params = expand_ssh_connection_params(&app, &saved_conn.params).await?; let expanded_params = expand_k8s_connection_params(&app, &expanded_params).await?; - let params = resolve_connection_params_with_id(&expanded_params, &connection_id)?; + let mut params = resolve_connection_params_with_id(&expanded_params, &connection_id)?; + // Scope the export to the selected database on connections that expose multiple + // databases (e.g. MySQL/MariaDB), so the query runs against the database the + // user is viewing rather than the connection's primary database. + if let Some(db) = database { + params.database = crate::models::DatabaseSelection::Single(db); + } let driver = saved_conn.params.driver.clone(); let export_format = ExportFormat::parse(&format)?; diff --git a/src/components/modals/DumpDatabaseModal.tsx b/src/components/modals/DumpDatabaseModal.tsx index 41187753..2cf1ec82 100644 --- a/src/components/modals/DumpDatabaseModal.tsx +++ b/src/components/modals/DumpDatabaseModal.tsx @@ -1,9 +1,10 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useMemo, useRef } from "react"; import { useTranslation } from "react-i18next"; import { invoke } from "@tauri-apps/api/core"; import { save } from "@tauri-apps/plugin-dialog"; import { useAlert } from "../../hooks/useAlert"; import { useDatabase } from "../../hooks/useDatabase"; +import { isMultiDatabaseCapable } from "../../utils/database"; import { Modal } from "../ui/Modal"; import { Loader2, Download, Database, Square, CheckSquare } from "lucide-react"; import { @@ -29,7 +30,8 @@ export const DumpDatabaseModal = ({ tables, }: DumpDatabaseModalProps) => { const { t } = useTranslation(); - const { activeSchema } = useDatabase(); + const { activeSchema, activeCapabilities, databaseDataMap, refreshDatabaseData } = + useDatabase(); const { showAlert } = useAlert(); const [includeStructure, setIncludeStructure] = useState(true); const [includeData, setIncludeData] = useState(true); @@ -40,13 +42,45 @@ export const DumpDatabaseModal = ({ const [elapsedTime, setElapsedTime] = useState(0); // in seconds const [startTime, setStartTime] = useState(null); + const isMultiDb = isMultiDatabaseCapable(activeCapabilities); + + // On a multi-database connection (e.g. MySQL) the dump targets a specific + // database whose cached table list may be missing or belong to a different + // database. Reload it whenever the dialog opens so the dump reflects the + // target database's current schema. A ref keeps refreshDatabaseData out of the + // dependency list (its identity changes on every store update, which would + // otherwise loop). + const refreshRef = useRef(refreshDatabaseData); + refreshRef.current = refreshDatabaseData; + useEffect(() => { + if (isOpen && isMultiDb && databaseName) { + refreshRef.current(databaseName); + } + }, [isOpen, isMultiDb, databaseName]); + + // For multi-database connections read the table list straight from the target + // database's freshly-loaded data (never the active-database fallback); other + // drivers keep using the list resolved by the parent. + const targetDbData = isMultiDb ? databaseDataMap[databaseName] : undefined; + const tablesLoading = isMultiDb ? (targetDbData?.isLoading ?? false) : false; + const effectiveTables = useMemo( + () => (isMultiDb ? (targetDbData?.tables ?? []).map((tbl) => tbl.name) : tables), + [isMultiDb, targetDbData, tables], + ); + + // Detect content changes without reacting to array-reference churn; the actual + // list is read from a ref so table names containing the separator stay intact. + const tablesKey = effectiveTables.join("\n"); + const effectiveTablesRef = useRef(effectiveTables); + effectiveTablesRef.current = effectiveTables; + useEffect(() => { if (isOpen) { - setSelectedTables(new Set(tables)); + setSelectedTables(new Set(effectiveTablesRef.current)); setElapsedTime(0); setStartTime(null); } - }, [isOpen, tables]); + }, [isOpen, tablesKey]); // Timer for elapsed time useEffect(() => { @@ -65,7 +99,7 @@ export const DumpDatabaseModal = ({ }; const handleSelectAll = () => { - setSelectedTables(selectAllTables(selectedTables, tables)); + setSelectedTables(selectAllTables(selectedTables, effectiveTables)); }; const handleExport = async () => { @@ -97,6 +131,11 @@ export const DumpDatabaseModal = ({ setStartTime(Date.now()); setElapsedTime(0); + // On multi-database connections (e.g. MySQL) scope the dump to the selected + // database so it does not fall back to the connection's primary database. + const databaseParam = + isMultiDb && databaseName ? { database: databaseName } : {}; + // Rust command expects `options` struct await invoke("dump_database", { connectionId, @@ -107,6 +146,7 @@ export const DumpDatabaseModal = ({ tables: Array.from(selectedTables), }, ...(activeSchema ? { schema: activeSchema } : {}), + ...databaseParam, }); showAlert(t("dump.success"), { kind: "info" }); @@ -138,14 +178,14 @@ export const DumpDatabaseModal = ({ - +
{/* Options */}