diff --git a/src-tauri/src/commands/query.rs b/src-tauri/src/commands/query.rs index 0faae57b5..4639dc7e6 100644 --- a/src-tauri/src/commands/query.rs +++ b/src-tauri/src/commands/query.rs @@ -6,6 +6,7 @@ use tokio::time::timeout; use tokio_util::sync::CancellationToken; use crate::commands::connection::{AppState, PoolKind}; +use crate::commands::sql_file::split_sql_statements; use crate::db; const QUERY_TIMEOUT: Duration = Duration::from_secs(30); @@ -269,9 +270,8 @@ pub async fn cancel_query( Ok(state.running_queries.cancel(&execution_id)) } -#[tauri::command] -pub async fn execute_batch( - state: State<'_, Arc>, +async fn execute_statements( + state: &Arc, connection_id: String, database: String, statements: Vec, @@ -312,6 +312,26 @@ pub async fn execute_batch( }) } +#[tauri::command] +pub async fn execute_batch( + state: State<'_, Arc>, + connection_id: String, + database: String, + statements: Vec, +) -> Result { + execute_statements(&state, connection_id, database, statements).await +} + +#[tauri::command] +pub async fn execute_script( + state: State<'_, Arc>, + connection_id: String, + database: String, + sql: String, +) -> Result { + execute_statements(&state, connection_id, database, split_sql_statements(&sql)).await +} + #[cfg(test)] mod tests { use super::*; diff --git a/src-tauri/src/commands/sql_file.rs b/src-tauri/src/commands/sql_file.rs index 252c761c7..8e8460133 100644 --- a/src-tauri/src/commands/sql_file.rs +++ b/src-tauri/src/commands/sql_file.rs @@ -225,6 +225,13 @@ impl SqlStatementSplitter { } } +pub(crate) fn split_sql_statements(sql: &str) -> Vec { + let mut splitter = SqlStatementSplitter::default(); + let mut statements = splitter.push_chunk(sql); + statements.extend(splitter.finish()); + statements +} + fn starts_with_chars(chars: &[char], start: usize, needle: &[char]) -> bool { start + needle.len() <= chars.len() && chars[start..start + needle.len()] == *needle } diff --git a/src-tauri/src/commands/transfer.rs b/src-tauri/src/commands/transfer.rs index 965c69a39..58c92f035 100644 --- a/src-tauri/src/commands/transfer.rs +++ b/src-tauri/src/commands/transfer.rs @@ -707,6 +707,8 @@ pub async fn start_transfer( status: TransferStatus::Error, error: Some(e), }); + CANCELLED.write().await.remove(&transfer_id); + return; } } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8f9e6be76..71db055a3 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -61,6 +61,7 @@ pub fn run() { commands::query::execute_query, commands::query::cancel_query, commands::query::execute_batch, + commands::query::execute_script, commands::sql_file::preview_sql_file, commands::sql_file::execute_sql_file, commands::sql_file::cancel_sql_file_execution, diff --git a/src/App.vue b/src/App.vue index 5acfff5fb..412378f99 100644 --- a/src/App.vue +++ b/src/App.vue @@ -41,6 +41,7 @@ const SchemaDiagramDialog = defineAsyncComponent(() => import("@/components/diag const TableImportDialog = defineAsyncComponent(() => import("@/components/import/TableImportDialog.vue")); const TableStructureEditorDialog = defineAsyncComponent(() => import("@/components/structure/TableStructureEditorDialog.vue")); const ExplainPlanViewer = defineAsyncComponent(() => import("@/components/explain/ExplainPlanViewer.vue")); +const FieldLineageDialog = defineAsyncComponent(() => import("@/components/lineage/FieldLineageDialog.vue")); import type { ConnectionConfig } from "@/types/database"; import { useConnectionStore } from "@/stores/connectionStore"; import { useQueryStore } from "@/stores/queryStore"; @@ -55,6 +56,7 @@ import * as api from "@/lib/tauri"; import { canCancelQueryExecution, queryExecutionLabelKey } from "@/lib/queryExecutionState"; import { resolveExecutableSql } from "@/lib/sqlExecutionTarget"; import { buildTableSelectSql, quoteTableIdentifier } from "@/lib/tableSelectSql"; +import { isTauriRuntime } from "@/lib/tauriRuntime"; import type { SqlFormatDialect } from "@/lib/sqlFormatter"; import { isCloseTabShortcut, isExecuteSqlShortcut } from "@/lib/keyboardShortcuts"; @@ -128,6 +130,7 @@ const showSqlFileDialog = ref(false); const showDiagramDialog = ref(false); const showTableImportDialog = ref(false); const showStructureEditorDialog = ref(false); +const showFieldLineageDialog = ref(false); const transferPrefillConnectionId = ref(""); const transferPrefillDatabase = ref(""); const schemaDiffPrefillConnectionId = ref(""); @@ -146,6 +149,18 @@ const structurePrefillConnectionId = ref(""); const structurePrefillDatabase = ref(""); const structurePrefillSchema = ref(""); const structurePrefillTable = ref(""); +const lineagePrefillConnectionId = ref(""); +const lineagePrefillDatabase = ref(""); +const lineagePrefillSchema = ref(""); +const lineagePrefillTable = ref(""); +const lineagePrefillColumn = ref(""); +type LineageNavigationTarget = { + connectionId: string; + database: string; + schema?: string; + tableName: string; + columnName?: string; +}; const databaseOptions = ref>({}); const loadingDatabaseOptions = ref>({}); const checkingUpdates = ref(false); @@ -233,6 +248,18 @@ watch(() => connectionStore.structureEditorSource, (v) => { } }); +watch(() => connectionStore.fieldLineageSource, (v) => { + if (v) { + lineagePrefillConnectionId.value = v.connectionId; + lineagePrefillDatabase.value = v.database; + lineagePrefillSchema.value = v.schema ?? ""; + lineagePrefillTable.value = v.tableName; + lineagePrefillColumn.value = v.columnName; + showFieldLineageDialog.value = true; + connectionStore.fieldLineageSource = null; + } +}); + async function onStructureEditorSaved() { const tab = activeTab.value; if (tab?.mode === "data" && tab.tableMeta?.tableName === structurePrefillTable.value) { @@ -255,6 +282,42 @@ async function onStructureEditorSaved() { } } +async function openLineageTarget(target: LineageNavigationTarget) { + showFieldLineageDialog.value = false; + connectionStore.activeConnectionId = target.connectionId; + const config = connectionStore.getConfig(target.connectionId); + const tabTitle = target.schema ? `${target.schema}.${target.tableName}` : target.tableName; + const tabId = queryStore.createTab(target.connectionId, target.database, tabTitle, "data"); + queryStore.setExecuting(tabId, true); + + try { + await connectionStore.ensureConnected(target.connectionId); + if (!config) throw new Error("Connection config not found"); + + const querySchema = target.schema || target.database; + const columns = await api.getColumns(target.connectionId, target.database, querySchema, target.tableName); + const primaryKeys = columns.filter((column) => column.is_primary_key).map((column) => column.name); + const sql = buildTableSelectSql({ + databaseType: config.db_type, + schema: target.schema, + tableName: target.tableName, + primaryKeys, + }); + + queryStore.updateSql(tabId, sql); + queryStore.setTableMeta(tabId, { + schema: target.schema, + tableName: target.tableName, + columns, + primaryKeys, + }); + + await queryStore.executeTabSql(tabId, sql); + } catch (e: any) { + queryStore.setErrorResult(tabId, e); + } +} + function onConnectionConnectStarted(name: string) { toast(t("connection.connecting", { name }), 30000); } @@ -593,11 +656,15 @@ function buildTableSql( options: { orderBy?: string; limit?: number; offset?: number; whereInput?: string } = {}, ): string { const config = connectionStore.getConfig(tab.connectionId); + const fallbackOrderColumns = config?.db_type === "sqlserver" && !tab.tableMeta?.primaryKeys?.length + ? tab.tableMeta?.columns.slice(0, 1).map((column) => column.name) + : undefined; return buildTableSelectSql({ databaseType: config?.db_type, schema: tab.tableMeta?.schema, tableName: tab.tableMeta?.tableName ?? "", primaryKeys: tab.tableMeta?.primaryKeys, + fallbackOrderColumns, ...options, }); } @@ -628,7 +695,10 @@ const isDark = ref(localStorage.getItem("dbx-theme") === "dark"); function applyTheme() { document.documentElement.classList.toggle("dark", isDark.value); - getCurrentWindow().setTheme(isDark.value ? "dark" as Theme : "light" as Theme); + if (!isTauriRuntime()) return; + getCurrentWindow() + .setTheme(isDark.value ? "dark" as Theme : "light" as Theme) + .catch(() => {}); } function toggleTheme() { @@ -710,9 +780,11 @@ onMounted(() => { }); settingsStore.initAiConfig(); window.addEventListener("keydown", handleKeydown, true); - setupFileDrop(); - checkUpdates({ silent: true }); - getVersion().then((v) => { appVersion.value = v; }); + if (isTauriRuntime()) { + setupFileDrop().catch(() => {}); + checkUpdates({ silent: true }); + getVersion().then((v) => { appVersion.value = v; }).catch(() => {}); + } }); onUnmounted(() => { @@ -1411,6 +1483,15 @@ async function setupFileDrop() { :prefill-table="structurePrefillTable" @saved="onStructureEditorSaved" /> + diff --git a/src/components/diff/SchemaDiffDialog.vue b/src/components/diff/SchemaDiffDialog.vue index 1754cd7a1..15de58f80 100644 --- a/src/components/diff/SchemaDiffDialog.vue +++ b/src/components/diff/SchemaDiffDialog.vue @@ -7,6 +7,7 @@ import { } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; +import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; @@ -161,14 +162,7 @@ async function executeSql() { executing.value = true; try { await store.ensureConnected(targetConnectionId.value); - const statements = syncSql.value - .split(";") - .map((s) => s.trim()) - .filter((s) => s && !s.startsWith("--")); - - for (const sql of statements) { - await api.executeQuery(targetConnectionId.value, targetDatabase.value, sql); - } + await api.executeScript(targetConnectionId.value, targetDatabase.value, syncSql.value); toast(t("diff.syncSuccess"), 2000); open.value = false; } catch (e: any) { diff --git a/src/components/editor/QueryHistory.vue b/src/components/editor/QueryHistory.vue index 1c475d000..c59d81288 100644 --- a/src/components/editor/QueryHistory.vue +++ b/src/components/editor/QueryHistory.vue @@ -7,6 +7,7 @@ import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger, } from "@/components/ui/context-menu"; import { useHistoryStore } from "@/stores/historyStore"; +import { shouldClearHistory, shouldDeleteHistoryEntry } from "@/lib/historyActions"; const { t } = useI18n(); const store = useHistoryStore(); @@ -34,6 +35,18 @@ function copySql(sql: string) { navigator.clipboard.writeText(sql); } +function confirmDeleteEntry(id: string) { + if (shouldDeleteHistoryEntry(() => window.confirm(t("history.confirmDelete")))) { + store.remove(id); + } +} + +function confirmClearHistory() { + if (shouldClearHistory(store.entries.length, () => window.confirm(t("history.confirmClear")))) { + store.clear(); + } +} + function formatTime(iso: string): string { const d = new Date(iso); const pad = (n: number) => String(n).padStart(2, "0"); @@ -54,7 +67,7 @@ onMounted(() => store.load()); {{ t('history.title') }} -