From 6e7035823abd615149d951799ef41d44dc08a782 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Wed, 17 Jun 2026 15:30:31 +0800 Subject: [PATCH] fix(editor): use editor snapshot for SQL execution --- apps/desktop/src/App.vue | 12 ++++++------ .../src/components/editor/QueryEditor.vue | 14 +++++++++++--- .../src/components/layout/ContentArea.vue | 5 +++-- .../desktop/src/composables/useSqlExecution.ts | 18 +++++++++++------- apps/desktop/src/lib/sqlExecutionTarget.ts | 12 ++++++++++++ 5 files changed, 43 insertions(+), 18 deletions(-) diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 7ccc48669..5e42b3897 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -35,7 +35,7 @@ import { quickConnectionOpenTarget } from "@/lib/connectionOpenTarget"; import { resolveDefaultDatabase } from "@/lib/defaultDatabase"; import { findTreeNodeById, resolveNewQueryTarget } from "@/lib/newQueryContext"; import { buildExecutableObjectSourceStatements, objectSourceSaveExecutionMode } from "@/lib/objectSourceEditor"; -import { resolveExecutableSql, resolveExecutableSqlWithBackend } from "@/lib/sqlExecutionTarget"; +import { resolveExecutableSql, resolveExecutableSqlWithBackend, type SqlExecutionSnapshot } from "@/lib/sqlExecutionTarget"; import { uuid } from "@/lib/utils"; import { isTauriRuntime } from "@/lib/tauriRuntime"; import { openQueryResultArchiveFile } from "@/lib/queryResultArchiveFile"; @@ -179,12 +179,12 @@ const executableSql = computed(() => { : ""; }); -async function resolveActiveExecutableSql() { +async function resolveActiveExecutableSql(snapshot?: SqlExecutionSnapshot) { const tab = activeTab.value; return tab - ? await resolveExecutableSqlWithBackend(tab.sql, selectedSql.value, { + ? await resolveExecutableSqlWithBackend(snapshot?.fullSql ?? tab.sql, snapshot?.selectedSql ?? selectedSql.value, { mode: settingsStore.editorSettings.executeMode, - cursorPos: cursorPos.value, + cursorPos: snapshot?.cursorPos ?? cursorPos.value, databaseType: activeConnection.value?.db_type, }) : ""; @@ -1230,7 +1230,7 @@ onUnmounted(() => { :block-dangerous-redis-commands="blockDangerousRedisCommands" @update:explain-mode="(m: 'explain' | 'autotrace') => (explainMode = m)" @update:block-dangerous-redis-commands="(v: boolean) => (blockDangerousRedisCommands = v)" - @execute="tryExecute()" + @execute="tryExecute($event)" @cancel="cancelActiveExecution()" @explain="tryExplain()" @format-sql="formatActiveSql" @@ -1256,7 +1256,7 @@ onUnmounted(() => { :cursor-pos="cursorPos" @update:active-output-view="activeOutputView = $event" @fix-with-ai="fixWithAi" - @execute="tryExecute()" + @execute="tryExecute($event)" @cancel="cancelActiveExecution()" @explain="tryExplain()" @editor-update="(tabId: string, v: string) => queryStore.updateSql(tabId, v)" diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index a65f26df7..e3ed3712d 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -8,7 +8,7 @@ import { search as cmSearch } from "@codemirror/search"; import EditorSearchPanel from "./EditorSearchPanel.vue"; import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomContextMenu.vue"; import { copyToClipboard } from "@/lib/clipboard"; -import { resolveExecutableSql } from "@/lib/sqlExecutionTarget"; +import { resolveExecutableSql, type SqlExecutionSnapshot } from "@/lib/sqlExecutionTarget"; import { formatSqlText, type SqlFormatDialect } from "@/lib/sqlFormatter"; import { useConnectionStore } from "@/stores/connectionStore"; import { useSettingsStore } from "@/stores/settingsStore"; @@ -64,7 +64,7 @@ const emit = defineEmits<{ selectionChange: [value: string]; cursorChange: [pos: number]; formatError: [message: string]; - execute: [sql: string]; + execute: [snapshot: SqlExecutionSnapshot]; save: []; clickTable: [tableName: string]; viewTableData: [tableName: string]; @@ -288,7 +288,7 @@ function handleTab(view: EditorViewType): boolean { } function executeCurrentSql() { - if (view.value) emit("execute", executableSqlFromView(view.value)); + if (view.value) emit("execute", sqlExecutionSnapshotFromView(view.value)); return true; } @@ -461,6 +461,14 @@ function executableSqlFromView(currentView: EditorViewType): string { return resolveExecutableSql(currentView.state.doc.toString(), selectedSqlFromView(currentView)); } +function sqlExecutionSnapshotFromView(currentView: EditorViewType): SqlExecutionSnapshot { + return { + fullSql: currentView.state.doc.toString(), + selectedSql: selectedSqlFromView(currentView), + cursorPos: currentView.state.selection.main.head, + }; +} + function identifierRangeAt(sql: string, pos: number): { from: number; to: number; text: string } | null { const isIdentifierChar = (ch: string | undefined) => !!ch && /[\w$.]/.test(ch); if (!isIdentifierChar(sql[pos]) && !isIdentifierChar(sql[pos - 1])) return null; diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index 0f387c11a..e1489bfe1 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -54,6 +54,7 @@ import { tableMetaForDataTab } from "@/lib/tableDataTabMeta"; import { formatShortcut } from "@/lib/shortcutRegistry"; import { effectiveDatabaseTypeForConnection } from "@/lib/jdbcDialect"; import { chartableColumnIndexes } from "@/lib/chartData"; +import type { SqlExecutionOverride } from "@/lib/sqlExecutionTarget"; import { useTabScroll } from "@/composables/useTabScroll"; import type { QueryTab, ConnectionConfig, TableInfoTab } from "@/types/database"; import type { SqlFormatDialect } from "@/lib/sqlFormatter"; @@ -101,7 +102,7 @@ const props = defineProps<{ const emit = defineEmits<{ "update:activeOutputView": [value: "result" | "summary" | "explain" | "chart"]; fixWithAi: [errorMessage: string]; - execute: [sqlOverride?: string]; + execute: [sqlOverride?: SqlExecutionOverride]; saveSql: []; cancel: []; explain: []; @@ -522,7 +523,7 @@ defineExpose({ focusSearch, refreshData, handleModRTarget }); @viewport-change="emit('editorViewportChange', activeTab.id, $event)" @selection-state-change="emit('editorSelectionStateChange', activeTab.id, $event)" @format-error="emit('formatError')" - @execute="emit('execute')" + @execute="emit('execute', $event)" @save="emit('saveSql')" @click-table="onHandleClickTable" @view-table-data="onHandleViewTableData" diff --git a/apps/desktop/src/composables/useSqlExecution.ts b/apps/desktop/src/composables/useSqlExecution.ts index 21ec61333..9323876ae 100644 --- a/apps/desktop/src/composables/useSqlExecution.ts +++ b/apps/desktop/src/composables/useSqlExecution.ts @@ -8,6 +8,7 @@ import { useToast } from "@/composables/useToast"; import { classifySqlActivityKind } from "@/lib/historyActivityKind"; import { sqlMetadataRefreshTarget } from "@/lib/sqlMetadataRefresh"; import { classifyRedisCommandSafety, firstRedisCommandToken } from "@/lib/redisCommandSafety"; +import { isSqlExecutionSnapshot, resolveExecutableSql, type SqlExecutionOverride, type SqlExecutionSnapshot } from "@/lib/sqlExecutionTarget"; import type { ConnectionConfig, QueryTab } from "@/types/database"; const DANGER_RE = /^\s*(DROP|DELETE|TRUNCATE|ALTER|UPDATE|MERGE|REPLACE)\b/i; @@ -37,7 +38,7 @@ export function useSqlExecution(deps: { activeTab: ComputedRef; activeConnection: ComputedRef; executableSql: ComputedRef; - resolveExecutableSql?: () => Promise; + resolveExecutableSql?: (snapshot?: SqlExecutionSnapshot) => Promise; activeOutputView: Ref<"result" | "summary" | "explain" | "chart">; blockDangerousRedisCommands?: Ref; }) { @@ -54,13 +55,16 @@ export function useSqlExecution(deps: { const suppressDangerConfirm = ref(false); const explainMode = ref<"explain" | "autotrace">("explain"); - async function resolvedExecutableSql(): Promise { - return deps.resolveExecutableSql ? await deps.resolveExecutableSql() : deps.executableSql.value; + async function resolvedExecutableSql(source?: SqlExecutionOverride): Promise { + if (typeof source === "string") return source; + if (deps.resolveExecutableSql) return await deps.resolveExecutableSql(source); + if (isSqlExecutionSnapshot(source)) return resolveExecutableSql(source.fullSql, source.selectedSql, { cursorPos: source.cursorPos }); + return deps.executableSql.value; } - async function tryExecute(sqlOverride?: string) { + async function tryExecute(sqlOverride?: SqlExecutionOverride) { const tab = deps.activeTab.value; - const sql = sqlOverride ?? (await resolvedExecutableSql()); + const sql = await resolvedExecutableSql(sqlOverride); if (!tab || !sql.trim()) return; // Redis: block dangerous commands when toggle is on (check each line for multi-line input) if (deps.activeConnection.value?.db_type === "redis" && deps.blockDangerousRedisCommands?.value !== false) { @@ -135,9 +139,9 @@ export function useSqlExecution(deps: { return t("explain.emptySql"); } - async function tryExplain(sqlOverride?: string) { + async function tryExplain(sqlOverride?: SqlExecutionOverride) { const tab = deps.activeTab.value; - const sql = sqlOverride ?? (await resolvedExecutableSql()); + const sql = await resolvedExecutableSql(sqlOverride); if (!tab || !sql.trim()) { toast(t("explain.emptySql")); return; diff --git a/apps/desktop/src/lib/sqlExecutionTarget.ts b/apps/desktop/src/lib/sqlExecutionTarget.ts index 7a13e678e..6377d72c8 100644 --- a/apps/desktop/src/lib/sqlExecutionTarget.ts +++ b/apps/desktop/src/lib/sqlExecutionTarget.ts @@ -3,6 +3,18 @@ import type { DatabaseType } from "@/types/database"; export type ExecuteMode = "all" | "current"; +export interface SqlExecutionSnapshot { + fullSql: string; + selectedSql: string; + cursorPos: number; +} + +export type SqlExecutionOverride = string | SqlExecutionSnapshot; + +export function isSqlExecutionSnapshot(value: SqlExecutionOverride | undefined): value is SqlExecutionSnapshot { + return typeof value === "object" && value !== null && typeof value.fullSql === "string" && typeof value.selectedSql === "string" && typeof value.cursorPos === "number"; +} + export function resolveExecutableSql(fullSql: string, selectedSql: string, options?: { mode?: ExecuteMode; cursorPos?: number }): string { const trimmedSelection = selectedSql.trim(); if (trimmedSelection) return trimmedSelection;