fix(query): surface PostgreSQL batch errors
This commit is contained in:
1 parent
31c7fcc16f
commit
49fb9e13cb
6 files changed
+87
-7
No files matched your search
@@ -243,6 +243,37 @@ describe("useSqlExecution", () => {
|
||||
expect(refreshObjects).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("records a later PostgreSQL batch error and skips metadata refresh", async () => {
|
||||
const activeTab = ref<QueryTab | undefined>(queryTab("app"));
|
||||
const activeConnection = ref<ConnectionConfig | undefined>(connection("postgres"));
|
||||
const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result");
|
||||
const queryStore = useQueryStore();
|
||||
const historyStore = useHistoryStore();
|
||||
const connectionStore = useConnectionStore();
|
||||
vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => {
|
||||
const tab = activeTab.value;
|
||||
if (!tab) return;
|
||||
const successfulResult = { columns: ["value"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 };
|
||||
tab.result = successfulResult;
|
||||
tab.results = [successfulResult, { columns: ["Error"], execution_error: true, rows: [["relation missing_table does not exist"]], affected_rows: 0, execution_time_ms: 1 }];
|
||||
tab.activeResultIndex = 0;
|
||||
});
|
||||
const addHistory = vi.spyOn(historyStore, "add").mockResolvedValue(undefined);
|
||||
const refreshObjects = vi.spyOn(connectionStore, "refreshObjectListTreeNode").mockResolvedValue(undefined);
|
||||
|
||||
const execution = useSqlExecution({
|
||||
activeTab: computed(() => activeTab.value),
|
||||
activeConnection: computed(() => activeConnection.value),
|
||||
executableSql: computed(() => "BEGIN; SELECT 1 AS value; SELECT * FROM missing_table"),
|
||||
activeOutputView,
|
||||
});
|
||||
|
||||
await execution.tryExecute();
|
||||
|
||||
expect(addHistory).toHaveBeenCalledWith(expect.objectContaining({ success: false, error: "relation missing_table does not exist", affected_rows: undefined }));
|
||||
expect(refreshObjects).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not treat an unmarked MySQL Error alias as a batch failure", async () => {
|
||||
const activeTab = ref<QueryTab | undefined>(queryTab("app"));
|
||||
const activeConnection = ref<ConnectionConfig | undefined>(connection("mysql"));
|
||||
|
||||
@@ -9,7 +9,7 @@ import { isSingleDatabase, usesTreeSchemaMode } from "@/lib/database/databaseCap
|
||||
import { supportsConnectionLevelSqlExecution } from "@/lib/connection/connectionLevelDatabaseBootstrap";
|
||||
import { classifySqlActivityKind } from "@/lib/history/historyActivityKind";
|
||||
import { sqlMetadataRefreshTarget } from "@/lib/sql/sqlMetadataRefresh";
|
||||
import { isMysqlExecutionErrorResult, usesMysqlProtocolDatabaseType } from "@/lib/query/queryResultError";
|
||||
import { isQueryExecutionErrorResult, usesMysqlProtocolDatabaseType } from "@/lib/query/queryResultError";
|
||||
import { classifyRedisCommandSafety, firstRedisCommandToken } from "@/lib/redis/redisCommandSafety";
|
||||
import { isSqlExecutionSnapshot, resolveExecutableSql, type SqlExecutionOverride, type SqlExecutionSnapshot } from "@/lib/sql/sqlExecutionTarget";
|
||||
import { isElasticsearchRestRequestText, parseElasticsearchRestRequestTarget, splitSqlStatementRanges } from "@/lib/sql/sqlStatementRanges";
|
||||
@@ -61,12 +61,11 @@ function primarySqlOperation(sql: string): string {
|
||||
|
||||
function firstQueryExecutionError(tab: Pick<QueryTab, "result" | "results">, databaseType: DatabaseType | undefined) {
|
||||
const activeResult = tab.result;
|
||||
if (activeResult && isMysqlExecutionErrorResult(activeResult, databaseType)) return activeResult;
|
||||
if (activeResult && isQueryExecutionErrorResult(activeResult)) return activeResult;
|
||||
if (!usesMysqlProtocolDatabaseType(databaseType) && activeResult?.columns.includes("Error")) return activeResult;
|
||||
if (!usesMysqlProtocolDatabaseType(databaseType)) return undefined;
|
||||
|
||||
const results = tab.results?.length ? tab.results : tab.result ? [tab.result] : [];
|
||||
return results.find((result) => isMysqlExecutionErrorResult(result, databaseType));
|
||||
return results.find((result) => isQueryExecutionErrorResult(result));
|
||||
}
|
||||
|
||||
export function useSqlExecution(deps: {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { QueryResult } from "@/types/database";
|
||||
|
||||
import { isMysqlExecutionErrorResult, isNoSnapshotErrorResult } from "@/lib/query/queryResultError";
|
||||
import { isMysqlExecutionErrorResult, isNoSnapshotErrorResult, isQueryExecutionErrorResult } from "@/lib/query/queryResultError";
|
||||
|
||||
function errorResult(message: string): QueryResult {
|
||||
return { columns: ["Error"], rows: [[message]], affected_rows: 0, execution_time_ms: 0 };
|
||||
@@ -59,3 +59,17 @@ describe("isMysqlExecutionErrorResult", () => {
|
||||
expect(isMysqlExecutionErrorResult({ ...errorResult("Duplicate entry '1'"), execution_error: true }, "jdbc")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isQueryExecutionErrorResult", () => {
|
||||
it("recognizes explicit execution errors for PostgreSQL", () => {
|
||||
expect(isQueryExecutionErrorResult({ ...errorResult("relation does not exist"), execution_error: true })).toBe(true);
|
||||
});
|
||||
|
||||
it("requires an explicit marker for PostgreSQL result groups", () => {
|
||||
expect(isQueryExecutionErrorResult(errorResult("relation does not exist"))).toBe(false);
|
||||
});
|
||||
|
||||
it("does not treat a successful MySQL Error alias as a failure", () => {
|
||||
expect(isQueryExecutionErrorResult({ ...dataResult(["Error"]), rows: [["2"]] })).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -18,6 +18,12 @@ export function isMysqlExecutionErrorResult(result: QueryResult, databaseType: D
|
||||
return usesMysqlProtocolDatabaseType(databaseType) && result.execution_error === true;
|
||||
}
|
||||
|
||||
export function isQueryExecutionErrorResult(result: QueryResult): boolean {
|
||||
// Multi-database batch responses use this explicit marker so a successful
|
||||
// query column named Error is never promoted as the failed statement.
|
||||
return result.execution_error === true;
|
||||
}
|
||||
|
||||
export function isNoSnapshotErrorResult(result: QueryResult | undefined | null): boolean {
|
||||
if (!result || !result.columns.includes("Error") || result.rows.length === 0) return false;
|
||||
return NO_SNAPSHOT_ERROR_PATTERN.test(String(result.rows[0]?.[0] ?? ""));
|
||||
|
||||
@@ -83,6 +83,36 @@ describe("queryStore multi-statement errors", () => {
|
||||
expect(tab.result?.columns).toEqual(["Error"]);
|
||||
});
|
||||
|
||||
it("opens a later PostgreSQL error result from a mixed result batch", async () => {
|
||||
mocks.getConnectionConfig.mockReturnValue({
|
||||
id: "postgres-1",
|
||||
name: "PostgreSQL",
|
||||
db_type: "postgres",
|
||||
database: "app",
|
||||
query_timeout_secs: 30,
|
||||
});
|
||||
mocks.executeMulti.mockResolvedValue([
|
||||
{ columns: [], rows: [], affected_rows: 0, execution_time_ms: 1, statement_index: 0 },
|
||||
{ columns: ["value"], rows: [[1]], affected_rows: 0, execution_time_ms: 1, statement_index: 1 },
|
||||
{ columns: ["Error"], execution_error: true, rows: [["relation missing_table does not exist"]], affected_rows: 0, execution_time_ms: 1, statement_index: 2 },
|
||||
{ columns: ["after_error"], rows: [[2]], affected_rows: 0, execution_time_ms: 1, statement_index: 3 },
|
||||
]);
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("postgres-1", "app", "Query");
|
||||
|
||||
await store.executeTabSql(tabId, "BEGIN; SELECT 1 AS value; SELECT * FROM missing_table");
|
||||
|
||||
const tab = store.tabs.find((item) => item.id === tabId)!;
|
||||
expect(tab.activeResultIndex).toBe(2);
|
||||
expect(tab.result).toMatchObject({
|
||||
columns: ["Error"],
|
||||
execution_error: true,
|
||||
rows: [["relation missing_table does not exist"]],
|
||||
});
|
||||
expect(tab.results).toHaveLength(4);
|
||||
});
|
||||
|
||||
it("invalidates only the executing Oracle tab after successful CURRENT_SCHEMA changes", async () => {
|
||||
mocks.getConnectionConfig.mockReturnValue({
|
||||
id: "oracle-1",
|
||||
|
||||
@@ -49,7 +49,7 @@ import { clearDataGridPendingSnapshotsForTab } from "@/composables/useDataGridEd
|
||||
import { buildTabResultSnapshot, deleteTabResultSnapshot, pruneTabResultSnapshots, readTabResultSnapshot, tabResultCacheKey, writeTabResultSnapshot } from "@/lib/tabs/tabResultCache";
|
||||
import { estimateQueryResultsBytes, selectInactiveResultEvictions } from "@/lib/tabs/queryResultSize";
|
||||
import { queryResultBaseSql, queryResultExecutionSql } from "@/lib/tabs/tabPresentation";
|
||||
import { isMysqlExecutionErrorResult } from "@/lib/query/queryResultError";
|
||||
import { isQueryExecutionErrorResult } from "@/lib/query/queryResultError";
|
||||
import { decodeQueryResultArchive, encodeQueryResultArchive, type DecodedQueryResultArchive } from "@/lib/query/queryResultArchive";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
@@ -3472,7 +3472,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
current.results[activeGroupIndex] = results[0];
|
||||
current.result = results[0];
|
||||
} else if (results.length > 1) {
|
||||
const errorResultIndex = results.findIndex((result) => isMysqlExecutionErrorResult(result, conn?.db_type));
|
||||
const errorResultIndex = results.findIndex((result) => isQueryExecutionErrorResult(result));
|
||||
const activeResultIndex = results.findIndex((result) => result.columns.length > 0);
|
||||
const resultIndex = errorResultIndex >= 0 ? errorResultIndex : (preservedResultIndex(results, current.activeResultIndex, options?.preserveActiveResultIndex) ?? (activeResultIndex >= 0 ? activeResultIndex : 0));
|
||||
current.results = results;
|
||||
|
||||
Reference in new issue
Block a user