fix(oceanbase): support manual transaction control in oracle mode
This commit is contained in:
15 files changed
+391
-52
No files matched your search
@@ -13,6 +13,7 @@ import { createContentSurfaceEventForwarders } from "@/lib/tabs/contentSurfaceEv
|
||||
import { isPreviewTab } from "@/lib/tabs/tabPresentation";
|
||||
import { resolveExecutableSql } from "@/lib/sql/sqlExecutionTarget";
|
||||
import { effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
|
||||
import { usesOracleStickyTransactionState } from "@/lib/database/databaseFeatureSupport";
|
||||
import { GROUP_TAB_BAR_PORTAL } from "./groupTabBarPortal";
|
||||
import type { ContentAreaSurfaceEmits, ContentAreaSurfaceProps, QueryEditorSurfaceHandle, StatementRange } from "./querySurfaces";
|
||||
import type { QueryTab } from "@/types/database";
|
||||
@@ -108,7 +109,7 @@ const groupTabs = computed(() => {
|
||||
const activeTab = computed(() => groupTabs.value.find((tab) => tab.id === props.activeTabId) ?? groupTabs.value[0] ?? null);
|
||||
const activeConnection = computed(() => (activeTab.value ? connectionStore.getConfig(activeTab.value.connectionId) : undefined));
|
||||
const showGroupToolbar = computed(() => activeTab.value?.mode === "query" && !isPreviewTab(activeTab.value));
|
||||
const isGroupOracleManualTransaction = computed(() => effectiveDatabaseTypeForConnection(activeConnection.value) === "oracle" && (activeTab.value?.autoCommit ?? true) === false);
|
||||
const isGroupOracleManualTransaction = computed(() => usesOracleStickyTransactionState(effectiveDatabaseTypeForConnection(activeConnection.value)) && (activeTab.value?.autoCommit ?? true) === false);
|
||||
// Each group previews the executable SQL of its own active tab (selection
|
||||
// stored on the tab), not the focused tab's global selection.
|
||||
// tabPlacement drives each pane's own bar position: the strip sits above,
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
supportsTableImport,
|
||||
supportsTableVacuum,
|
||||
supportsTransaction,
|
||||
usesOracleStickyTransactionState,
|
||||
usesConnectionOnlyQueryTarget,
|
||||
usesTreeSchemaMode,
|
||||
schemaNodeHasLoadableName,
|
||||
@@ -160,10 +161,10 @@ describe("supportsTransaction", () => {
|
||||
expect(supportsTransaction("mysql")).toBe(true);
|
||||
expect(supportsTransaction("oracle")).toBe(true);
|
||||
expect(supportsTransaction("jdbc")).toBe(true);
|
||||
expect(supportsTransaction("oceanbase-oracle")).toBe(true);
|
||||
});
|
||||
|
||||
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);
|
||||
@@ -198,6 +199,7 @@ describe("defaultAutoCommitForDbType", () => {
|
||||
expect(defaultAutoCommitForDbType("postgres", "manual")).toBe(false);
|
||||
expect(defaultAutoCommitForDbType("oracle", "manual")).toBe(false);
|
||||
expect(defaultAutoCommitForDbType("jdbc", "manual")).toBe(false);
|
||||
expect(defaultAutoCommitForDbType("oceanbase-oracle", "manual")).toBe(false);
|
||||
expect(defaultAutoCommitForDbType("mysql", "auto")).toBe(true);
|
||||
expect(defaultAutoCommitForDbType(undefined, "auto")).toBe(true);
|
||||
});
|
||||
@@ -208,11 +210,24 @@ describe("defaultAutoCommitForDbType", () => {
|
||||
expect(defaultAutoCommitForDbType("sqlite", "manual")).toBe(true);
|
||||
expect(defaultAutoCommitForDbType("dameng", "manual")).toBe(true);
|
||||
expect(defaultAutoCommitForDbType("clickhouse", "manual")).toBe(true);
|
||||
expect(defaultAutoCommitForDbType("oceanbase-oracle", "manual")).toBe(true);
|
||||
expect(defaultAutoCommitForDbType(undefined, "manual")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("usesOracleStickyTransactionState", () => {
|
||||
it("enables sticky manual-transaction state for Oracle family", () => {
|
||||
expect(usesOracleStickyTransactionState("oracle")).toBe(true);
|
||||
expect(usesOracleStickyTransactionState("oceanbase-oracle")).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps other databases on the generic transaction path", () => {
|
||||
expect(usesOracleStickyTransactionState("mysql")).toBe(false);
|
||||
expect(usesOracleStickyTransactionState("postgres")).toBe(false);
|
||||
expect(usesOracleStickyTransactionState("jdbc")).toBe(false);
|
||||
expect(usesOracleStickyTransactionState(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("supportsSqlInListPaste", () => {
|
||||
it("allows generic and SQL-like editors", () => {
|
||||
expect(supportsSqlInListPaste(undefined)).toBe(true);
|
||||
|
||||
@@ -257,7 +257,12 @@ export function usesPostgresLikeStructureCopy(dbType?: DatabaseType): boolean {
|
||||
return !!dbType && PG_LIKE_STRUCTURE_TYPES.has(dbType);
|
||||
}
|
||||
|
||||
const TRANSACTION_SUPPORTED_TYPES: readonly string[] = ["postgres", "mysql", "oracle", "jdbc"];
|
||||
const TRANSACTION_SUPPORTED_TYPES: readonly string[] = ["postgres", "mysql", "oracle", "jdbc", "oceanbase-oracle"];
|
||||
|
||||
/** Oracle-family databases that use the sticky manual-transaction UX. OceanBase
|
||||
* Oracle mode runs the same transaction model as Oracle, so it shares the
|
||||
* commit/rollback dirty-state behavior rather than the generic transaction path. */
|
||||
const ORACLE_STICKY_TRANSACTION_TYPES: ReadonlySet<string> = new Set(["oracle", "oceanbase-oracle"]);
|
||||
|
||||
/**
|
||||
* Returns true if the given database type supports explicit transaction control
|
||||
@@ -311,6 +316,14 @@ export function rejectsAliasReferenceInHaving(dbType?: string): boolean {
|
||||
return !!dbType && HAVING_ALIAS_REJECTED_DATABASE_TYPES.has(dbType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the database type uses Oracle's sticky manual-transaction state
|
||||
* (commit/rollback hidden until an unproven statement dirties the session).
|
||||
*/
|
||||
export function usesOracleStickyTransactionState(dbType?: string): boolean {
|
||||
return !!dbType && ORACLE_STICKY_TRANSACTION_TYPES.has(dbType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Default auto-commit mode when opening a query tab for the given database type.
|
||||
* Query tabs default to auto-commit; users can explicitly switch to manual transactions.
|
||||
|
||||
+22
-15
@@ -125,12 +125,12 @@ describe("queryStore manual transaction expiry recovery", () => {
|
||||
expect(tab.result?.affected_rows).toBe(1);
|
||||
});
|
||||
|
||||
it("normalizes a stale manual OceanBase tab before query dispatch", async () => {
|
||||
it("normalizes a stale manual tab for a non-transactional database before query dispatch", async () => {
|
||||
mocks.getConnectionConfig.mockReturnValue({
|
||||
id: "oceanbase-1",
|
||||
name: "OceanBase Oracle",
|
||||
db_type: "oceanbase-oracle",
|
||||
database: "SYS",
|
||||
id: "sqlite-1",
|
||||
name: "SQLite",
|
||||
db_type: "sqlite",
|
||||
database: "main",
|
||||
query_timeout_secs: 30,
|
||||
});
|
||||
mocks.beginManualTransaction.mockRejectedValue(new Error("BEGIN manual transaction failed: Agent RPC error (-1): Unknown method: begin_manual_transaction"));
|
||||
@@ -138,7 +138,7 @@ describe("queryStore manual transaction expiry recovery", () => {
|
||||
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("oceanbase-1", "SYS", "Query", "query", "SYS");
|
||||
const tabId = store.createTab("sqlite-1", "main", "Query", "query", "main");
|
||||
// Simulate a manual-mode value restored from an older saved query tab.
|
||||
store.setAutoCommit(tabId, false);
|
||||
|
||||
@@ -153,13 +153,20 @@ describe("queryStore manual transaction expiry recovery", () => {
|
||||
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 () => {
|
||||
it.each(["oracle", "oceanbase-oracle"] as const)("does not fall back to auto-commit when a supported %s manual transaction fails to begin", async (databaseType) => {
|
||||
mocks.beginManualTransaction.mockRejectedValue(new Error("manual transaction begin failed"));
|
||||
mocks.executeMulti.mockResolvedValue(successfulSelect());
|
||||
mocks.getConnectionConfig.mockReturnValue({
|
||||
id: `${databaseType}-1`,
|
||||
name: databaseType,
|
||||
db_type: databaseType,
|
||||
database: "ORCL",
|
||||
query_timeout_secs: 30,
|
||||
});
|
||||
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("oracle-1", "ORCL", "Query", "query", "APP");
|
||||
const tabId = store.createTab(`${databaseType}-1`, "ORCL", "Query", "query", "APP");
|
||||
store.setAutoCommit(tabId, false);
|
||||
|
||||
await store.executeTabSql(tabId, "SELECT 1 FROM DUAL");
|
||||
@@ -238,20 +245,20 @@ describe("queryStore manual transaction expiry recovery", () => {
|
||||
expect(tab.result?.rows).toEqual([[1]]);
|
||||
});
|
||||
|
||||
it("surfaces ordinary query failures after normalizing a stale OceanBase tab", async () => {
|
||||
it("surfaces ordinary query failures after normalizing a stale non-transactional tab", async () => {
|
||||
mocks.getConnectionConfig.mockReturnValue({
|
||||
id: "oceanbase-1",
|
||||
name: "OceanBase Oracle",
|
||||
db_type: "oceanbase-oracle",
|
||||
database: "SYS",
|
||||
id: "sqlite-1",
|
||||
name: "SQLite",
|
||||
db_type: "sqlite",
|
||||
database: "main",
|
||||
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"));
|
||||
mocks.executeMulti.mockRejectedValue(new Error("SQLite query failed"));
|
||||
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("oceanbase-1", "SYS", "Query", "query", "SYS");
|
||||
const tabId = store.createTab("sqlite-1", "main", "Query", "query", "main");
|
||||
store.setAutoCommit(tabId, false);
|
||||
|
||||
await store.executeTabSql(tabId, "SELECT missing_column FROM DUAL");
|
||||
|
||||
@@ -558,11 +558,11 @@ describe("queryStore multi-statement errors", () => {
|
||||
expect(tab.results).toHaveLength(4);
|
||||
});
|
||||
|
||||
it("invalidates only the executing Oracle tab after successful CURRENT_SCHEMA changes", async () => {
|
||||
it.each(["oracle", "oceanbase-oracle"] as const)("invalidates only the executing %s tab after successful CURRENT_SCHEMA changes", async (databaseType) => {
|
||||
mocks.getConnectionConfig.mockReturnValue({
|
||||
id: "oracle-1",
|
||||
name: "Oracle",
|
||||
db_type: "oracle",
|
||||
id: `${databaseType}-1`,
|
||||
name: databaseType,
|
||||
db_type: databaseType,
|
||||
database: "ORCL",
|
||||
query_timeout_secs: 30,
|
||||
});
|
||||
@@ -572,8 +572,8 @@ describe("queryStore multi-statement errors", () => {
|
||||
.mockResolvedValueOnce([{ columns: [], rows: [], affected_rows: 0, execution_time_ms: 1 }]);
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabA = store.createTab("oracle-1", "ORCL", "Tab A");
|
||||
const tabB = store.createTab("oracle-1", "ORCL", "Tab B");
|
||||
const tabA = store.createTab(`${databaseType}-1`, "ORCL", "Tab A");
|
||||
const tabB = store.createTab(`${databaseType}-1`, "ORCL", "Tab B");
|
||||
// Exercise the explicit auto-commit execute-multi path.
|
||||
store.setAutoCommit(tabA, true);
|
||||
store.setAutoCommit(tabB, true);
|
||||
|
||||
@@ -38,7 +38,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, supportsTransaction } from "@/lib/database/databaseFeatureSupport";
|
||||
import { defaultAutoCommitForDbType, supportsClearableQuerySchema, supportsTransaction, usesOracleStickyTransactionState } from "@/lib/database/databaseFeatureSupport";
|
||||
import { canInsertTableRows, canUseKeylessRowPredicate, DBX_ROWID_COLUMN, editablePrimaryKeys, shouldIncludeSyntheticRowId, usesSyntheticRowIdKey } from "@/lib/table/tableEditing";
|
||||
import { TABLE_DATA_EXPORT_PAGE_SIZE } from "@/lib/table/tableDataExport";
|
||||
import { tableMetaForDataTab } from "@/lib/table/tableDataTabMeta";
|
||||
@@ -4578,7 +4578,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
const tab = tabs.value.find((item) => item.id === id);
|
||||
if (!tab?.txnSessionId) return;
|
||||
const dbType = effectiveDatabaseTypeForConnection(useConnectionStore().getConfig(tab.connectionId));
|
||||
if (dbType === "oracle") tab.oracleTxnPossiblyDirty = true;
|
||||
if (usesOracleStickyTransactionState(dbType)) tab.oracleTxnPossiblyDirty = true;
|
||||
}
|
||||
|
||||
/** Reset only the Oracle sticky-dirty bit. Used when a session continues but
|
||||
@@ -7009,7 +7009,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
executionDispatched = true;
|
||||
// Only an initial manual execution classifies the user SQL (Oracle-only).
|
||||
// A later cursor-page fetch must neither set nor clear the sticky bit.
|
||||
const isInitialOracleManualExecution = effectiveDbType === "oracle" && !options?.pagination?.sessionId;
|
||||
const isInitialOracleManualExecution = usesOracleStickyTransactionState(effectiveDbType) && !options?.pagination?.sessionId;
|
||||
const classificationSql = isInitialOracleManualExecution ? queryBaseSql : undefined;
|
||||
let manualTransactionRecoveryAttempted = false;
|
||||
executionPromise = (async () => {
|
||||
@@ -7059,14 +7059,14 @@ export const useQueryStore = defineStore("query", () => {
|
||||
// must neither set nor clear the bit, and the Core no-op (empty script)
|
||||
// must neither set nor clear it. Otherwise any result that is not proven
|
||||
// read-only dirties the session monotonically.
|
||||
if (tab.autoCommit === false && effectiveDbType === "oracle" && !options?.pagination?.sessionId && tab.txnSessionId) {
|
||||
if (tab.autoCommit === false && usesOracleStickyTransactionState(effectiveDbType) && !options?.pagination?.sessionId && tab.txnSessionId) {
|
||||
const rawResults = annotatedResults.results;
|
||||
const isCoreNoOp = rawResults.length > 0 && rawResults.every((result) => result.manual_transaction_no_statement === true);
|
||||
if (!isCoreNoOp && rawResults.some((result) => result.manual_transaction_proven_read_only !== true)) {
|
||||
tab.oracleTxnPossiblyDirty = true;
|
||||
}
|
||||
}
|
||||
const successfulOracleSchemaChanges = effectiveDbType === "oracle" ? results.filter((result) => result.execution_error !== true && isOracleCurrentSchemaStatement(result.sourceStatement)).length : 0;
|
||||
const successfulOracleSchemaChanges = usesOracleStickyTransactionState(effectiveDbType) ? results.filter((result) => result.execution_error !== true && isOracleCurrentSchemaStatement(result.sourceStatement)).length : 0;
|
||||
const successfulSapHanaSchemaChanges = effectiveDbType === "saphana" ? results.filter((result) => result.execution_error !== true && isSapHanaSetSchemaStatement(result.sourceStatement)).length : 0;
|
||||
const sqlServerUseDatabase = effectiveDbType === "sqlserver" ? annotatedResults.sqlServerUseDatabase : undefined;
|
||||
if (hiddenPrimaryKeys.length > 0 && results.length === 1) {
|
||||
@@ -7297,7 +7297,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
clearOracleTxnPossiblyDirty(tab);
|
||||
tab.txnSessionId = undefined;
|
||||
tab.txnAutoRolledBack = false;
|
||||
} else if (tab.txnSessionId && executionDispatched && !options?.pagination?.sessionId && effectiveDatabaseTypeForConnection(useConnectionStore().getConfig(tab.connectionId)) === "oracle") {
|
||||
} else if (tab.txnSessionId && executionDispatched && !options?.pagination?.sessionId && usesOracleStickyTransactionState(effectiveDatabaseTypeForConnection(useConnectionStore().getConfig(tab.connectionId)))) {
|
||||
// Frontend timeout/cancel or mid-script failure: the statement may still
|
||||
// have executed server-side while the manual session survives, so keep
|
||||
// the sticky dirty state fail-closed instead of a clean toolbar on a
|
||||
|
||||
Reference in new issue
Block a user