fix(editor): keep scroll after canceling dangerous sql
This commit is contained in:
1 parent
a751b5690b
commit
694611aade
9 files changed
+215
-11
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { computed, ref } from "vue";
|
||||
import { computed, nextTick, ref } from "vue";
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { isDangerousSql, requiresDatabaseSelection, supportsSqlTemplateParameters, useSqlExecution } from "../useSqlExecution";
|
||||
@@ -7,6 +7,7 @@ import { useHistoryStore } from "@/stores/historyStore";
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { useProductionSafetyStore } from "@/stores/productionSafetyStore";
|
||||
import { createQueryEditorExecutionViewportOwnership } from "@/lib/editor/queryEditorExecutionViewport";
|
||||
import * as objectMetadataCache from "@/lib/metadata/objectMetadataCache";
|
||||
import type { ConnectionConfig, QueryTab } from "@/types/database";
|
||||
|
||||
@@ -1114,6 +1115,97 @@ SELECT @value AS Message;`;
|
||||
expect(addHistory).toHaveBeenCalledWith(expect.objectContaining({ success: true, error: undefined }));
|
||||
});
|
||||
|
||||
it("cancels the editor viewport request when dangerous SQL is dismissed", async () => {
|
||||
const sql = "DELETE FROM users WHERE id = 7";
|
||||
const activeTab = ref<QueryTab | undefined>({ ...queryTab("app"), sql });
|
||||
const activeConnection = ref<ConnectionConfig | undefined>(connection("mysql"));
|
||||
const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result");
|
||||
const queryStore = useQueryStore();
|
||||
const executeCurrentSql = vi.spyOn(queryStore, "executeCurrentSql");
|
||||
const onExecutionCancelled = vi.fn();
|
||||
|
||||
const execution = useSqlExecution({
|
||||
activeTab: computed(() => activeTab.value),
|
||||
activeConnection: computed(() => activeConnection.value),
|
||||
executableSql: computed(() => sql),
|
||||
activeOutputView,
|
||||
onExecutionCancelled,
|
||||
});
|
||||
|
||||
await execution.tryExecute({
|
||||
fullSql: sql,
|
||||
selectedSql: sql,
|
||||
cursorPos: 0,
|
||||
selectionFrom: 0,
|
||||
selectionTo: sql.length,
|
||||
editorViewportRequestId: 17,
|
||||
});
|
||||
|
||||
expect(execution.showDangerDialog.value).toBe(true);
|
||||
execution.showDangerDialog.value = false;
|
||||
await nextTick();
|
||||
|
||||
expect(onExecutionCancelled).toHaveBeenCalledTimes(1);
|
||||
expect(onExecutionCancelled).toHaveBeenCalledWith(17);
|
||||
expect(executeCurrentSql).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not reuse a cancelled viewport request on the next dangerous execution", async () => {
|
||||
const sql = "DELETE FROM users WHERE id = 7";
|
||||
const activeTab = ref<QueryTab | undefined>({ ...queryTab("app"), sql });
|
||||
const activeConnection = ref<ConnectionConfig | undefined>(connection("mysql"));
|
||||
const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result");
|
||||
const queryStore = useQueryStore();
|
||||
const ownership = createQueryEditorExecutionViewportOwnership();
|
||||
const executeCurrentSql = vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async (_sql, options) => {
|
||||
options?.onExecutionStarted?.();
|
||||
if (activeTab.value) activeTab.value.result = { columns: [], rows: [], affected_rows: 1, execution_time_ms: 1 };
|
||||
});
|
||||
vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined);
|
||||
const onExecutionStarted = vi.fn((requestId: number) => ownership.acceptRequest(requestId));
|
||||
const onExecutionCancelled = vi.fn((requestId: number) => ownership.cancelPendingRequest(requestId));
|
||||
|
||||
const execution = useSqlExecution({
|
||||
activeTab: computed(() => activeTab.value),
|
||||
activeConnection: computed(() => activeConnection.value),
|
||||
executableSql: computed(() => sql),
|
||||
activeOutputView,
|
||||
onExecutionStarted,
|
||||
onExecutionCancelled,
|
||||
});
|
||||
|
||||
const snapshotFor = (editorViewportRequestId: number) => ({
|
||||
fullSql: sql,
|
||||
selectedSql: sql,
|
||||
cursorPos: 0,
|
||||
selectionFrom: 0,
|
||||
selectionTo: sql.length,
|
||||
editorViewportRequestId,
|
||||
});
|
||||
const firstRequestId = ownership.beginRequest();
|
||||
await execution.tryExecute(snapshotFor(firstRequestId));
|
||||
expect(execution.showDangerDialog.value).toBe(true);
|
||||
|
||||
execution.showDangerDialog.value = false;
|
||||
await nextTick();
|
||||
|
||||
expect(onExecutionCancelled).toHaveBeenCalledTimes(1);
|
||||
expect(onExecutionCancelled).toHaveBeenCalledWith(firstRequestId);
|
||||
expect(ownership.acceptRequest(firstRequestId)).toBe(false);
|
||||
|
||||
const secondRequestId = ownership.beginRequest();
|
||||
await execution.tryExecute(snapshotFor(secondRequestId));
|
||||
expect(execution.showDangerDialog.value).toBe(true);
|
||||
|
||||
await execution.onDangerConfirm();
|
||||
|
||||
expect(onExecutionStarted).toHaveBeenCalledTimes(1);
|
||||
expect(onExecutionStarted).toHaveBeenCalledWith(secondRequestId);
|
||||
expect(ownership.consumeCompletionPreservation()).toBe(true);
|
||||
expect(ownership.consumeCompletionPreservation()).toBe(false);
|
||||
expect(executeCurrentSql).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("keeps the full dangerous script and new-result-tab intent through confirmation", async () => {
|
||||
const activeTab = ref<QueryTab | undefined>(queryTab("app"));
|
||||
const activeConnection = ref<ConnectionConfig | undefined>(connection("mysql"));
|
||||
|
||||
@@ -117,6 +117,7 @@ export function useSqlExecution(deps: {
|
||||
onMissingDatabase?: () => void;
|
||||
requestDangerConfirmation?: (request: SqlExecutionDangerRequest) => Promise<boolean>;
|
||||
onExecutionStarted?: (editorViewportRequestId: number) => void;
|
||||
onExecutionCancelled?: (editorViewportRequestId: number) => void;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const queryStore = useQueryStore();
|
||||
@@ -144,6 +145,10 @@ export function useSqlExecution(deps: {
|
||||
const pendingDangerEditorViewportRequestId = ref<number | undefined>();
|
||||
let pendingSqlParameterContinuation: ((sql: string, sourceOffset?: number) => Promise<void> | void) | undefined;
|
||||
|
||||
function cancelEditorViewportRequest(editorViewportRequestId?: number) {
|
||||
if (editorViewportRequestId !== undefined) deps.onExecutionCancelled?.(editorViewportRequestId);
|
||||
}
|
||||
|
||||
async function resolvedExecutableSql(source?: SqlExecutionOverride): Promise<{ sql: string; sourceOffset?: number; editorViewportRequestId?: number }> {
|
||||
const atSetEnabled = resolveSqlVariableSyntaxToggles(settingsStore.editorSettings.sqlVariableSyntaxOverrides, deps.activeConnection.value?.db_type, settingsStore.editorSettings.sqlVariableSubstitutionEnabled).atSet;
|
||||
const databaseType = effectiveDatabaseTypeForConnection(deps.activeConnection.value) ?? deps.activeConnection.value?.db_type;
|
||||
@@ -164,9 +169,13 @@ export function useSqlExecution(deps: {
|
||||
const tab = deps.activeTab.value;
|
||||
const { sql, sourceOffset, editorViewportRequestId } = await resolvedExecutableSql(sqlOverride);
|
||||
const executionOptions = { ...options, editorViewportRequestId };
|
||||
if (!tab || !sql.trim()) return;
|
||||
if (!tab || !sql.trim()) {
|
||||
cancelEditorViewportRequest(editorViewportRequestId);
|
||||
return;
|
||||
}
|
||||
if (requiresDatabaseSelection(tab, deps.activeConnection.value, sql)) {
|
||||
deps.onMissingDatabase?.();
|
||||
cancelEditorViewportRequest(editorViewportRequestId);
|
||||
return;
|
||||
}
|
||||
if (supportsSqlTemplateParameters(deps.activeConnection.value, sql) && prepareSqlParameterDialog(sql, sourceOffset, executionOptions)) return;
|
||||
@@ -178,7 +187,10 @@ export function useSqlExecution(deps: {
|
||||
}
|
||||
|
||||
async function continueExecute(sql: string, sourceOffset?: number, options: SqlExecutionOptions = {}) {
|
||||
if (!(await ensureReadOnlyWriteAccess({ connection: deps.activeConnection.value, sql, source: t("production.sourceSqlEditor") }))) return;
|
||||
if (!(await ensureReadOnlyWriteAccess({ connection: deps.activeConnection.value, sql, source: t("production.sourceSqlEditor") }))) {
|
||||
cancelEditorViewportRequest(options.editorViewportRequestId);
|
||||
return;
|
||||
}
|
||||
// Redis: block dangerous commands when toggle is on (scan entire batch for highest safety level)
|
||||
if (deps.activeConnection.value?.db_type === "redis" && deps.blockDangerousRedisCommands?.value !== false) {
|
||||
const commands = sql
|
||||
@@ -198,6 +210,7 @@ export function useSqlExecution(deps: {
|
||||
}
|
||||
if (highestSafety === "blocked") {
|
||||
toast(t("redis.blockedCommand", { command: "Redis" }), 5000);
|
||||
cancelEditorViewportRequest(options.editorViewportRequestId);
|
||||
return;
|
||||
}
|
||||
if (highestSafety === "confirm") {
|
||||
@@ -223,6 +236,7 @@ export function useSqlExecution(deps: {
|
||||
source: t("production.sourceSqlEditor"),
|
||||
});
|
||||
if (confirmed) await doExecute(sql, sourceOffset, options);
|
||||
else cancelEditorViewportRequest(options.editorViewportRequestId);
|
||||
return;
|
||||
}
|
||||
if (isDangerousSql(sql, deps.activeConnection.value?.db_type) && settingsStore.editorSettings.confirmDangerousSqlExecution) {
|
||||
@@ -284,13 +298,19 @@ export function useSqlExecution(deps: {
|
||||
}
|
||||
|
||||
async function doExecute(sql?: string, sourceOffset?: number, options: SqlExecutionOptions = {}) {
|
||||
if (sql === undefined) ({ sql, sourceOffset } = await resolvedExecutableSql());
|
||||
if (sql === undefined) {
|
||||
({ sql, sourceOffset } = await resolvedExecutableSql());
|
||||
}
|
||||
const tab = deps.activeTab.value;
|
||||
if (!tab || !sql.trim()) return;
|
||||
if (!tab || !sql.trim()) {
|
||||
cancelEditorViewportRequest(options.editorViewportRequestId);
|
||||
return;
|
||||
}
|
||||
const executionConnection = connectionStore.getConfig(tab.connectionId) ?? deps.activeConnection.value;
|
||||
const executionDatabaseType = executionConnection?.db_type;
|
||||
if (requiresDatabaseSelection(tab, executionConnection, sql)) {
|
||||
deps.onMissingDatabase?.();
|
||||
cancelEditorViewportRequest(options.editorViewportRequestId);
|
||||
return;
|
||||
}
|
||||
const statementCount = splitSqlStatementRanges(sql, executionDatabaseType).length;
|
||||
@@ -304,7 +324,10 @@ export function useSqlExecution(deps: {
|
||||
...(options.openInNewResultTab ? { openInNewResultTab: true } : {}),
|
||||
...(options.editorViewportRequestId !== undefined ? { onExecutionStarted: () => deps.onExecutionStarted?.(options.editorViewportRequestId!) } : {}),
|
||||
});
|
||||
if (producedResult === false) return;
|
||||
if (producedResult === false) {
|
||||
cancelEditorViewportRequest(options.editorViewportRequestId);
|
||||
return;
|
||||
}
|
||||
const executionTabStillActive = deps.activeTab.value?.id === tab.id;
|
||||
const sqlServerMessageResultIndex = executionDatabaseType === "sqlserver" ? tab.results?.findIndex((result) => result.server_message === true) : undefined;
|
||||
if (sqlServerMessageResultIndex !== undefined && sqlServerMessageResultIndex >= 0) {
|
||||
@@ -554,7 +577,10 @@ export function useSqlExecution(deps: {
|
||||
settingsStore.updateEditorSettings({ confirmDangerousSqlExecution: false });
|
||||
}
|
||||
suppressDangerConfirm.value = false;
|
||||
if (!(await ensureReadOnlyWriteAccess({ connection: deps.activeConnection.value, sql, source: t("production.sourceSqlEditor") }))) return;
|
||||
if (!(await ensureReadOnlyWriteAccess({ connection: deps.activeConnection.value, sql, source: t("production.sourceSqlEditor") }))) {
|
||||
cancelEditorViewportRequest(editorViewportRequestId);
|
||||
return;
|
||||
}
|
||||
await doExecute(sql, sourceOffset, { openInNewResultTab, editorViewportRequestId });
|
||||
}
|
||||
|
||||
@@ -578,6 +604,7 @@ export function useSqlExecution(deps: {
|
||||
|
||||
watch(showSqlParameterDialog, (open) => {
|
||||
if (open) return;
|
||||
const editorViewportRequestId = pendingSqlParameterEditorViewportRequestId.value;
|
||||
sqlParameterSourceSql.value = "";
|
||||
sqlParameterNames.value = [];
|
||||
sqlParameterDatabaseType.value = undefined;
|
||||
@@ -586,16 +613,19 @@ export function useSqlExecution(deps: {
|
||||
pendingOpenInNewResultTab.value = false;
|
||||
pendingSqlParameterEditorViewportRequestId.value = undefined;
|
||||
pendingSqlParameterContinuation = undefined;
|
||||
cancelEditorViewportRequest(editorViewportRequestId);
|
||||
});
|
||||
|
||||
watch(showDangerDialog, (open) => {
|
||||
if (open) return;
|
||||
const editorViewportRequestId = pendingDangerEditorViewportRequestId.value;
|
||||
pendingDangerSql.value = "";
|
||||
pendingDangerSourceOffset.value = undefined;
|
||||
pendingDangerKind.value = "sql";
|
||||
pendingOpenInNewResultTab.value = false;
|
||||
pendingDangerEditorViewportRequestId.value = undefined;
|
||||
suppressDangerConfirm.value = false;
|
||||
cancelEditorViewportRequest(editorViewportRequestId);
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
Reference in new issue
Block a user