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 */}