From ba25a871fd6c6e0c735eb81d43b3794c08fb6e17 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Mon, 24 Aug 2026 12:55:43 +0000 Subject: [PATCH] fix(query): guard unsupported stale manual transaction state Closes #7045 --- .../database/databaseFeatureSupport.spec.ts | 2 + ...queryStore.manualTransactionExpiry.spec.ts | 106 ++++++++++++++++++ apps/desktop/src/stores/queryStore.ts | 5 +- 3 files changed, 112 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/lib/__tests__/database/databaseFeatureSupport.spec.ts b/apps/desktop/src/lib/__tests__/database/databaseFeatureSupport.spec.ts index 98b3a92b53..b342d77f20 100644 --- a/apps/desktop/src/lib/__tests__/database/databaseFeatureSupport.spec.ts +++ b/apps/desktop/src/lib/__tests__/database/databaseFeatureSupport.spec.ts @@ -95,6 +95,7 @@ describe("supportsTransaction", () => { }); it("returns false for unsupported database types", () => { + expect(supportsTransaction("oceanbase-oracle")).toBe(false); expect(supportsTransaction("redis")).toBe(false); expect(supportsTransaction("mongodb")).toBe(false); expect(supportsTransaction("duckdb")).toBe(false); @@ -116,6 +117,7 @@ describe("supportsTransaction", () => { describe("defaultAutoCommitForDbType", () => { it("defaults query tabs to auto-commit", () => { + expect(defaultAutoCommitForDbType("oceanbase-oracle")).toBe(true); expect(defaultAutoCommitForDbType("oracle")).toBe(true); expect(defaultAutoCommitForDbType("mysql")).toBe(true); expect(defaultAutoCommitForDbType("postgres")).toBe(true); diff --git a/apps/desktop/src/stores/__tests__/queryStore.manualTransactionExpiry.spec.ts b/apps/desktop/src/stores/__tests__/queryStore.manualTransactionExpiry.spec.ts index eabce2e942..76ed58e01c 100644 --- a/apps/desktop/src/stores/__tests__/queryStore.manualTransactionExpiry.spec.ts +++ b/apps/desktop/src/stores/__tests__/queryStore.manualTransactionExpiry.spec.ts @@ -7,6 +7,7 @@ const mocks = vi.hoisted(() => ({ closeClientConnectionSession: vi.fn(), closeQuerySession: vi.fn(), executeInManualTransaction: vi.fn(), + executeMulti: vi.fn(), getConnectionConfig: vi.fn(), prepareQueryPaginationExecutionPlan: vi.fn(), saveOpenTabsState: vi.fn(), @@ -18,6 +19,7 @@ vi.mock("@/lib/backend/api", () => ({ closeClientConnectionSession: mocks.closeClientConnectionSession, closeQuerySession: mocks.closeQuerySession, executeInManualTransaction: mocks.executeInManualTransaction, + executeMulti: mocks.executeMulti, prepareQueryPaginationExecutionPlan: mocks.prepareQueryPaginationExecutionPlan, saveOpenTabsState: mocks.saveOpenTabsState, })); @@ -70,6 +72,10 @@ function successfulUpdate() { return [{ columns: [], rows: [], affected_rows: 1, execution_time_ms: 1 }]; } +function successfulSelect() { + return [{ columns: ["VALUE"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 }]; +} + describe("queryStore manual transaction expiry recovery", () => { beforeEach(() => { vi.clearAllMocks(); @@ -118,4 +124,104 @@ describe("queryStore manual transaction expiry recovery", () => { expect(tab.result?.execution_error).not.toBe(true); expect(tab.result?.affected_rows).toBe(1); }); + + it("normalizes a stale manual OceanBase tab before query dispatch", async () => { + mocks.getConnectionConfig.mockReturnValue({ + id: "oceanbase-1", + name: "OceanBase Oracle", + db_type: "oceanbase-oracle", + database: "SYS", + query_timeout_secs: 30, + }); + mocks.beginManualTransaction.mockRejectedValue(new Error("BEGIN manual transaction failed: Agent RPC error (-1): Unknown method: begin_manual_transaction")); + mocks.executeMulti.mockResolvedValue(successfulSelect()); + + const { useQueryStore } = await import("@/stores/queryStore"); + const store = useQueryStore(); + const tabId = store.createTab("oceanbase-1", "SYS", "Query", "query", "SYS"); + // Simulate a manual-mode value restored from an older saved query tab. + store.setAutoCommit(tabId, false); + + await store.executeTabSql(tabId, "SELECT 1 FROM DUAL"); + + const tab = store.tabs.find((item) => item.id === tabId)!; + expect(tab.autoCommit).toBe(true); + expect(mocks.beginManualTransaction).not.toHaveBeenCalled(); + expect(mocks.executeInManualTransaction).not.toHaveBeenCalled(); + expect(mocks.executeMulti).toHaveBeenCalledOnce(); + expect(tab.result?.rows).toEqual([[1]]); + expect(tab.result?.execution_error).not.toBe(true); + }); + + it("does not fall back to auto-commit when a supported manual transaction fails to begin", async () => { + mocks.beginManualTransaction.mockRejectedValue(new Error("manual transaction begin failed")); + mocks.executeMulti.mockResolvedValue(successfulSelect()); + + const { useQueryStore } = await import("@/stores/queryStore"); + const store = useQueryStore(); + const tabId = store.createTab("oracle-1", "ORCL", "Query", "query", "APP"); + store.setAutoCommit(tabId, false); + + await store.executeTabSql(tabId, "SELECT 1 FROM DUAL"); + + const tab = store.tabs.find((item) => item.id === tabId)!; + expect(tab.autoCommit).toBe(false); + expect(mocks.beginManualTransaction).toHaveBeenCalledOnce(); + expect(mocks.executeInManualTransaction).not.toHaveBeenCalled(); + expect(mocks.executeMulti).not.toHaveBeenCalled(); + expect(tab.result?.execution_error).toBe(true); + }); + + it("keeps raw JDBC connections in manual mode when their effective dialect is unsupported", async () => { + mocks.getConnectionConfig.mockReturnValue({ + id: "jdbc-1", + name: "Databend JDBC", + db_type: "jdbc", + database: "default", + connection_string: "jdbc:databend://localhost:8000/default", + query_timeout_secs: 30, + }); + mocks.beginManualTransaction.mockResolvedValue("txn-jdbc"); + mocks.executeInManualTransaction.mockResolvedValue(successfulSelect()); + + const { useQueryStore } = await import("@/stores/queryStore"); + const store = useQueryStore(); + const tabId = store.createTab("jdbc-1", "default", "Query", "query", "analytics"); + store.setAutoCommit(tabId, false); + + await store.executeTabSql(tabId, "SELECT 1"); + + const tab = store.tabs.find((item) => item.id === tabId)!; + expect(tab.autoCommit).toBe(false); + expect(mocks.beginManualTransaction).toHaveBeenCalledOnce(); + expect(mocks.executeInManualTransaction).toHaveBeenCalledOnce(); + expect(mocks.executeMulti).not.toHaveBeenCalled(); + expect(tab.result?.rows).toEqual([[1]]); + }); + + it("surfaces ordinary query failures after normalizing a stale OceanBase tab", async () => { + mocks.getConnectionConfig.mockReturnValue({ + id: "oceanbase-1", + name: "OceanBase Oracle", + db_type: "oceanbase-oracle", + database: "SYS", + query_timeout_secs: 30, + }); + mocks.beginManualTransaction.mockRejectedValue(new Error("BEGIN manual transaction failed: Agent RPC error (-1): Unknown method: begin_manual_transaction")); + mocks.executeMulti.mockRejectedValue(new Error("OceanBase query failed")); + + const { useQueryStore } = await import("@/stores/queryStore"); + const store = useQueryStore(); + const tabId = store.createTab("oceanbase-1", "SYS", "Query", "query", "SYS"); + store.setAutoCommit(tabId, false); + + await store.executeTabSql(tabId, "SELECT missing_column FROM DUAL"); + + const tab = store.tabs.find((item) => item.id === tabId)!; + expect(tab.autoCommit).toBe(true); + expect(mocks.beginManualTransaction).not.toHaveBeenCalled(); + expect(mocks.executeInManualTransaction).not.toHaveBeenCalled(); + expect(mocks.executeMulti).toHaveBeenCalledOnce(); + expect(tab.result?.execution_error).toBe(true); + }); }); diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index ef438ce573..79d82dde07 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -35,7 +35,7 @@ import { redisCommandResultToQueryResult } from "@/lib/redis/redisQueryResult"; import { nextRedisCommandDb } from "@/lib/redis/redisCommandSession"; import { isRedisMutatingCommand } from "@/lib/redis/redisCommandTable"; import { usesAgentCursorForQuery } from "@/lib/database/databaseDriverManifest"; -import { defaultAutoCommitForDbType, supportsClearableQuerySchema } from "@/lib/database/databaseFeatureSupport"; +import { defaultAutoCommitForDbType, supportsClearableQuerySchema, supportsTransaction } from "@/lib/database/databaseFeatureSupport"; import { canInsertTableRows, canUseKeylessRowPredicate, DBX_ROWID_COLUMN, editablePrimaryKeys, usesSyntheticRowIdKey } from "@/lib/table/tableEditing"; import { TABLE_DATA_EXPORT_PAGE_SIZE } from "@/lib/table/tableDataExport"; import { tableMetaForDataTab } from "@/lib/table/tableDataTabMeta"; @@ -4496,6 +4496,9 @@ export const useQueryStore = defineStore("query", () => { mongoCommands = splitMongoCommandRanges(sql); } const effectiveDbType = effectiveDatabaseTypeForConnection(conn); + if (tab.autoCommit === false && !supportsTransaction(conn?.db_type)) { + tab.autoCommit = true; + } const targetContext = options?.targetContext; if (targetContext?.scope === "namespace") { throw new Error("Namespace execution targets require a registered execution adapter.");