Files
dbx/apps/desktop/src/composables/__tests__/useSqlExecutionMultiDbFocus.spec.ts
T

292 lines
11 KiB
TypeScript
Vendored

/**
* Issue #6189 — SQL Server "focus jumps to the message result".
*
* Background:
* - `crates/dbx-core/src/db/sqlserver.rs:523-542` synthesizes a pseudo result with a
* single "Message" column and `server_message: true` for any batch segment that
* produced only server messages (PRINT / "DBCC execution completed" / ...).
* - `stores/queryStore.ts:4727` then picks the first result that HAS COLUMNS as the
* active one. The synthesized message grid has a column, so a message result can win.
* - `composables/useSqlExecution.ts` (`focusSqlServerDataResult`, shared by both
* `doExecute` and `executeTargetSql`) corrects that: when the selected result is a
* `server_message` one it re-focuses the first real data result. Before this fix,
* `executeTargetSql` (the multi-database execute path) had no such correction.
*/
import { computed, ref } from "vue";
import { createPinia, setActivePinia } from "pinia";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { useSqlExecution } from "../useSqlExecution";
import { useHistoryStore } from "@/stores/historyStore";
import { useQueryStore } from "@/stores/queryStore";
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),
}));
function installLocalStorage() {
const data = new Map<string, string>();
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 sqlServerConnection(): ConnectionConfig {
return {
id: "conn-1",
name: "SQLServer",
db_type: "sqlserver",
host: "localhost",
port: 1433,
username: "sa",
password: "",
};
}
function queryTab(database = "dbx_sqlserver_demo"): QueryTab {
return {
id: "tab-1",
connectionId: "conn-1",
database,
schema: undefined,
title: "SQL",
sql: "",
mode: "query",
isDirty: false,
isExecuting: false,
isCancelling: false,
isExplaining: false,
};
}
/**
* What the SQL Server driver really returns for `PRINT N'x'; SELECT 1 AS value;`:
* the PRINT becomes a synthesized single-column "Message" grid flagged
* `server_message: true`, followed by the real SELECT result.
*/
function sqlServerMessageFirstResults() {
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,
};
return { messageResult, dataResult };
}
describe("SQL Server result focus: doExecute vs executeTargetSql", () => {
beforeEach(() => {
installLocalStorage();
setActivePinia(createPinia());
});
// ---- control: the single-connection editor path (fixed by be3336c1e) ----
it("CONTROL doExecute re-focuses the real data result when a message result was selected", async () => {
const sql = "PRINT N'x'; SELECT 1 AS value;";
const tab = { ...queryTab(), sql };
const activeTab = ref<QueryTab | undefined>(tab);
const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result");
const queryStore = useQueryStore();
const { messageResult, dataResult } = sqlServerMessageFirstResults();
const setActiveResultIndex = vi.spyOn(queryStore, "setActiveResultIndex").mockImplementation((_id, index) => {
if (!tab.results) return;
tab.activeResultIndex = index;
tab.result = tab.results[index];
});
vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => {
tab.results = [messageResult, dataResult];
// queryStore.ts:4727 — first result WITH COLUMNS wins, and the message grid has one.
tab.activeResultIndex = 0;
tab.result = messageResult;
return true;
});
vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined);
const execution = useSqlExecution({
activeTab: computed(() => activeTab.value),
activeConnection: computed(() => sqlServerConnection()),
executableSql: computed(() => sql),
activeOutputView,
});
await execution.tryExecute();
expect(setActiveResultIndex).toHaveBeenCalledWith("tab-1", 1);
expect(tab.activeResultIndex).toBe(1);
expect(tab.result?.server_message).toBeUndefined();
expect(tab.result?.rows).toEqual([[1]]);
});
it("executeTargetSql focuses the real data result, like doExecute (fix for #6189)", async () => {
const sql = "PRINT N'x'; SELECT 1 AS value;";
const tab = { ...queryTab(), sql };
const connection = sqlServerConnection();
const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result");
const queryStore = useQueryStore();
const { messageResult, dataResult } = sqlServerMessageFirstResults();
vi.spyOn(queryStore, "setActiveResultIndex").mockImplementation((_id, index) => {
if (!tab.results) return;
tab.activeResultIndex = index;
tab.result = tab.results[index];
});
vi.spyOn(queryStore, "executeTabSql").mockImplementation(async () => {
tab.results = [messageResult, dataResult];
tab.activeResultIndex = 0;
tab.result = messageResult;
return true;
});
vi.spyOn(queryStore, "getExecutionTab").mockReturnValue(tab);
vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined);
const execution = useSqlExecution({
activeTab: computed(() => tab as QueryTab | undefined),
activeConnection: computed(() => connection),
executableSql: computed(() => sql),
activeOutputView,
});
await execution.executeTargetSql({ tab, connection, sql });
expect(tab.activeResultIndex).toBe(1);
expect(tab.result?.server_message).toBeUndefined();
expect(tab.result?.rows).toEqual([[1]]);
});
// ---- the "trailing message" shape actually described by #6189 / #5566 ----
it("executeTargetSql keeps the first data result when the message result is last", async () => {
const sql = "SELECT 1 AS a; SELECT 2 AS b; PRINT N'DBCC execution completed.';";
const tab = { ...queryTab(), sql };
const connection = sqlServerConnection();
const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result");
const queryStore = useQueryStore();
const first = { columns: ["a"], column_types: ["int"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 };
const second = { columns: ["b"], column_types: ["int"], rows: [[2]], affected_rows: 0, execution_time_ms: 1 };
const trailingMessage = {
columns: ["Message"],
column_types: ["nvarchar"],
rows: [["DBCC execution completed."]],
affected_rows: 0,
execution_time_ms: 1,
server_message: true,
} as const;
vi.spyOn(queryStore, "executeTabSql").mockImplementation(async () => {
tab.results = [first, second, trailingMessage];
tab.activeResultIndex = 0;
tab.result = first;
return true;
});
vi.spyOn(queryStore, "getExecutionTab").mockReturnValue(tab);
vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined);
const execution = useSqlExecution({
activeTab: computed(() => tab as QueryTab | undefined),
activeConnection: computed(() => connection),
executableSql: computed(() => sql),
activeOutputView,
});
await execution.executeTargetSql({ tab, connection, sql });
expect(tab.activeResultIndex).toBe(0);
expect(tab.result?.rows).toEqual([[1]]);
});
});
describe("preservedResultIndex staleness theory", () => {
beforeEach(() => {
installLocalStorage();
setActivePinia(createPinia());
});
it("doExecute never asks the store to preserve the previous active result index", async () => {
const sql = "SELECT 1 AS a; SELECT 2 AS b; PRINT N'trailing';";
const tab = { ...queryTab(), sql, results: undefined, activeResultIndex: 2 } as QueryTab;
const activeTab = ref<QueryTab | undefined>(tab);
const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result");
const queryStore = useQueryStore();
const executeCurrentSql = vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => {
tab.results = [
{ columns: ["a"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 },
{ columns: ["Message"], rows: [["trailing"]], affected_rows: 0, execution_time_ms: 1, server_message: true },
];
tab.activeResultIndex = 0;
tab.result = tab.results[0];
return true;
});
vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined);
const execution = useSqlExecution({
activeTab: computed(() => activeTab.value),
activeConnection: computed(() => sqlServerConnection()),
executableSql: computed(() => sql),
activeOutputView,
});
await execution.tryExecute();
const options = executeCurrentSql.mock.calls[0]?.[1] ?? {};
// queryStore.ts:238-241 only reuses the previous index when this flag is true,
// so without it a new execution can never inherit a stale "last card" index.
expect(options).not.toHaveProperty("preserveActiveResultIndex");
expect(tab.activeResultIndex).toBe(0);
});
it("executeTargetSql never asks the store to preserve the previous active result index", async () => {
const sql = "SELECT 1 AS a; PRINT N'trailing';";
const tab = { ...queryTab(), sql, activeResultIndex: 2 } as QueryTab;
const connection = sqlServerConnection();
const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result");
const queryStore = useQueryStore();
const executeTabSql = vi.spyOn(queryStore, "executeTabSql").mockImplementation(async () => {
tab.results = [
{ columns: ["a"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 },
{ columns: ["Message"], rows: [["trailing"]], affected_rows: 0, execution_time_ms: 1, server_message: true },
];
tab.activeResultIndex = 0;
tab.result = tab.results[0];
return true;
});
vi.spyOn(queryStore, "getExecutionTab").mockReturnValue(tab);
vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined);
const execution = useSqlExecution({
activeTab: computed(() => tab as QueryTab | undefined),
activeConnection: computed(() => connection),
executableSql: computed(() => sql),
activeOutputView,
});
await execution.executeTargetSql({ tab, connection, sql });
const options = executeTabSql.mock.calls[0]?.[2] ?? {};
expect(options).not.toHaveProperty("preserveActiveResultIndex");
expect(tab.activeResultIndex).toBe(0);
});
});