fix(query): show SQL Server notices outside result-set tabs
Closes #9166
This commit is contained in:
11 files changed
+188
-12
No files matched your search
@@ -4,6 +4,7 @@ import { safeLocalStorageGet, safeLocalStorageSet } from "@/lib/backend/safeStor
|
||||
import { appendDebugLog, isDebugLoggingEnabled } from "@/lib/backend/debugLog";
|
||||
import { canReloadUnavailableDataTab, restoredDataTabReloadFilters } from "@/lib/table/tableDataRefresh";
|
||||
import { defaultViewForResult } from "@/lib/query/queryResultDefaultView";
|
||||
import { queryResultMessages } from "@/lib/query/queryResultMessages";
|
||||
import { isQueryExecutionErrorResult } from "@/lib/query/queryResultError";
|
||||
import { hasQueryOutput as tabHasQueryOutput } from "@/lib/query/queryOutput";
|
||||
import { batchSqlRecoveryState, type BatchSqlRecoveryAction } from "@/lib/query/batchSqlRecovery";
|
||||
@@ -555,7 +556,7 @@ const batchExecutionPercent = computed(() => {
|
||||
return progress?.total ? Math.round((progress.completed / progress.total) * 100) : 0;
|
||||
});
|
||||
const hasTabularResult = computed(() => {
|
||||
if (props.activeTab.result?.columns.length) return true;
|
||||
if (props.activeTab.result?.columns.length && props.activeTab.result.server_message !== true) return true;
|
||||
return visibleResultItems.value.length > 0;
|
||||
});
|
||||
const canShowResultOutput = computed(() => hasTabularResult.value || props.activeTab.isExecuting);
|
||||
@@ -567,7 +568,7 @@ const canShowExplainOutput = computed(() => !!props.activeTab.explainPlan || !!p
|
||||
// silently loses its notices once a later statement owns the active result.
|
||||
const resultMessages = computed<QueryMessage[]>(() => {
|
||||
const results = props.activeTab.results?.length ? props.activeTab.results : props.activeTab.result ? [props.activeTab.result] : [];
|
||||
return results.flatMap((result) => result.messages ?? []);
|
||||
return results.flatMap(queryResultMessages);
|
||||
});
|
||||
const resultMessageCount = computed(() => resultMessages.value.length);
|
||||
const canShowMessagesOutput = computed(() => resultMessageCount.value > 0);
|
||||
@@ -783,7 +784,7 @@ watch(
|
||||
// view when its own tab finishes executing.
|
||||
if (props.editorOnly) return;
|
||||
if (props.activeTab.isExecuting) return;
|
||||
if (hasExecutionSummary.value && !hasTabularResult.value && props.activeOutputView === "result") {
|
||||
if (hasExecutionSummary.value && (!hasTabularResult.value || props.activeTab.result?.server_message === true) && props.activeOutputView === "result") {
|
||||
const result = props.activeTab.result;
|
||||
emit("update:activeOutputView", props.activeTab.id, result ? defaultViewForResult(result) : "summary");
|
||||
}
|
||||
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { createApp, defineComponent, h, nextTick, reactive } from "vue";
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { createI18n } from "vue-i18n";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { QueryResult, QueryTab } from "@/types/database";
|
||||
|
||||
vi.mock("@/components/editor/QueryEditor.vue", () => ({ default: { render: () => null } }));
|
||||
vi.mock("@/components/grid/DataGrid.vue", () => ({ default: { render: () => h("div", { "data-test": "data-grid" }) } }));
|
||||
|
||||
import ContentArea from "../ContentArea.vue";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
|
||||
const cleanups: Array<() => void> = [];
|
||||
afterEach(() => {
|
||||
cleanups.splice(0).forEach((cleanup) => cleanup());
|
||||
document.body.replaceChildren();
|
||||
window.localStorage?.clear();
|
||||
});
|
||||
|
||||
async function mountResults(results: QueryResult[], activeIndex: number) {
|
||||
const pinia = createPinia();
|
||||
setActivePinia(pinia);
|
||||
const connection = { id: "sqlserver", name: "SQL Server", db_type: "sqlserver" as const, host: "localhost", port: 1433, username: "", password: "" };
|
||||
useConnectionStore().connections = [connection];
|
||||
const tab: QueryTab = { id: "query", title: "Query", connectionId: connection.id, database: "app", mode: "query", sql: "EXEC demo", isExecuting: false, results, result: results[activeIndex], activeResultIndex: activeIndex };
|
||||
useQueryStore().tabs.push(tab);
|
||||
const state = reactive({ view: "result" as "result" | "messages" });
|
||||
const host = document.createElement("div");
|
||||
document.body.appendChild(host);
|
||||
const app = createApp(
|
||||
defineComponent({
|
||||
setup: () => () =>
|
||||
h(ContentArea, {
|
||||
activeTab: tab,
|
||||
activeConnection: connection,
|
||||
activeOutputView: state.view,
|
||||
executableSql: "",
|
||||
formatSqlRequest: null,
|
||||
compressSqlRequest: null,
|
||||
selectedSql: "",
|
||||
cursorPos: 0,
|
||||
resultOnly: true,
|
||||
blockDangerousRedisCommands: false,
|
||||
"onUpdate:activeOutputView": (_tabId: string, view: string) => {
|
||||
state.view = view as typeof state.view;
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
app.use(pinia);
|
||||
app.use(createI18n({ legacy: false, locale: "en", messages: { en: { tabs: { resultN: "Result {n}", allResults: "All results ({count})" } } }, missingWarn: false, fallbackWarn: false }));
|
||||
app.mount(host);
|
||||
cleanups.push(() => app.unmount());
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
return { host, state };
|
||||
}
|
||||
|
||||
describe("SQL Server messages in the result surface", () => {
|
||||
const message: QueryResult = { columns: ["Message"], rows: [["before notice"]], affected_rows: 0, execution_time_ms: 1, server_message: true };
|
||||
const data: QueryResult = { columns: ["Message"], rows: [["real data"]], affected_rows: 0, execution_time_ms: 1 };
|
||||
|
||||
it("shows only three result tabs and preserves both message blocks in Messages", async () => {
|
||||
const { host, state } = await mountResults([message, data, { ...message, rows: [["after notice"]] }, { ...data, rows: [] }, data], 1);
|
||||
expect([...host.querySelectorAll(".result-set-scroll button")].map((button) => button.textContent?.trim())).toEqual(["Result 1", "Result 2", "Result 3"]);
|
||||
state.view = "messages";
|
||||
await nextTick();
|
||||
expect(host.textContent).toContain("before notice");
|
||||
expect(host.textContent).toContain("after notice");
|
||||
expect(host.textContent).not.toContain("real data");
|
||||
});
|
||||
|
||||
it.each([false, true])("redirects restored message output without a fake grid (mixed=%s)", async (mixed) => {
|
||||
const { host, state } = await mountResults(mixed ? [message, data] : [message], 0);
|
||||
expect(state.view).toBe("messages");
|
||||
expect(host.textContent).toContain("before notice");
|
||||
expect(host.querySelector('[data-test="data-grid"]')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -12,9 +12,9 @@ afterEach(() => {
|
||||
document.body.replaceChildren();
|
||||
});
|
||||
|
||||
async function mountNavigator() {
|
||||
async function mountNavigator(inputResults?: QueryResult[]) {
|
||||
// Include a non-tabular result so ordinal and storage index are different.
|
||||
const results = [{ columns: [], rows: [] }, ...Array.from({ length: 100 }, (_, i) => ({ columns: ["value"], rows: [[i + 1]], sourceStatement: `SELECT ${i + 1} AS value` }))] as QueryResult[];
|
||||
const results = inputResults ?? ([{ columns: [], rows: [] }, ...Array.from({ length: 100 }, (_, i) => ({ columns: ["value"], rows: [[i + 1]], sourceStatement: `SELECT ${i + 1} AS value` }))] as QueryResult[]);
|
||||
const select = vi.fn();
|
||||
const container = document.createElement("div");
|
||||
document.body.append(container);
|
||||
@@ -26,6 +26,18 @@ async function mountNavigator() {
|
||||
}
|
||||
|
||||
describe("large result-set navigation", () => {
|
||||
it("renders three data sets rather than five tabs when messages are interleaved", async () => {
|
||||
const message: QueryResult = { columns: ["Message"], rows: [["notice"]], affected_rows: 0, execution_time_ms: 1, server_message: true };
|
||||
const data: QueryResult = { columns: ["Message"], rows: [["real data"]], affected_rows: 0, execution_time_ms: 1 };
|
||||
const { container, select } = await mountNavigator([message, data, message, { ...data, rows: [] }, data]);
|
||||
const buttons = [...container.querySelectorAll<HTMLButtonElement>(".result-set-scroll button")];
|
||||
|
||||
expect(buttons.map((button) => button.textContent?.trim())).toEqual(["Result 1", "Result 2", "Result 3"]);
|
||||
expect(container.textContent).toContain("All results (3)");
|
||||
buttons[1]?.click();
|
||||
expect(select.mock.calls[0]?.[0]).toMatchObject({ index: 3, n: 2 });
|
||||
});
|
||||
|
||||
it("scrolls the result-set strip with a vertical wheel and consumes boundary gestures", async () => {
|
||||
const { container } = await mountNavigator();
|
||||
const strip = container.querySelector<HTMLElement>(".result-set-scroll")!;
|
||||
|
||||
Reference in new issue
Block a user