fix(query): show SQL Server notices outside result-set tabs

Closes #9166
This commit is contained in:
t8y2 committed 2026-09-16 00:47:21 +08:00
1 parent 8373f153a3
commit dffd3025a7
11 files changed
+188 -12

No files matched your search

@@ -0,0 +1,34 @@
import { describe, expect, it } from "vitest";
import { queryResultMessages } from "@/lib/query/queryResultMessages";
import { defaultViewForResult } from "@/lib/query/queryResultDefaultView";
import type { QueryResult } from "@/types/database";
const messageResult: QueryResult = { columns: ["Message"], rows: [["before"], [""]], affected_rows: 0, execution_time_ms: 1, server_message: true };
describe("SQL Server result messages", () => {
it("collects tagged messages around data and errors in execution order", () => {
const data: QueryResult = { columns: ["Message"], rows: [["not a notice"]], affected_rows: 0, execution_time_ms: 1 };
const error: QueryResult = { ...data, columns: ["Error"], rows: [["failed"]], execution_error: true };
const results = [messageResult, data, { ...messageResult, rows: [["between"]] }, error, { ...messageResult, rows: [["after"]] }];
expect(results.flatMap(queryResultMessages).map(({ message }) => message)).toEqual(["before", "", "between", "after"]);
expect(queryResultMessages(data)).toEqual([]);
expect(queryResultMessages(error)).toEqual([]);
expect(messageResult.rows).toEqual([["before"], [""]]);
});
it("preserves structured metadata without duplicating message rows", () => {
const messages = [{ severity: "WARNING", message: "warning", code: "01000", detail: "detail" }];
expect(queryResultMessages({ ...messageResult, messages })).toBe(messages);
expect(queryResultMessages({ rows: [], messages })).toBe(messages);
expect(queryResultMessages({ ...messageResult, rows: [[null], []] })).toEqual([]);
});
it("opens message-only output and preserves other databases' default views", () => {
expect(defaultViewForResult(messageResult)).toBe("messages");
expect(defaultViewForResult({ ...messageResult, rows: [] })).toBe("summary");
expect(defaultViewForResult({ columns: [], rows: [], affected_rows: 0, messages: [{ severity: "NOTICE", message: "postgres" }] })).toBe("messages");
expect(defaultViewForResult({ columns: [], rows: [], affected_rows: 1, messages: [{ severity: "INFO", message: "Records: 1" }] })).toBe("summary");
expect(defaultViewForResult({ columns: [], rows: [], affected_rows: 0 })).toBe("summary");
});
});
+16 -1
View File
@@ -21,7 +21,7 @@ import {
dirtyTabTitleStyle,
} from "@/lib/tabs/tabPresentation";
import { sqlTextFingerprint } from "@/lib/sql/sqlTextFingerprint";
import type { ConnectionConfig, QueryTab } from "@/types/database";
import type { ConnectionConfig, QueryResult, QueryTab } from "@/types/database";
const translations: Record<string, string> = {
"tabs.tooltipConnection": "Connection:",
@@ -97,6 +97,21 @@ describe("query result SQL selection", () => {
});
describe("query result labels", () => {
it("excludes tagged server messages without renumbering storage indexes", () => {
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 empty: QueryResult = { ...data, rows: [] };
const results = [message, data, message, empty, data];
expect(tabularResultItems(results).map(({ index, n }) => ({ index, n }))).toEqual([
{ index: 1, n: 1 },
{ index: 3, n: 2 },
{ index: 4, n: 3 },
]);
expect(tabularResultItems([message])).toEqual([]);
expect(results).toHaveLength(5);
});
it("preserves both ends when shortening long source labels", () => {
expect(middleEllipsis("easy_manager_tool.tool_monitor_data_index_item")).toBe("easy_manage...index_item");
expect(middleEllipsis("aaa.apis")).toBe("aaa.apis");
@@ -1,4 +1,5 @@
import type { QueryResult } from "@/types/database";
import { queryResultMessages } from "./queryResultMessages";
/**
* Picks the default output view for a result that has no result set.
@@ -6,7 +7,8 @@ import type { QueryResult } from "@/types/database";
* messages view; routine DML like a MySQL INSERT also carries an INFO message
* ("Records: N ...") but keeps the established summary view.
*/
export function defaultViewForResult(result: Pick<QueryResult, "columns" | "rows" | "affected_rows" | "messages">): "messages" | "summary" {
export function defaultViewForResult(result: Pick<QueryResult, "columns" | "rows" | "affected_rows" | "messages" | "server_message">): "messages" | "summary" {
if (result.server_message === true && queryResultMessages(result).length > 0) return "messages";
const messageOnly = result.columns.length === 0 && result.rows.length === 0 && result.affected_rows === 0 && (result.messages?.length ?? 0) > 0;
return messageOnly ? "messages" : "summary";
}
@@ -0,0 +1,6 @@
import type { QueryMessage, QueryResult } from "@/types/database";
export function queryResultMessages(result: Pick<QueryResult, "rows" | "messages" | "server_message">): QueryMessage[] {
if (result.messages?.length || result.server_message !== true) return result.messages ?? [];
return result.rows.flatMap((row) => (row[0] == null ? [] : [{ severity: "INFO", message: String(row[0]) }]));
}
+1 -1
View File
@@ -362,7 +362,7 @@ export function tabularResultItems(results: QueryResult[] | undefined): { result
if (!results) return [];
return results
.map((result, index) => ({ result, index }))
.filter((item) => item.result.columns.length > 0)
.filter((item) => item.result.columns.length > 0 && item.result.server_message !== true)
.map((item, ordinal) => {
const label = queryResultStatementLabel(item.result);
const displayLabel = label ? middleEllipsis(label) : undefined;