fix(query): preserve structured backend errors

This commit is contained in:
miracle authored and GitHub committed 2026-08-02 16:37:46 +08:00
1 parent 2e9dcdc26b
commit bcafc4a474
58 files changed
+4182 -554

No files matched your search

@@ -425,14 +425,14 @@ describe("useSqlExecution", () => {
expect(addHistory).toHaveBeenCalledWith(expect.objectContaining({ success: true, error: undefined }));
});
it("continues to record active non-MySQL errors as failures", async () => {
it("continues to record explicitly marked non-MySQL errors as failures", 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();
vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => {
if (activeTab.value) activeTab.value.result = { columns: ["Error"], rows: [["relation does not exist"]], affected_rows: 0, execution_time_ms: 1 };
if (activeTab.value) activeTab.value.result = { columns: ["Error"], rows: [["relation does not exist"]], affected_rows: 0, execution_time_ms: 1, execution_error: true };
});
const addHistory = vi.spyOn(historyStore, "add").mockResolvedValue(undefined);
@@ -448,6 +448,29 @@ describe("useSqlExecution", () => {
expect(addHistory).toHaveBeenCalledWith(expect.objectContaining({ success: false, error: "relation does not exist" }));
});
it("does not treat an unmarked PostgreSQL Error alias as a failure", 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();
vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => {
if (activeTab.value) activeTab.value.result = { columns: ["Error"], rows: [[2]], affected_rows: 0, execution_time_ms: 1 };
});
const addHistory = vi.spyOn(historyStore, "add").mockResolvedValue(undefined);
const execution = useSqlExecution({
activeTab: computed(() => activeTab.value),
activeConnection: computed(() => activeConnection.value),
executableSql: computed(() => "SELECT 2 AS Error"),
activeOutputView,
});
await execution.tryExecute();
expect(addHistory).toHaveBeenCalledWith(expect.objectContaining({ success: true, error: undefined }));
});
it("keeps the full dangerous script and new-result-tab intent through confirmation", async () => {
const activeTab = ref<QueryTab | undefined>(queryTab("app"));
const activeConnection = ref<ConnectionConfig | undefined>(connection("mysql"));
@@ -2,7 +2,7 @@ import * as api from "@/lib/backend/api";
import { connectionObjectTreeNodeSchema, effectiveDatabaseTypeForConnection, metadataSchemaForConnection } from "@/lib/database/jdbcDialect";
import { invalidateTableMetadataCache, loadTableMetadata } from "@/lib/metadata/tableMetadataCache";
import { canApplyDataTabMetadata } from "@/lib/sidebar/dataTabOpenPolicy";
import { isNoSnapshotErrorResult } from "@/lib/query/queryResultError";
import { isNoSnapshotErrorResult, isQueryExecutionErrorResult } from "@/lib/query/queryResultError";
import { buildTableSelectSql } from "@/lib/table/tableSelectSql";
import { editableRowIdentifierColumns, usesSyntheticRowIdKey } from "@/lib/table/tableEditing";
import { tableOpenPageLimit } from "@/lib/table/tableOpenPageLimit";
@@ -169,7 +169,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab
const tabAfterFirstExecute = queryStore.tabs.find((tab) => tab.id === tabId);
const firstResult = tabAfterFirstExecute?.result;
const cancelRequestedDuringExecute = (tabAfterFirstExecute?.cancelRequestCount ?? 0) > cancelCountBeforeExecute;
const firstQueryFailed = cancelRequestedDuringExecute || tabAfterFirstExecute?.isCancelling === true || (firstResult?.columns.length === 1 && firstResult.columns[0] === "Error");
const firstQueryFailed = cancelRequestedDuringExecute || tabAfterFirstExecute?.isCancelling === true || (firstResult !== undefined && isQueryExecutionErrorResult(firstResult));
// executeTabSql surfaces query failures as an "Error" result instead of throwing.
// A snapshot-less lake table fails the data preview above but its metadata still
// reads fine — retry with LIMIT 0 so the user sees the table structure (columns +
@@ -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 { isQueryExecutionErrorResult, usesMysqlProtocolDatabaseType } from "@/lib/query/queryResultError";
import { isQueryExecutionErrorResult } from "@/lib/query/queryResultError";
import { classifyRedisCommandSafety } from "@/lib/redis/redisCommandSafety";
import { isSqlExecutionSnapshot, resolveExecutableSql, type SqlExecutionOverride, type SqlExecutionSnapshot } from "@/lib/sql/sqlExecutionTarget";
import { isElasticsearchRestRequestText, parseElasticsearchRestRequestTarget, splitSqlStatementRanges } from "@/lib/sql/sqlStatementRanges";
@@ -63,10 +63,9 @@ function primarySqlOperation(sql: string): string {
return statement?.match(/^([a-z]+)/i)?.[1]?.toUpperCase() || "SQL";
}
function firstQueryExecutionError(tab: Pick<QueryTab, "result" | "results">, databaseType: DatabaseType | undefined) {
function firstQueryExecutionError(tab: Pick<QueryTab, "result" | "results">) {
const activeResult = tab.result;
if (activeResult && isQueryExecutionErrorResult(activeResult)) return activeResult;
if (!usesMysqlProtocolDatabaseType(databaseType) && activeResult?.columns.includes("Error")) return activeResult;
const results = tab.results?.length ? tab.results : tab.result ? [tab.result] : [];
return results.find((result) => isQueryExecutionErrorResult(result));
@@ -233,7 +232,7 @@ export function useSqlExecution(deps: {
deps.activeOutputView.value = "summary";
}
const elapsed = Date.now() - start;
const failure = firstQueryExecutionError(tab, executionDatabaseType);
const failure = firstQueryExecutionError(tab);
const success = !failure;
historyStore.add({
connection_id: tab.connectionId,