fix(query): guard unsupported stale manual transaction state

Closes #7045
This commit is contained in:
t8y2 committed 2026-08-24 12:55:43 +00:00
1 parent 255faf0e4c
commit ba25a871fd
3 files changed
+112 -1

No files matched your search

@@ -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);
@@ -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);
});
});
+4 -1
View File
@@ -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.");