import { computed, ref } from "vue"; import { createPinia, setActivePinia } from "pinia"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { isDangerousSql, requiresDatabaseSelection, supportsSqlTemplateParameters, useSqlExecution } from "../useSqlExecution"; import { useConnectionStore } from "@/stores/connectionStore"; import { useHistoryStore } from "@/stores/historyStore"; import { useQueryStore } from "@/stores/queryStore"; import { useSettingsStore } from "@/stores/settingsStore"; import { useProductionSafetyStore } from "@/stores/productionSafetyStore"; import * as objectMetadataCache from "@/lib/metadata/objectMetadataCache"; import type { ConnectionConfig, QueryTab } from "@/types/database"; vi.mock("vue-i18n", () => ({ createI18n: () => ({ global: { locale: { value: "en" }, setLocaleMessage: vi.fn() } }), useI18n: () => ({ t: (key: string) => key }), })); vi.mock("@/lib/backend/api", () => ({ saveEditorSettings: vi.fn(), saveHistory: vi.fn(), unlockConnectionWrites: vi.fn(), lockConnectionWrites: vi.fn(), connectionWriteUnlockState: vi.fn().mockResolvedValue(0), })); vi.mock("@/lib/metadata/objectMetadataCache", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, invalidateObjectMetadataCache: vi.fn() }; }); function installLocalStorage() { const data = new Map(); vi.stubGlobal("localStorage", { getItem: vi.fn((key: string) => data.get(key) ?? null), setItem: vi.fn((key: string, value: string) => data.set(key, value)), removeItem: vi.fn((key: string) => data.delete(key)), }); } function connection(dbType: ConnectionConfig["db_type"]): ConnectionConfig { return { id: "conn-1", name: "Local", db_type: dbType, host: "localhost", port: 3306, username: "root", password: "", }; } function queryTab(database = ""): QueryTab { return { id: "tab-1", connectionId: "conn-1", database, schema: undefined, title: "SQL", sql: "", mode: "query", isDirty: false, isExecuting: false, isCancelling: false, isExplaining: false, }; } describe("requiresDatabaseSelection", () => { beforeEach(() => { installLocalStorage(); setActivePinia(createPinia()); }); it("allows MySQL CREATE DATABASE to run without a selected database", () => { expect(requiresDatabaseSelection(queryTab(), connection("mysql"), "CREATE DATABASE app_db")).toBe(false); }); it("allows MySQL SHOW DATABASES to run without a selected database", () => { expect(requiresDatabaseSelection(queryTab(), connection("mysql"), "SHOW DATABASES")).toBe(false); }); it("allows MySQL SHOW VARIABLES without a selected database", () => { expect(requiresDatabaseSelection(queryTab(), connection("mysql"), "SHOW VARIABLES LIKE 'version%'")).toBe(false); }); it("allows MySQL CREATE SCHEMA with options to run without a selected database", () => { expect(requiresDatabaseSelection(queryTab(), connection("mysql"), "CREATE SCHEMA `app-db` DEFAULT CHARACTER SET utf8mb4")).toBe(false); }); it("allows MySQL install batches that switch databases before table DDL", () => { expect(requiresDatabaseSelection(queryTab(), connection("mysql"), "CREATE DATABASE app_db; USE app_db; CREATE TABLE users(id INT PRIMARY KEY)")).toBe(false); }); it("allows MySQL install batches with session setup before switching databases", () => { expect(requiresDatabaseSelection(queryTab(), connection("mysql"), "SET NAMES utf8mb4; DROP DATABASE IF EXISTS app_db; CREATE DATABASE app_db; USE app_db; INSERT INTO users VALUES (1)")).toBe(false); }); it("lets MySQL report statement-specific database requirements", () => { expect(requiresDatabaseSelection(queryTab(), connection("mysql"), "CREATE DATABASE app_db; CREATE TABLE users(id INT)")).toBe(false); }); it("lets MySQL reject malformed database switches", () => { expect(requiresDatabaseSelection(queryTab(), connection("mysql"), "CREATE DATABASE app_db; USE app_db SELECT 1; CREATE TABLE users(id INT)")).toBe(false); }); it.each(["SELECT 1", "SELECT VERSION()", "SELECT * FROM mysql.user", "SELECT * FROM users"])("allows connection-level MySQL query: %s", (sql) => { expect(requiresDatabaseSelection(queryTab(), connection("mysql"), sql)).toBe(false); }); it("still requires a database for non-MySQL multi-database connections", () => { expect(requiresDatabaseSelection(queryTab(), connection("mssql"), "SELECT * FROM dbo.users")).toBe(true); }); it("allows HANA with default database (empty string) to execute queries", () => { expect(requiresDatabaseSelection(queryTab(""), connection("saphana"), "SELECT * FROM MOMX_MES.Z_SHIPMENT_INFORMATION")).toBe(false); }); it("allows JDBC with default database (empty string) to execute queries", () => { expect(requiresDatabaseSelection(queryTab(""), connection("jdbc"), "SELECT * FROM users")).toBe(false); }); it("allows PostgreSQL with default database (empty string) to execute queries", () => { expect(requiresDatabaseSelection(queryTab(""), connection("postgres"), "SELECT * FROM public.users")).toBe(false); }); }); describe("supportsSqlTemplateParameters", () => { it("disables SQL parameter prompts for Meilisearch input", () => { expect(supportsSqlTemplateParameters(connection("meilisearch"), "GET /indexes/:uid")).toBe(false); expect(supportsSqlTemplateParameters(connection("meilisearch"), "plain :value")).toBe(false); }); }); describe("useSqlExecution", () => { beforeEach(() => { installLocalStorage(); setActivePinia(createPinia()); vi.mocked(objectMetadataCache.invalidateObjectMetadataCache).mockClear(); }); it("invalidates object metadata after successful connection-level DDL", async () => { const sql = "CREATE DATABASE app_db"; const activeTab = ref({ ...queryTab(), sql }); const activeConnection = ref(connection("mysql")); const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result"); const queryStore = useQueryStore(); const connectionStore = useConnectionStore(); vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => { if (activeTab.value) activeTab.value.result = { columns: [], rows: [], affected_rows: 1, execution_time_ms: 1 }; }); vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined); const invalidateMetadata = vi.spyOn(connectionStore, "invalidateMetadataCache"); const loadDatabases = vi.spyOn(connectionStore, "loadDatabases").mockResolvedValue(undefined); const execution = useSqlExecution({ activeTab: computed(() => activeTab.value), activeConnection: computed(() => activeConnection.value), executableSql: computed(() => sql), activeOutputView, }); await execution.tryExecute(); expect(invalidateMetadata).toHaveBeenCalledWith("conn-1"); expect(loadDatabases).toHaveBeenCalledWith("conn-1", { force: true }); }); it("invalidates table facets after successful Kingbase constraint DDL", async () => { const sql = "ALTER TABLE public.orders DISABLE CONSTRAINT orders_check"; const activeTab = ref({ ...queryTab("app"), schema: "public", sql }); const activeConnection = ref(connection("kingbase")); const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result"); const queryStore = useQueryStore(); const connectionStore = useConnectionStore(); useSettingsStore().editorSettings.confirmDangerousSqlExecution = false; vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => { if (activeTab.value) activeTab.value.result = { columns: [], rows: [], affected_rows: 0, execution_time_ms: 1 }; }); vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined); const invalidateObjectMetadata = vi.mocked(objectMetadataCache.invalidateObjectMetadataCache).mockResolvedValue(undefined); const refreshObjects = vi.spyOn(connectionStore, "refreshObjectListTreeNode").mockResolvedValue(undefined); const execution = useSqlExecution({ activeTab: computed(() => activeTab.value), activeConnection: computed(() => activeConnection.value), executableSql: computed(() => sql), activeOutputView, requestDangerConfirmation: async () => true, }); await execution.tryExecute(); expect(invalidateObjectMetadata).toHaveBeenCalledWith({ connectionId: "conn-1", database: "app", schema: "public" }); expect(refreshObjects).toHaveBeenCalledWith("conn-1", "app", "public"); }); it("sends every placeholder syntax and @set unchanged when substitution is disabled", async () => { const sql = ["@set tenant = 42;", "SELECT ?, :named, ${shell}, #{mybatis}, @sqlserver, @tenant;"].join("\n"); const activeTab = ref({ ...queryTab("app"), sql }); const activeConnection = ref(connection("mysql")); const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result"); const queryStore = useQueryStore(); const settingsStore = useSettingsStore(); const executeCurrentSql = vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => { if (activeTab.value) activeTab.value.result = { columns: ["ok"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 }; }); vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined); settingsStore.editorSettings.sqlVariableSubstitutionEnabled = false; const execution = useSqlExecution({ activeTab: computed(() => activeTab.value), activeConnection: computed(() => activeConnection.value), executableSql: computed(() => sql), activeOutputView, }); await execution.tryExecute(); expect(execution.showSqlParameterDialog.value).toBe(false); expect(execution.sqlParameterNames.value).toEqual([]); expect(executeCurrentSql).toHaveBeenCalledWith(sql, {}); }); it("passes the selected statement's editor offset to the query store", async () => { const fullSql = "SELECT * FROM users;\nSELECT * FROM users;"; const selectionFrom = fullSql.lastIndexOf("SELECT"); const selectedSql = fullSql.slice(selectionFrom, -1); const activeTab = ref({ ...queryTab("app"), sql: fullSql }); const activeConnection = ref(connection("mysql")); const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result"); const queryStore = useQueryStore(); const executeCurrentSql = vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => { if (activeTab.value) activeTab.value.result = { columns: ["id"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 }; }); vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined); const execution = useSqlExecution({ activeTab: computed(() => activeTab.value), activeConnection: computed(() => activeConnection.value), executableSql: computed(() => selectedSql), resolveExecutableSql: async () => selectedSql, activeOutputView, }); await execution.tryExecute({ fullSql, selectedSql, cursorPos: selectionFrom, selectionFrom, selectionTo: fullSql.length - 1 }); expect(executeCurrentSql).toHaveBeenCalledWith(selectedSql, { sourceOffset: selectionFrom }); }); it("expands preceding @set values in a selected shell-style statement", async () => { const selectedSql = "SELECT * FROM patrol WHERE post_id = ${postid};"; const fullSql = ["-- saved query", "@set postid='224';", selectedSql, "@set postid = 'future';"].join("\n"); const selectionFrom = fullSql.indexOf("SELECT"); const selectionTo = selectionFrom + selectedSql.length; const activeTab = ref({ ...queryTab("app"), sql: fullSql }); const activeConnection = ref(connection("postgres")); const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result"); const queryStore = useQueryStore(); const executeCurrentSql = vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => { if (activeTab.value) activeTab.value.result = { columns: ["post_id"], rows: [["224"]], affected_rows: 0, execution_time_ms: 1 }; }); vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined); const execution = useSqlExecution({ activeTab: computed(() => activeTab.value), activeConnection: computed(() => activeConnection.value), executableSql: computed(() => selectedSql), resolveExecutableSql: async () => selectedSql, activeOutputView, }); await execution.tryExecute({ fullSql, selectedSql, cursorPos: selectionFrom, selectionFrom, selectionTo }); expect(execution.showSqlParameterDialog.value).toBe(false); expect(executeCurrentSql).toHaveBeenCalledWith("SELECT * FROM patrol WHERE post_id = '224';", {}); }); it("opens the result table for a multi-statement batch by default", async () => { const sql = "SELECT 1;\nSELECT 2;"; const activeTab = ref({ ...queryTab("app"), sql }); const activeConnection = ref(connection("mysql")); const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result"); const queryStore = useQueryStore(); vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => { if (activeTab.value) activeTab.value.result = { columns: ["value"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 }; }); vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined); const execution = useSqlExecution({ activeTab: computed(() => activeTab.value), activeConnection: computed(() => activeConnection.value), executableSql: computed(() => sql), activeOutputView, }); await execution.tryExecute(); expect(activeOutputView.value).toBe("result"); }); it("opens the execution summary for a multi-statement batch when configured", async () => { const sql = "SELECT 1;\nSELECT 2;"; const activeTab = ref({ ...queryTab("app"), sql }); const activeConnection = ref(connection("mysql")); const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result"); const queryStore = useQueryStore(); useSettingsStore().editorSettings.multiStatementDefaultView = "summary"; vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => { if (activeTab.value) activeTab.value.result = { columns: ["value"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 }; }); vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined); const execution = useSqlExecution({ activeTab: computed(() => activeTab.value), activeConnection: computed(() => activeConnection.value), executableSql: computed(() => sql), activeOutputView, }); await execution.tryExecute(); expect(activeOutputView.value).toBe("summary"); }); it("does not change the current output view when a background tab finishes", async () => { const sql = "SELECT run_side_effect()"; const executionTab = { ...queryTab("app"), sql }; const activeTab = ref(executionTab); const activeConnection = ref(connection("mysql")); const activeOutputView = ref<"result" | "summary" | "explain" | "chart" | "messages">("result"); const queryStore = useQueryStore(); let finishExecution!: () => void; const executionFinished = new Promise((resolve) => { finishExecution = resolve; }); const executeCurrentSql = vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => { await executionFinished; executionTab.result = { columns: [], rows: [], affected_rows: 1, execution_time_ms: 1 }; }); vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined); const execution = useSqlExecution({ activeTab: computed(() => activeTab.value), activeConnection: computed(() => activeConnection.value), executableSql: computed(() => sql), activeOutputView, }); const pending = execution.tryExecute(); await vi.waitFor(() => expect(executeCurrentSql).toHaveBeenCalledOnce()); activeTab.value = { ...queryTab("app"), id: "tab-2" }; activeOutputView.value = "messages"; finishExecution(); await pending; expect(activeOutputView.value).toBe("messages"); }); it("shows SQL Server PRINT messages instead of leaving them behind the execution summary", async () => { const sql = `IF 1 = 1 BEGIN PRINT 'x'; END ELSE BEGIN PRINT 'y'; END GO`; const activeTab = ref({ ...queryTab("dbx_sqlserver_demo"), sql }); const activeConnection = ref(connection("sqlserver")); const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result"); const queryStore = useQueryStore(); vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => { if (activeTab.value) activeTab.value.result = { columns: ["Message"], column_types: ["nvarchar"], rows: [["x"]], affected_rows: 0, execution_time_ms: 1, server_message: true }; }); vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined); const execution = useSqlExecution({ activeTab: computed(() => activeTab.value), activeConnection: computed(() => activeConnection.value), executableSql: computed(() => sql), activeOutputView, }); await execution.tryExecute(); expect(activeOutputView.value).toBe("result"); expect(activeTab.value?.result?.rows).toEqual([["x"]]); }); it("keeps a SQL Server data result selected when a trailing message result exists", async () => { const sql = "SELECT 1 AS value; PRINT N'x';"; const activeTab = ref({ ...queryTab("dbx_sqlserver_demo"), sql }); const activeConnection = ref(connection("sqlserver")); const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result"); const queryStore = useQueryStore(); const setActiveResultIndex = vi.spyOn(queryStore, "setActiveResultIndex").mockImplementation((_id, index) => { if (!activeTab.value?.results) return; activeTab.value.activeResultIndex = index; activeTab.value.result = activeTab.value.results[index]; }); vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => { if (!activeTab.value) return; const dataResult = { columns: ["value"], column_types: ["int"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 }; const messageResult = { columns: ["Message"], column_types: ["nvarchar"], rows: [["x"]], affected_rows: 0, execution_time_ms: 1, server_message: true }; activeTab.value.results = [dataResult, messageResult]; activeTab.value.activeResultIndex = 0; activeTab.value.result = dataResult; }); vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined); const execution = useSqlExecution({ activeTab: computed(() => activeTab.value), activeConnection: computed(() => activeConnection.value), executableSql: computed(() => sql), activeOutputView, }); await execution.tryExecute(); expect(activeOutputView.value).toBe("result"); expect(setActiveResultIndex).not.toHaveBeenCalled(); expect(activeTab.value?.activeResultIndex).toBe(0); expect(activeTab.value?.result?.server_message).toBeUndefined(); expect(activeTab.value?.result?.rows).toEqual([[1]]); }); it("selects a SQL Server data result after an earlier message result", async () => { const sql = "PRINT N'x'; SELECT 1 AS value;"; const activeTab = ref({ ...queryTab("dbx_sqlserver_demo"), sql }); const activeConnection = ref(connection("sqlserver")); const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result"); const queryStore = useQueryStore(); const setActiveResultIndex = vi.spyOn(queryStore, "setActiveResultIndex").mockImplementation((_id, index) => { if (!activeTab.value?.results) return; activeTab.value.activeResultIndex = index; activeTab.value.result = activeTab.value.results[index]; }); vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => { if (!activeTab.value) return; const messageResult = { columns: ["Message"], column_types: ["nvarchar"], rows: [["x"]], affected_rows: 0, execution_time_ms: 1, server_message: true } as const; const dataResult = { columns: ["value"], column_types: ["int"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 }; activeTab.value.results = [messageResult, dataResult]; activeTab.value.activeResultIndex = 0; activeTab.value.result = messageResult; }); vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined); const execution = useSqlExecution({ activeTab: computed(() => activeTab.value), activeConnection: computed(() => activeConnection.value), executableSql: computed(() => sql), activeOutputView, }); await execution.tryExecute(); expect(activeOutputView.value).toBe("result"); expect(setActiveResultIndex).toHaveBeenCalledWith("tab-1", 1); expect(activeTab.value?.activeResultIndex).toBe(1); expect(activeTab.value?.result?.rows).toEqual([[1]]); }); it("keeps a SQL Server execution error selected when a message result also exists", async () => { const sql = "SELECT missing_column FROM demo;"; const activeTab = ref({ ...queryTab("dbx_sqlserver_demo"), sql }); const activeConnection = ref(connection("sqlserver")); const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result"); const queryStore = useQueryStore(); const setActiveResultIndex = vi.spyOn(queryStore, "setActiveResultIndex"); vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => { if (!activeTab.value) return; const messageResult = { columns: ["Message"], column_types: ["nvarchar"], rows: [["before error"]], affected_rows: 0, execution_time_ms: 1, server_message: true }; const errorResult = { columns: ["Error"], column_types: ["string"], rows: [["Invalid column name"]], affected_rows: 0, execution_time_ms: 1, execution_error: true }; activeTab.value.results = [messageResult, errorResult]; activeTab.value.activeResultIndex = 1; activeTab.value.result = errorResult; }); vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined); const execution = useSqlExecution({ activeTab: computed(() => activeTab.value), activeConnection: computed(() => activeConnection.value), executableSql: computed(() => sql), activeOutputView, }); await execution.tryExecute(); expect(setActiveResultIndex).not.toHaveBeenCalled(); expect(activeTab.value?.activeResultIndex).toBe(1); expect(activeTab.value?.result?.execution_error).toBe(true); }); it("opens ordinary SQL Server multi-result batches in the result table by default", async () => { const sql = "SELECT 1 AS first_value; SELECT 2 AS second_value;"; const activeTab = ref({ ...queryTab("dbx_sqlserver_demo"), sql }); const activeConnection = ref(connection("sqlserver")); const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result"); const queryStore = useQueryStore(); const setActiveResultIndex = vi.spyOn(queryStore, "setActiveResultIndex"); vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => { if (!activeTab.value) return; const firstResult = { columns: ["first_value"], column_types: ["int"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 }; const secondResult = { columns: ["second_value"], column_types: ["int"], rows: [[2]], affected_rows: 0, execution_time_ms: 1 }; activeTab.value.results = [firstResult, secondResult]; activeTab.value.activeResultIndex = 0; activeTab.value.result = firstResult; }); vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined); const execution = useSqlExecution({ activeTab: computed(() => activeTab.value), activeConnection: computed(() => activeConnection.value), executableSql: computed(() => sql), activeOutputView, }); await execution.tryExecute(); expect(activeOutputView.value).toBe("result"); expect(setActiveResultIndex).not.toHaveBeenCalled(); expect(activeTab.value?.result?.rows).toEqual([[1]]); }); it("opens ordinary SQL Server data aliased as Message in the result table by default", async () => { const sql = `DECLARE @value nvarchar(1) = N'x'; SELECT @value AS Message;`; const activeTab = ref({ ...queryTab("dbx_sqlserver_demo"), sql }); const activeConnection = ref(connection("sqlserver")); const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result"); const queryStore = useQueryStore(); vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => { if (activeTab.value) activeTab.value.result = { columns: ["Message"], column_types: ["nvarchar"], rows: [["x"]], affected_rows: 0, execution_time_ms: 1 }; }); vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined); const execution = useSqlExecution({ activeTab: computed(() => activeTab.value), activeConnection: computed(() => activeConnection.value), executableSql: computed(() => sql), activeOutputView, }); await execution.tryExecute(); expect(activeOutputView.value).toBe("result"); expect(activeTab.value?.result?.rows).toEqual([["x"]]); }); it("opens the messages view for a message-only result", async () => { const sql = "DO $$ BEGIN RAISE NOTICE 'hello'; END $$;"; const activeTab = ref({ ...queryTab("app"), sql }); const activeConnection = ref(connection("postgres")); const activeOutputView = ref<"result" | "summary" | "explain" | "chart" | "messages">("result"); const queryStore = useQueryStore(); vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => { if (activeTab.value) activeTab.value.result = { columns: [], rows: [], affected_rows: 0, execution_time_ms: 1, messages: [{ severity: "NOTICE", message: "hello", code: "00000" }] }; }); vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined); const execution = useSqlExecution({ activeTab: computed(() => activeTab.value), activeConnection: computed(() => activeConnection.value), executableSql: computed(() => sql), activeOutputView, }); await execution.tryExecute(); expect(activeOutputView.value).toBe("messages"); }); it("keeps the summary view for a MySQL INSERT that carries an INFO message", async () => { const sql = "INSERT INTO users (name) VALUES ('a'), ('b')"; const activeTab = ref({ ...queryTab("app"), sql }); const activeConnection = ref(connection("mysql")); const activeOutputView = ref<"result" | "summary" | "explain" | "chart" | "messages">("result"); const queryStore = useQueryStore(); vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => { if (activeTab.value) activeTab.value.result = { columns: [], rows: [], affected_rows: 2, execution_time_ms: 1, messages: [{ severity: "Note", message: "Records: 2 Duplicates: 0 Warnings: 0" }] }; }); vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined); const execution = useSqlExecution({ activeTab: computed(() => activeTab.value), activeConnection: computed(() => activeConnection.value), executableSql: computed(() => sql), activeOutputView, }); await execution.tryExecute(); expect(activeOutputView.value).toBe("summary"); }); it("keeps the summary view for a batch whose statements emit messages", async () => { const sql = "DO $$ BEGIN RAISE NOTICE 'one'; END $$;\nDO $$ BEGIN RAISE NOTICE 'two'; END $$;"; const activeTab = ref({ ...queryTab("app"), sql }); const activeConnection = ref(connection("postgres")); const activeOutputView = ref<"result" | "summary" | "explain" | "chart" | "messages">("result"); const queryStore = useQueryStore(); vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => { if (activeTab.value) activeTab.value.result = { columns: [], rows: [], affected_rows: 0, execution_time_ms: 1, messages: [ { severity: "NOTICE", message: "one" }, { severity: "NOTICE", message: "two" }, ], }; }); vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined); const execution = useSqlExecution({ activeTab: computed(() => activeTab.value), activeConnection: computed(() => activeConnection.value), executableSql: computed(() => sql), activeOutputView, }); await execution.tryExecute(); expect(activeOutputView.value).toBe("summary"); }); it("keeps the result view when messages accompany a tabular result", async () => { const sql = "SELECT 1"; const activeTab = ref({ ...queryTab("app"), sql }); const activeConnection = ref(connection("postgres")); const activeOutputView = ref<"result" | "summary" | "explain" | "chart" | "messages">("result"); const queryStore = useQueryStore(); vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => { if (activeTab.value) activeTab.value.result = { columns: ["value"], rows: [[1]], affected_rows: 0, execution_time_ms: 1, messages: [{ severity: "NOTICE", message: "hello" }] }; }); vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined); const execution = useSqlExecution({ activeTab: computed(() => activeTab.value), activeConnection: computed(() => activeConnection.value), executableSql: computed(() => sql), activeOutputView, }); await execution.tryExecute(); expect(activeOutputView.value).toBe("result"); }); it("forwards execute-in-new-result-tab intent to the query store", async () => { const sql = "SELECT * FROM users"; const activeTab = ref({ ...queryTab("app"), sql }); const activeConnection = ref(connection("mysql")); const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result"); const queryStore = useQueryStore(); const executeCurrentSql = vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => { if (activeTab.value) activeTab.value.result = { columns: ["id"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 }; }); vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined); const execution = useSqlExecution({ activeTab: computed(() => activeTab.value), activeConnection: computed(() => activeConnection.value), executableSql: computed(() => sql), activeOutputView, }); await execution.tryExecuteInNewResultTab(); expect(executeCurrentSql).toHaveBeenCalledWith(sql, { openInNewResultTab: true }); }); it("does not record or refresh when a new-result execution restores the prior run", async () => { const result = { columns: ["value"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 }; const activeTab = ref({ ...queryTab("app"), result }); const activeConnection = ref(connection("mysql")); const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result"); const queryStore = useQueryStore(); vi.spyOn(queryStore, "executeCurrentSql").mockResolvedValue(false); const addHistory = vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined); const refreshObjects = vi.spyOn(useConnectionStore(), "refreshObjectListTreeNode").mockResolvedValue(undefined); const execution = useSqlExecution({ activeTab: computed(() => activeTab.value), activeConnection: computed(() => activeConnection.value), executableSql: computed(() => "CREATE TABLE users (id INT)"), activeOutputView, }); await execution.tryExecuteInNewResultTab(); expect(addHistory).not.toHaveBeenCalled(); expect(refreshObjects).not.toHaveBeenCalled(); }); it("keeps the new-result-tab intent through SQL parameter input", async () => { const sql = "SELECT * FROM users WHERE id = :id"; const resolvedSql = "SELECT * FROM users WHERE id = 7"; const activeTab = ref({ ...queryTab("app"), sql }); const activeConnection = ref(connection("mysql")); const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result"); const queryStore = useQueryStore(); const executeCurrentSql = vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => { if (activeTab.value) activeTab.value.result = { columns: ["id"], rows: [[7]], affected_rows: 0, execution_time_ms: 1 }; }); vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined); const execution = useSqlExecution({ activeTab: computed(() => activeTab.value), activeConnection: computed(() => activeConnection.value), executableSql: computed(() => sql), activeOutputView, }); await execution.tryExecuteInNewResultTab(); expect(execution.showSqlParameterDialog.value).toBe(true); expect(executeCurrentSql).not.toHaveBeenCalled(); await execution.onSqlParametersConfirm(resolvedSql); expect(executeCurrentSql).toHaveBeenCalledWith(resolvedSql, { openInNewResultTab: true }); }); it("executes the PostgreSQL ARRAY date-format query without opening the parameter dialog", async () => { const sql = ` WITH rec_flow AS ( SELECT order_no, ARRAY[ concat( operator_name, '(', COALESCE(remark, ''), ')[', CASE operate_action WHEN 'CREATE' THEN '创建工单' WHEN 'SUBMIT' THEN '提交至下一处理人' WHEN 'BACK' THEN '退回上一环节' WHEN 'FINISH' THEN '已完成' WHEN 'SUBMIT-CONFIRM' THEN '提交给创建人确认' WHEN 'COMPLETE' THEN '确认工单' ELSE operate_action END, ']' ) ]::varchar[] || CASE WHEN operate_action NOT IN ('FINISH','COMPLETE') THEN ARRAY[target_handler_name]::varchar[] ELSE ARRAY[]::varchar[] END AS name_arr, rn FROM ( SELECT 'ORD20260821001' AS order_no, '张三' AS operator_name, '发起流程' AS remark, 'SUBMIT' AS operate_action, '李四' AS target_handler_name, 1 AS rn ) AS mock_t3_flow ) SELECT to_char(current_timestamp, 'yyyy-MM-dd HH24:mi:ss'); `; const activeTab = ref(queryTab("app")); const activeConnection = ref(connection("postgres")); const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result"); const queryStore = useQueryStore(); const executeCurrentSql = vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => { if (activeTab.value) activeTab.value.result = { columns: ["value"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 }; }); vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined); const execution = useSqlExecution({ activeTab: computed(() => activeTab.value), activeConnection: computed(() => activeConnection.value), executableSql: computed(() => sql), activeOutputView, }); await execution.tryExecute(); expect(execution.showSqlParameterDialog.value).toBe(false); expect(execution.sqlParameterNames.value).toEqual([]); expect(executeCurrentSql).toHaveBeenCalledWith(sql, {}); }); it("executes Oracle database-link queries without opening the parameter dialog", async () => { const sql = "SELECT 1 FROM DUAL@WDHIS160;"; const activeTab = ref(queryTab("ORCL")); const activeConnection = ref(connection("oracle")); const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result"); const queryStore = useQueryStore(); const executeCurrentSql = vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => { if (activeTab.value) activeTab.value.result = { columns: ["1"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 }; }); vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined); const execution = useSqlExecution({ activeTab: computed(() => activeTab.value), activeConnection: computed(() => activeConnection.value), executableSql: computed(() => sql), activeOutputView, }); await execution.tryExecute(); expect(execution.showSqlParameterDialog.value).toBe(false); expect(execution.sqlParameterNames.value).toEqual([]); expect(executeCurrentSql).toHaveBeenCalledWith(sql, {}); }); it("sends native SET variables without client-side expansion", async () => { const activeTab = ref(queryTab("app")); const activeConnection = ref(connection("mysql")); const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result"); const queryStore = useQueryStore(); const historyStore = useHistoryStore(); const executeCurrentSql = vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => { if (activeTab.value) activeTab.value.result = { columns: ["ok"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 }; }); vi.spyOn(historyStore, "add").mockResolvedValue(undefined); const execution = useSqlExecution({ activeTab: computed(() => activeTab.value), activeConnection: computed(() => activeConnection.value), executableSql: computed( () => ` set @date_start = '2026-07-04 00:00:00'; select * from sa_access_decision_log AS fp where fp.create_at < @date_start; `, ), activeOutputView, }); await execution.tryExecute(); const executedSql = executeCurrentSql.mock.calls[0]?.[0] ?? ""; expect(executedSql).toContain("set @date_start = '2026-07-04 00:00:00'"); expect(executedSql).toContain("where fp.create_at < @date_start"); }); it("executes MySQL cursor procedures with compact labels without opening the parameter dialog", async () => { const sql = ` CREATE PROCEDURE process_orders() BEGIN DECLARE done INT DEFAULT FALSE; DECLARE order_id INT; DECLARE cur_orders CURSOR FOR SELECT id FROM orders; DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE; OPEN cur_orders; read_loop:LOOP FETCH cur_orders INTO order_id; IF done THEN LEAVE read_loop; END IF; END LOOP read_loop; CLOSE cur_orders; END `; const activeTab = ref(queryTab("app")); const activeConnection = ref(connection("mysql")); const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result"); const queryStore = useQueryStore(); const executeCurrentSql = vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => { if (activeTab.value) activeTab.value.result = { columns: [], rows: [], affected_rows: 0, execution_time_ms: 1 }; }); vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined); vi.spyOn(useConnectionStore(), "refreshObjectListTreeNode").mockResolvedValue(undefined); const execution = useSqlExecution({ activeTab: computed(() => activeTab.value), activeConnection: computed(() => activeConnection.value), executableSql: computed(() => sql), activeOutputView, }); await execution.tryExecute(); expect(execution.showSqlParameterDialog.value).toBe(false); expect(execution.sqlParameterNames.value).toEqual([]); expect(executeCurrentSql).toHaveBeenCalledWith(sql, {}); }); it.each([ { label: "JDBC MySQL", config: { ...connection("jdbc"), driver_label: "MySQL JDBC", connection_string: "jdbc:mysql://127.0.0.1:3306/app", }, }, { label: "GBase MySQL compatibility", config: { ...connection("gbase"), driver_profile: "gbase", driver_label: "GBase 8a", }, }, ])("executes $label cursor procedures from the gutter without opening the parameter dialog", async ({ config }) => { const sql = `CREATE PROCEDURE process_orders() BEGIN DECLARE done INT DEFAULT FALSE; DECLARE order_id INT; DECLARE cur_orders CURSOR FOR SELECT id FROM orders; DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE; OPEN cur_orders; read_loop:LOOP FETCH cur_orders INTO order_id; IF done THEN LEAVE read_loop; END IF; END LOOP read_loop; CLOSE cur_orders; END`; const activeTab = ref({ ...queryTab("app"), sql }); const activeConnection = ref(config); const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result"); const queryStore = useQueryStore(); const executeCurrentSql = vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => { if (activeTab.value) activeTab.value.result = { columns: [], rows: [], affected_rows: 0, execution_time_ms: 1 }; }); vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined); vi.spyOn(useConnectionStore(), "refreshObjectListTreeNode").mockResolvedValue(undefined); const execution = useSqlExecution({ activeTab: computed(() => activeTab.value), activeConnection: computed(() => activeConnection.value), executableSql: computed(() => sql), resolveExecutableSql: async () => sql, activeOutputView, }); await execution.tryExecute({ fullSql: sql, selectedSql: sql, cursorPos: 0, selectionFrom: 0, selectionTo: sql.length, editorViewportRequestId: 1, }); expect(execution.showSqlParameterDialog.value).toBe(false); expect(execution.sqlParameterNames.value).toEqual([]); expect(executeCurrentSql).toHaveBeenCalledWith(sql, { sourceOffset: 0, onExecutionStarted: expect.any(Function) }); }); it("sends Doris STRUCT DDL unchanged without opening the parameter dialog", async () => { const sql = ` create table \`events\` ( \`field0\` int not null comment 'field 0', \`field_list\` array> comment 'field list' ) engine = olap properties ("replication_num" = "1"); `; const activeTab = ref(queryTab("analytics")); const activeConnection = ref(connection("doris")); const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result"); const queryStore = useQueryStore(); const executeCurrentSql = vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => { if (activeTab.value) activeTab.value.result = { columns: ["ok"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 }; }); vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined); vi.spyOn(useConnectionStore(), "refreshObjectListTreeNode").mockResolvedValue(undefined); const execution = useSqlExecution({ activeTab: computed(() => activeTab.value), activeConnection: computed(() => activeConnection.value), executableSql: computed(() => sql), activeOutputView, }); await execution.tryExecute(); expect(execution.showSqlParameterDialog.value).toBe(false); expect(execution.sqlParameterNames.value).toEqual([]); expect(executeCurrentSql).toHaveBeenCalledWith(sql, {}); }); it("records a later MySQL batch error and skips metadata refresh", async () => { const activeTab = ref(queryTab("app")); const activeConnection = ref(connection("mysql")); const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result"); const queryStore = useQueryStore(); const historyStore = useHistoryStore(); const connectionStore = useConnectionStore(); vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => { const tab = activeTab.value; if (!tab) return; const successfulResult = { columns: ["value"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 }; tab.result = successfulResult; tab.results = [successfulResult, { columns: ["Error"], execution_error: true, rows: [["Duplicate entry '1'"]], affected_rows: 0, execution_time_ms: 1 }]; tab.activeResultIndex = 0; }); const addHistory = vi.spyOn(historyStore, "add").mockResolvedValue(undefined); const refreshObjects = vi.spyOn(connectionStore, "refreshObjectListTreeNode").mockResolvedValue(undefined); const execution = useSqlExecution({ activeTab: computed(() => activeTab.value), activeConnection: computed(() => activeConnection.value), executableSql: computed(() => "SELECT 1 AS value; CREATE TABLE duplicate_target (id INT)"), activeOutputView, }); await execution.tryExecute(); expect(addHistory).toHaveBeenCalledWith(expect.objectContaining({ success: false, error: "Duplicate entry '1'", affected_rows: undefined })); expect(refreshObjects).not.toHaveBeenCalled(); }); it("records a later PostgreSQL batch error and skips metadata refresh", async () => { const activeTab = ref(queryTab("app")); const activeConnection = ref(connection("postgres")); const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result"); const queryStore = useQueryStore(); const historyStore = useHistoryStore(); const connectionStore = useConnectionStore(); vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => { const tab = activeTab.value; if (!tab) return; const successfulResult = { columns: ["value"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 }; tab.result = successfulResult; tab.results = [successfulResult, { columns: ["Error"], execution_error: true, rows: [["relation missing_table does not exist"]], affected_rows: 0, execution_time_ms: 1 }]; tab.activeResultIndex = 0; }); const addHistory = vi.spyOn(historyStore, "add").mockResolvedValue(undefined); const refreshObjects = vi.spyOn(connectionStore, "refreshObjectListTreeNode").mockResolvedValue(undefined); const execution = useSqlExecution({ activeTab: computed(() => activeTab.value), activeConnection: computed(() => activeConnection.value), executableSql: computed(() => "BEGIN; SELECT 1 AS value; SELECT * FROM missing_table"), activeOutputView, }); await execution.tryExecute(); expect(addHistory).toHaveBeenCalledWith(expect.objectContaining({ success: false, error: "relation missing_table does not exist", affected_rows: undefined })); expect(refreshObjects).not.toHaveBeenCalled(); }); it("does not treat an unmarked MySQL Error alias as a batch failure", async () => { const activeTab = ref(queryTab("app")); const activeConnection = ref(connection("mysql")); const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result"); const queryStore = useQueryStore(); const historyStore = useHistoryStore(); vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => { const tab = activeTab.value; if (!tab) return; const successfulResult = { columns: ["value"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 }; tab.result = successfulResult; tab.results = [successfulResult, { columns: ["Error"], rows: [[2]], affected_rows: 0, execution_time_ms: 1 }]; tab.activeResultIndex = 0; }); const addHistory = vi.spyOn(historyStore, "add").mockResolvedValue(undefined); const execution = useSqlExecution({ activeTab: computed(() => activeTab.value), activeConnection: computed(() => activeConnection.value), executableSql: computed(() => "SELECT 1 AS value; SELECT 2 AS Error"), activeOutputView, }); await execution.tryExecute(); expect(addHistory).toHaveBeenCalledWith(expect.objectContaining({ success: true, error: undefined })); }); it("continues to record explicitly marked non-MySQL errors as failures", async () => { const activeTab = ref(queryTab("app")); const activeConnection = ref(connection("postgres")); const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result"); const queryStore = useQueryStore(); const historyStore = useHistoryStore(); vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => { if (activeTab.value) activeTab.value.result = { columns: ["Error"], rows: [["relation does not exist"]], affected_rows: 0, execution_time_ms: 1, execution_error: true }; }); const addHistory = vi.spyOn(historyStore, "add").mockResolvedValue(undefined); const execution = useSqlExecution({ activeTab: computed(() => activeTab.value), activeConnection: computed(() => activeConnection.value), executableSql: computed(() => "SELECT * FROM missing_table"), activeOutputView, }); await execution.tryExecute(); expect(addHistory).toHaveBeenCalledWith(expect.objectContaining({ success: false, error: "relation does not exist" })); }); it("does not treat an unmarked PostgreSQL Error alias as a failure", async () => { const activeTab = ref(queryTab("app")); const activeConnection = ref(connection("postgres")); const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result"); const queryStore = useQueryStore(); const historyStore = useHistoryStore(); vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => { if (activeTab.value) activeTab.value.result = { columns: ["Error"], rows: [[2]], affected_rows: 0, execution_time_ms: 1 }; }); const addHistory = vi.spyOn(historyStore, "add").mockResolvedValue(undefined); const execution = useSqlExecution({ activeTab: computed(() => activeTab.value), activeConnection: computed(() => activeConnection.value), executableSql: computed(() => "SELECT 2 AS Error"), activeOutputView, }); await execution.tryExecute(); expect(addHistory).toHaveBeenCalledWith(expect.objectContaining({ success: true, error: undefined })); }); it("keeps the full dangerous script and new-result-tab intent through confirmation", async () => { const activeTab = ref(queryTab("app")); const activeConnection = ref(connection("mysql")); const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result"); const queryStore = useQueryStore(); const sql = Array.from({ length: 40_000 }, (_, index) => `${index === 0 ? "DROP TABLE IF EXISTS t;" : ""} INSERT INTO t VALUES (${index});`).join("\n"); const executeCurrentSql = vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => { if (activeTab.value) activeTab.value.result = { columns: [], rows: [], affected_rows: 40_000, execution_time_ms: 1 }; }); vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined); const execution = useSqlExecution({ activeTab: computed(() => activeTab.value), activeConnection: computed(() => activeConnection.value), executableSql: computed(() => sql), activeOutputView, }); await execution.tryExecuteInNewResultTab(); expect(execution.showDangerDialog.value).toBe(true); expect(execution.pendingDangerSql.value).toBe(sql); expect(executeCurrentSql).not.toHaveBeenCalled(); await execution.onDangerConfirm(); expect(executeCurrentSql).toHaveBeenCalledWith(sql, { openInNewResultTab: true }); }); it("keeps the active retained result when a new-result dangerous prompt is cancelled", async () => { const result = { columns: ["value"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 }; const activeTab = ref({ ...queryTab("app"), result, resultRuns: [{ id: "run-1", title: "Run 1", sequence: 1, sql: "SELECT 1", createdAt: 1, result }], activeResultRunId: "run-1", }); const activeConnection = ref(connection("mysql")); const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result"); const queryStore = useQueryStore(); const executeCurrentSql = vi.spyOn(queryStore, "executeCurrentSql"); const execution = useSqlExecution({ activeTab: computed(() => activeTab.value), activeConnection: computed(() => activeConnection.value), executableSql: computed(() => "DROP TABLE users"), activeOutputView, }); await execution.tryExecuteInNewResultTab(); expect(execution.showDangerDialog.value).toBe(true); execution.showDangerDialog.value = false; await Promise.resolve(); expect(executeCurrentSql).not.toHaveBeenCalled(); expect(activeTab.value?.activeResultRunId).toBe("run-1"); expect(activeTab.value?.result).toEqual(result); }); it("uses the inferred Oracle dialect when explaining through custom JDBC", async () => { const sql = "SELECT * FROM DUAL"; const activeTab = ref({ ...queryTab("ORCL"), sql }); const activeConnection = ref({ ...connection("jdbc"), connection_string: "jdbc:oracle:thin:@127.0.0.1:1521:ORCL", jdbc_driver_class: "oracle.jdbc.OracleDriver", }); const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result"); const queryStore = useQueryStore(); const explainTabSql = vi.spyOn(queryStore, "explainTabSql").mockResolvedValue({ ok: true, sql }); const execution = useSqlExecution({ activeTab: computed(() => activeTab.value), activeConnection: computed(() => activeConnection.value), executableSql: computed(() => sql), activeOutputView, }); await execution.tryExplain(); expect(explainTabSql).toHaveBeenCalledWith("tab-1", sql, "oracle", "explain"); }); it("keeps the new-result-tab intent through Redis command confirmation", async () => { const sql = "DEL user:1"; const activeTab = ref({ ...queryTab("0"), sql }); const activeConnection = ref(connection("redis")); const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result"); const queryStore = useQueryStore(); const executeCurrentSql = vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => { if (activeTab.value) activeTab.value.result = { columns: [], rows: [], affected_rows: 1, execution_time_ms: 1 }; }); vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined); const execution = useSqlExecution({ activeTab: computed(() => activeTab.value), activeConnection: computed(() => activeConnection.value), executableSql: computed(() => sql), activeOutputView, blockDangerousRedisCommands: ref(true), }); await execution.tryExecuteInNewResultTab(); expect(execution.showDangerDialog.value).toBe(true); expect(executeCurrentSql).not.toHaveBeenCalled(); await execution.onDangerConfirm(); expect(executeCurrentSql).toHaveBeenCalledWith(sql, { skipRedisSafetyCheck: false, openInNewResultTab: true }); }); it("distinguishes read-only and mutating Meilisearch REST requests", () => { expect(isDangerousSql("GET /health", "meilisearch")).toBe(false); expect(isDangerousSql('POST /indexes/movies/documents/fetch\n{"limit":10}', "meilisearch")).toBe(false); expect(isDangerousSql('POST /indexes/movies/search\n{"q":"space"}', "meilisearch")).toBe(false); expect(isDangerousSql('POST /indexes\n{"uid":"movies"}', "meilisearch")).toBe(true); expect(isDangerousSql("PUT /indexes/movies/settings", "meilisearch")).toBe(true); expect(isDangerousSql("DELETE /indexes/movies/documents/001", "meilisearch")).toBe(true); }); it("treats Elasticsearch PATCH requests as mutating", () => { expect(isDangerousSql('PATCH /products/_settings\n{"index":{"refresh_interval":"5s"}}', "elasticsearch")).toBe(true); expect(isDangerousSql('PATCH /products/_settings\n{"index":{"refresh_interval":"5s"}}', "easysearch")).toBe(true); }); it("requires production confirmation even when ordinary danger prompts are disabled", async () => { const activeTab = ref(queryTab("prod_app")); const activeConnection = ref({ ...connection("mysql"), production_databases: ["prod_app"] }); const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result"); const queryStore = useQueryStore(); const settingsStore = useSettingsStore(); const productionSafetyStore = useProductionSafetyStore(); const executeCurrentSql = vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => { if (activeTab.value) activeTab.value.result = { columns: ["ok"], rows: [[1]], affected_rows: 1, execution_time_ms: 1 }; }); vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined); settingsStore.editorSettings.confirmDangerousSqlExecution = false; const execution = useSqlExecution({ activeTab: computed(() => activeTab.value), activeConnection: computed(() => activeConnection.value), executableSql: computed(() => "UPDATE users SET active = 1 WHERE id = 7"), activeOutputView, }); const pendingExecution = execution.tryExecute(); await vi.waitFor(() => { expect(productionSafetyStore.pending?.sql).toContain("UPDATE users"); }); expect(executeCurrentSql).not.toHaveBeenCalled(); productionSafetyStore.confirm(); await pendingExecution; expect(executeCurrentSql).toHaveBeenCalledTimes(1); }); });