fix(editor): use editor snapshot for SQL execution

This commit is contained in:
t8y2 2026-06-17 15:30:31 +08:00
parent 4dbd1c7cc8
commit 6e7035823a
5 changed files with 43 additions and 18 deletions

View File

@ -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)"

View File

@ -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;

View File

@ -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"

View File

@ -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<QueryTab | undefined>;
activeConnection: ComputedRef<ConnectionConfig | undefined>;
executableSql: ComputedRef<string>;
resolveExecutableSql?: () => Promise<string>;
resolveExecutableSql?: (snapshot?: SqlExecutionSnapshot) => Promise<string>;
activeOutputView: Ref<"result" | "summary" | "explain" | "chart">;
blockDangerousRedisCommands?: Ref<boolean>;
}) {
@ -54,13 +55,16 @@ export function useSqlExecution(deps: {
const suppressDangerConfirm = ref(false);
const explainMode = ref<"explain" | "autotrace">("explain");
async function resolvedExecutableSql(): Promise<string> {
return deps.resolveExecutableSql ? await deps.resolveExecutableSql() : deps.executableSql.value;
async function resolvedExecutableSql(source?: SqlExecutionOverride): Promise<string> {
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;

View File

@ -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;