fix(query): preserve structured backend errors
This commit is contained in:
1 parent
2e9dcdc26b
commit
bcafc4a474
58 files changed
+4182
-554
No files matched your search
@@ -76,6 +76,17 @@ function deferred<T>() {
|
||||
return { promise, resolve, reject };
|
||||
}
|
||||
|
||||
function structuredTimeoutError() {
|
||||
return {
|
||||
version: 1 as const,
|
||||
code: "DBX-JDBC-2002",
|
||||
messageKey: "backendErrors.jdbc.operationTimedOut",
|
||||
messageParams: { stage: "execute" },
|
||||
source: "jdbcAgent" as const,
|
||||
operationOutcome: "unknown" as const,
|
||||
};
|
||||
}
|
||||
|
||||
describe("queryStore multi-statement errors", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -115,7 +126,7 @@ describe("queryStore multi-statement errors", () => {
|
||||
success,
|
||||
executionTimeMs: result.execution_time_ms,
|
||||
affectedRows: result.affected_rows,
|
||||
error: success ? undefined : String(result.rows[0]?.[0] ?? ""),
|
||||
error: success ? undefined : result.error,
|
||||
});
|
||||
});
|
||||
return results;
|
||||
@@ -123,9 +134,10 @@ describe("queryStore multi-statement errors", () => {
|
||||
});
|
||||
|
||||
it("opens the first error result from a mixed result batch", async () => {
|
||||
const structuredError = structuredTimeoutError();
|
||||
mocks.executeMulti.mockResolvedValue([
|
||||
{ columns: ["value"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 },
|
||||
{ columns: ["Error"], execution_error: true, rows: [["no such table: missing"]], affected_rows: 0, execution_time_ms: 1 },
|
||||
{ columns: ["Error"], execution_error: true, error: structuredError, rows: [["no such table: missing"]], affected_rows: 0, execution_time_ms: 1 },
|
||||
]);
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
@@ -136,6 +148,10 @@ describe("queryStore multi-statement errors", () => {
|
||||
const tab = store.tabs.find((item) => item.id === tabId)!;
|
||||
expect(tab.activeResultIndex).toBe(1);
|
||||
expect(tab.result?.columns).toEqual(["Error"]);
|
||||
expect(tab.result?.error).toEqual(structuredError);
|
||||
expect(tab.batchSqlExecution?.items[1]?.errorDetails).toEqual(structuredError);
|
||||
expect(tab.batchSqlExecution?.items[1]?.error).not.toBe(structuredError.code);
|
||||
expect(tab.batchSqlExecution?.items[1]?.error).not.toBe("[object Object]");
|
||||
});
|
||||
|
||||
it("updates live per-statement progress before the batch promise resolves", async () => {
|
||||
@@ -195,6 +211,24 @@ describe("queryStore multi-statement errors", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves a structured top-level batch failure", async () => {
|
||||
const structuredError = structuredTimeoutError();
|
||||
mocks.executeMultiWithProgress.mockRejectedValueOnce(structuredError);
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("mysql-1", "app", "Query", "query", undefined, "SELECT 1;\nSELECT 2");
|
||||
|
||||
await store.executeTabSql(tabId, "SELECT 1;\nSELECT 2");
|
||||
|
||||
const tab = store.tabs.find((item) => item.id === tabId)!;
|
||||
expect(tab.result?.error).toEqual(structuredError);
|
||||
expect(tab.result?.rows[0]?.[0]).toEqual(expect.any(String));
|
||||
expect(tab.result?.rows[0]?.[0]).not.toBe(structuredError.code);
|
||||
expect(tab.result?.rows[0]?.[0]).not.toBe("[object Object]");
|
||||
expect(tab.batchSqlExecution?.items[0]?.errorDetails).toEqual(structuredError);
|
||||
expect(tab.batchSqlExecution?.items[0]?.error).toBe(tab.result?.rows[0]?.[0]);
|
||||
});
|
||||
|
||||
it("keeps completed progress and records a later top-level batch failure", async () => {
|
||||
mocks.executeMultiWithProgress.mockImplementationOnce((_connectionId, _database, _sql, onProgress, _schema, options) => {
|
||||
onProgress({
|
||||
@@ -359,7 +393,7 @@ describe("queryStore multi-statement errors", () => {
|
||||
database: "FooDB",
|
||||
query_timeout_secs: 30,
|
||||
});
|
||||
mocks.executeMulti.mockResolvedValueOnce([{ columns: [], rows: [], affected_rows: 0, execution_time_ms: 1 }]).mockResolvedValueOnce([{ columns: ["Error"], rows: [["Database does not exist"]], affected_rows: 0, execution_time_ms: 1 }]);
|
||||
mocks.executeMulti.mockResolvedValueOnce([{ columns: [], rows: [], affected_rows: 0, execution_time_ms: 1 }]).mockResolvedValueOnce([{ columns: ["Error"], rows: [["Database does not exist"]], affected_rows: 0, execution_time_ms: 1, execution_error: true }]);
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabA = store.createTab("sqlserver-1", "FooDB", "Tab A", "query", "dbo");
|
||||
|
||||
@@ -61,7 +61,7 @@ import { useSavedSqlStore } from "@/stores/savedSqlStore";
|
||||
import { useExportTracker } from "@/composables/useExportTracker";
|
||||
import { recordQueryCancellationLatency, resourceLifecycleDiagnostics } from "@/lib/diagnostics/resourceLifecycleDiagnostics";
|
||||
import { appendDebugLog } from "@/lib/backend/debugLog";
|
||||
import { formatError } from "@/lib/backend/errorUtils";
|
||||
import { formatError, normalizeBackendError, type BackendError } from "@/lib/backend/errorUtils";
|
||||
import { createSavedSqlEditorPosition, initSavedSqlEditorPositions, restoreSavedSqlEditorPosition, saveSavedSqlEditorPosition } from "@/lib/app/savedSqlEditorPosition";
|
||||
import { ensureSqlExtension } from "@/lib/savedSql/savedSqlFileName";
|
||||
import { resolveSavedSqlExecutionTarget, savedSqlExecutionTargetFromTab, type SavedSqlExecutionTarget, type SavedSqlOpenTargetMode } from "@/lib/savedSql/savedSqlExecutionTarget";
|
||||
@@ -278,7 +278,7 @@ function applyBatchSqlProgress(
|
||||
success: boolean;
|
||||
executionTimeMs: number;
|
||||
affectedRows: number;
|
||||
error?: string;
|
||||
error?: BackendError;
|
||||
},
|
||||
continueOnError: boolean,
|
||||
) {
|
||||
@@ -289,7 +289,8 @@ function applyBatchSqlProgress(
|
||||
item.status = progress.success ? "success" : "error";
|
||||
item.executionTimeMs = progress.executionTimeMs;
|
||||
item.affectedRows = progress.affectedRows;
|
||||
item.error = progress.error;
|
||||
item.errorDetails = progress.error;
|
||||
item.error = progress.error ? translateBackendError(i18n.global.t, progress.error) : undefined;
|
||||
batch.completed = Math.max(batch.completed, progress.completed);
|
||||
if ((progress.success || continueOnError) && progress.completed < batch.total) {
|
||||
const next = batch.items[progress.statementIndex + 1];
|
||||
@@ -310,7 +311,8 @@ function reconcileBatchSqlResults(tab: QueryTab, executionId: string, results: Q
|
||||
item.status = failed ? "error" : "success";
|
||||
item.executionTimeMs = result.execution_time_ms;
|
||||
item.affectedRows = result.affected_rows;
|
||||
item.error = failed ? String(result.rows[0]?.[0] ?? "") : undefined;
|
||||
item.errorDetails = failed ? result.error : undefined;
|
||||
item.error = failed ? (result.error ? translateBackendError(i18n.global.t, result.error) : String(result.rows[0]?.[0] ?? "")) : undefined;
|
||||
}
|
||||
batch.completed = batch.items.filter((item) => item.status === "success" || item.status === "error").length;
|
||||
}
|
||||
@@ -321,7 +323,8 @@ function failBatchSqlExecution(tab: QueryTab, executionId: string, error: unknow
|
||||
const item = batch.items.find((candidate) => candidate.status === "running") ?? batch.items.find((candidate) => candidate.status === "pending");
|
||||
if (!item) return;
|
||||
item.status = cancelled ? "cancelled" : "error";
|
||||
item.error = cancelled ? undefined : error instanceof Error ? error.message : String(error);
|
||||
item.errorDetails = cancelled ? undefined : (normalizeBackendError(error) ?? undefined);
|
||||
item.error = cancelled ? undefined : translateBackendError(i18n.global.t, error);
|
||||
batch.completed = batch.items.filter((candidate) => candidate.status === "success" || candidate.status === "error").length;
|
||||
}
|
||||
|
||||
@@ -329,7 +332,7 @@ function finishBatchSqlExecution(tab: QueryTab, executionId: string, cancelled:
|
||||
const batch = batchSqlExecutionFor(tab, executionId);
|
||||
if (!batch) return;
|
||||
if (cancelled) {
|
||||
const cancelledError = [...batch.items].reverse().find((item) => item.status === "error" && /cancel|取消/i.test(item.error ?? ""));
|
||||
const cancelledError = [...batch.items].reverse().find((item) => item.status === "error" && (item.errorDetails?.code === "DBX-JDBC-2003" || /cancel|取消/i.test(item.error ?? "")));
|
||||
if (cancelledError) {
|
||||
cancelledError.status = "cancelled";
|
||||
cancelledError.error = undefined;
|
||||
@@ -384,9 +387,7 @@ function sqlServerUseDatabaseFromStatement(statement: string | undefined): strin
|
||||
}
|
||||
|
||||
function isSqlServerBatchErrorResult(result: QueryResult): boolean {
|
||||
// SQL Server batch errors can arrive without execution_error metadata.
|
||||
// A standalone USE statement cannot legitimately return an Error column.
|
||||
return result.execution_error === true || (result.columns.length === 1 && result.columns[0] === "Error" && result.rows.length > 0);
|
||||
return result.execution_error === true;
|
||||
}
|
||||
|
||||
function sapHanaCurrentSchemaFromResult(result: QueryResult): string | undefined {
|
||||
@@ -2717,13 +2718,14 @@ export const useQueryStore = defineStore("query", () => {
|
||||
}
|
||||
|
||||
function toErrorResult(e: any): NonNullable<QueryTab["result"]> {
|
||||
const raw = e instanceof Error ? e.message : String(e);
|
||||
// Single funnel for every query execution failure, so backend messages DBX
|
||||
// knows about are shown in the active locale rather than as raw English.
|
||||
const message = translateBackendError(i18n.global.t, raw);
|
||||
const error = normalizeBackendError(e) ?? undefined;
|
||||
const message = translateBackendError(i18n.global.t, e);
|
||||
return markQueryResultRowsRaw({
|
||||
columns: ["Error"],
|
||||
execution_error: true,
|
||||
error,
|
||||
rows: [[message]],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: 0,
|
||||
@@ -3422,7 +3424,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
connStore.invalidateCompletionCache(tab.connectionId, String(currentDb));
|
||||
}
|
||||
} catch (e: any) {
|
||||
allResults.push(annotateQueryResultSource({ columns: ["Error"], rows: [[e?.message ?? String(e)]], affected_rows: 0, execution_time_ms: 0 }, command, undefined, undefined, sourceRange));
|
||||
allResults.push(annotateQueryResultSource(toErrorResult(e), command, undefined, undefined, sourceRange));
|
||||
}
|
||||
}
|
||||
queryExecutionLog("info", "redis:done", { traceId, commandCount: commands.length, elapsed: elapsed() });
|
||||
@@ -3431,7 +3433,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
if (current?.executionId === executionId) {
|
||||
if (openInNewResultTab && current.isCancelling && restorePendingResultRun(current, executionId)) return false;
|
||||
if (allResults.length > 1) {
|
||||
const activeResultIndex = allResults.findIndex((r) => !r.columns.includes("Error"));
|
||||
const activeResultIndex = allResults.findIndex((result) => !isQueryExecutionErrorResult(result));
|
||||
const resultIndex = preservedResultIndex(allResults, current.activeResultIndex, options?.preserveActiveResultIndex) ?? (activeResultIndex >= 0 ? activeResultIndex : 0);
|
||||
current.results = allResults;
|
||||
current.activeResultIndex = resultIndex;
|
||||
@@ -3770,7 +3772,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
} else if (allResults.length > 1) {
|
||||
// Open grouped output on the first non-error result when possible so
|
||||
// mixed success/error batches land on the most useful table first.
|
||||
const activeResultIndex = allResults.findIndex((result) => !result.columns.includes("Error"));
|
||||
const activeResultIndex = allResults.findIndex((result) => !isQueryExecutionErrorResult(result));
|
||||
const resultIndex = preservedResultIndex(allResults, current.activeResultIndex, options?.preserveActiveResultIndex) ?? (activeResultIndex >= 0 ? activeResultIndex : 0);
|
||||
current.results = allResults;
|
||||
current.activeResultIndex = resultIndex;
|
||||
@@ -3839,7 +3841,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
if (current?.executionId === executionId && openInNewResultTab && current.isCancelling && restorePendingResultRun(current, executionId)) return false;
|
||||
if (current?.executionId === executionId && allResults.length > 0) {
|
||||
clearResultNavigationState(current);
|
||||
const errorResultIndex = allResults.findIndex((result) => result.columns.includes("Error") || elasticsearchHttpErrorStatus(result) !== undefined);
|
||||
const errorResultIndex = allResults.findIndex((result) => isQueryExecutionErrorResult(result) || elasticsearchHttpErrorStatus(result) !== undefined);
|
||||
const resultIndex = errorResultIndex >= 0 ? errorResultIndex : 0;
|
||||
current.results = allResults.length > 1 ? allResults : undefined;
|
||||
current.activeResultIndex = allResults.length > 1 ? resultIndex : undefined;
|
||||
|
||||
Reference in new issue
Block a user