fix(explain): preserve backend error details

This commit is contained in:
t8y2 committed 2026-07-18 00:11:21 +08:00
1 parent c40683431b
commit fa64952c08
5 files changed
+160 -14

No files matched your search

@@ -0,0 +1,112 @@
import { createPinia, setActivePinia } from "pinia";
import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
buildExplainSql: vi.fn(),
getExplainInfo: vi.fn(),
parseOracleExplainText: vi.fn(),
saveOpenTabsState: vi.fn(),
getConfig: vi.fn(),
}));
vi.mock("@/lib/diagram/explainPlan", () => ({
buildExplainSql: mocks.buildExplainSql,
parseExplainResult: vi.fn(),
parseDamengExplainText: vi.fn(),
parseOracleExplainText: mocks.parseOracleExplainText,
}));
vi.mock("@/lib/backend/api", () => ({
getExplainInfo: mocks.getExplainInfo,
saveOpenTabsState: mocks.saveOpenTabsState,
}));
vi.mock("@/stores/connectionStore", () => ({
useConnectionStore: () => ({
getConfig: mocks.getConfig,
recordConnectionLostError: vi.fn(),
}),
}));
vi.mock("@/stores/settingsStore", () => ({
useSettingsStore: () => ({
editorSettings: { pageSize: 100, openTabsRestoreMode: "all", confirmUnsavedSqlClose: false },
}),
}));
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)),
});
}
describe("queryStore Oracle explain errors", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.unstubAllGlobals();
installLocalStorage();
setActivePinia(createPinia());
mocks.getConfig.mockReturnValue({ id: "oracle-1", name: "Oracle", db_type: "oracle" });
mocks.buildExplainSql.mockResolvedValue({ ok: true, sql: "EXPLAIN PLAN FOR SELECT * FROM DUAL" });
mocks.saveOpenTabsState.mockResolvedValue(undefined);
});
it("shows the real ORA error returned by the backend", async () => {
mocks.getExplainInfo.mockRejectedValue(new Error("ORA-01031: insufficient privileges"));
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("oracle-1", "ORCL", "Query", "query", "APP");
await store.explainTabSql(tabId, "SELECT * FROM DUAL", "oracle");
expect(store.tabs.find((tab) => tab.id === tabId)).toMatchObject({
isExplaining: false,
explainPlan: undefined,
explainError: "ORA-01031: insufficient privileges",
});
});
it("shows string Agent errors without replacing them", async () => {
mocks.getExplainInfo.mockRejectedValue("Agent error: DBMS_XPLAN execution failed");
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("oracle-1", "ORCL", "Query");
await store.explainTabSql(tabId, "SELECT * FROM DUAL", "oracle");
expect(store.tabs.find((tab) => tab.id === tabId)?.explainError).toBe("Agent error: DBMS_XPLAN execution failed");
});
it("keeps the empty-plan message for a successful empty response", async () => {
mocks.getExplainInfo.mockResolvedValue("");
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("oracle-1", "ORCL", "Query");
await store.explainTabSql(tabId, "SELECT * FROM DUAL", "oracle");
expect(store.tabs.find((tab) => tab.id === tabId)?.explainError).toBe("No explain plan returned");
expect(mocks.parseOracleExplainText).not.toHaveBeenCalled();
});
it("keeps successful Oracle explain behavior unchanged", async () => {
const parsedPlan = { databaseType: "oracle", rawText: "Plan hash value: 123", nodes: [] };
mocks.getExplainInfo.mockResolvedValue("Plan hash value: 123");
mocks.parseOracleExplainText.mockReturnValue(parsedPlan);
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("oracle-1", "ORCL", "Query");
await store.explainTabSql(tabId, "SELECT * FROM DUAL", "oracle");
expect(mocks.parseOracleExplainText).toHaveBeenCalledWith("Plan hash value: 123");
expect(store.tabs.find((tab) => tab.id === tabId)).toMatchObject({
explainPlan: parsedPlan,
explainSql: "EXPLAIN PLAN FOR SELECT * FROM DUAL",
explainError: undefined,
});
});
});
+3 -1
View File
@@ -57,6 +57,7 @@ import { useSettingsStore } from "@/stores/settingsStore";
import { useSavedSqlStore } from "@/stores/savedSqlStore";
import { recordQueryCancellationLatency, resourceLifecycleDiagnostics } from "@/lib/diagnostics/resourceLifecycleDiagnostics";
import { appendDebugLog } from "@/lib/backend/debugLog";
import { formatError } from "@/lib/backend/errorUtils";
import { createSavedSqlEditorPosition, initSavedSqlEditorPositions, restoreSavedSqlEditorPosition, saveSavedSqlEditorPosition } from "@/lib/app/savedSqlEditorPosition";
import { ensureSqlExtension } from "@/lib/savedSql/savedSqlFileName";
import { safeLocalStorageGet, safeLocalStorageRemove } from "@/lib/backend/safeStorage";
@@ -3598,7 +3599,8 @@ export const useQueryStore = defineStore("query", () => {
const current = tabs.value.find((t) => t.id === id);
if (current?.explainExecutionId === executionId) {
current.explainPlan = undefined;
current.explainError = String(e?.message || e);
// Backend rejections contain the real ORA/Agent diagnostic; only successful empty responses use the generic empty-plan message.
current.explainError = formatError(e);
}
} finally {
const current = tabs.value.find((t) => t.id === id);