fix(editor): hide commit/rollback for clean mysql/pg manual transactions
Closes #9018
This commit is contained in:
1 parent
91b8fa0aa8
commit
e9be6d0419
15 files changed
+1016
-137
No files matched your search
@@ -1171,7 +1171,11 @@ describe("queryStore hidden primary key editing", () => {
|
||||
|
||||
await store.executeTabSql(tabId, "SELECT name FROM users");
|
||||
|
||||
expect(executeInManualTransaction).toHaveBeenCalledWith("txn-1", "SELECT name, `id` AS `__DBX_PK_0` FROM users", "app", undefined, expect.any(Number), false, undefined, undefined, undefined);
|
||||
// MySQL is a sticky proven-read-only dialect (#9018): the call now ends
|
||||
// with the user-facing classification SQL (9th argument), but still opts
|
||||
// out of table-data preview. Every slot stays asserted — the spec's mocks
|
||||
// are fixed, so database/schema/row-limit/cursor args must not drift.
|
||||
expect(executeInManualTransaction).toHaveBeenCalledWith("txn-1", "SELECT name, `id` AS `__DBX_PK_0` FROM users", "app", undefined, 100000, false, undefined, undefined, "SELECT name FROM users");
|
||||
});
|
||||
|
||||
it("keeps a keyless Oracle query editable when its WHERE clause reads another table", async () => {
|
||||
|
||||
+120
-28
@@ -59,17 +59,17 @@ function installLocalStorage() {
|
||||
});
|
||||
}
|
||||
|
||||
type OracleResult = Record<string, unknown>;
|
||||
type StickyResult = Record<string, unknown>;
|
||||
|
||||
function cleanSelect(): OracleResult[] {
|
||||
function cleanSelect(): StickyResult[] {
|
||||
return [{ columns: ["VALUE"], rows: [[1]], affected_rows: 0, execution_time_ms: 1, manual_transaction_proven_read_only: true }];
|
||||
}
|
||||
|
||||
function dirtyUpdate(): OracleResult[] {
|
||||
function dirtyUpdate(): StickyResult[] {
|
||||
return [{ columns: [], rows: [], affected_rows: 1, execution_time_ms: 1 }];
|
||||
}
|
||||
|
||||
function noOpResult(): OracleResult[] {
|
||||
function noOpResult(): StickyResult[] {
|
||||
return [{ columns: [], rows: [], affected_rows: 0, execution_time_ms: 1, manual_transaction_no_statement: true }];
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ function expiredTransactionError() {
|
||||
};
|
||||
}
|
||||
|
||||
describe("queryStore Oracle manual-transaction sticky state", () => {
|
||||
describe("queryStore Oracle/OceanBase manual-transaction sticky state (behavior frozen)", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
@@ -144,7 +144,7 @@ describe("queryStore Oracle manual-transaction sticky state", () => {
|
||||
await store.executeTabSql(tabId, "SELECT * FROM EMP");
|
||||
|
||||
const tab = store.tabs.find((item) => item.id === tabId)!;
|
||||
expect(tab.oracleTxnPossiblyDirty).not.toBe(true);
|
||||
expect(tab.txnPossiblyDirty).not.toBe(true);
|
||||
expect(tab.txnSessionId).toBe("txn-oracle");
|
||||
});
|
||||
|
||||
@@ -158,7 +158,7 @@ describe("queryStore Oracle manual-transaction sticky state", () => {
|
||||
await store.executeTabSql(tabId, "UPDATE EMP SET DEPTNO = 10");
|
||||
|
||||
const tab = store.tabs.find((item) => item.id === tabId)!;
|
||||
expect(tab.oracleTxnPossiblyDirty).toBe(true);
|
||||
expect(tab.txnPossiblyDirty).toBe(true);
|
||||
});
|
||||
|
||||
it("marks an Oracle manual session dirty when the result grid dispatches a mutation", async () => {
|
||||
@@ -172,7 +172,7 @@ describe("queryStore Oracle manual-transaction sticky state", () => {
|
||||
store.markManualTransactionDirty(tabId);
|
||||
|
||||
const tab = store.tabs.find((item) => item.id === tabId)!;
|
||||
expect(tab.oracleTxnPossiblyDirty).toBe(true);
|
||||
expect(tab.txnPossiblyDirty).toBe(true);
|
||||
});
|
||||
|
||||
it("never clears the sticky state after a later read (UPDATE -> SELECT)", async () => {
|
||||
@@ -186,7 +186,7 @@ describe("queryStore Oracle manual-transaction sticky state", () => {
|
||||
await store.executeTabSql(tabId, "SELECT * FROM EMP");
|
||||
|
||||
const tab = store.tabs.find((item) => item.id === tabId)!;
|
||||
expect(tab.oracleTxnPossiblyDirty).toBe(true);
|
||||
expect(tab.txnPossiblyDirty).toBe(true);
|
||||
});
|
||||
|
||||
it("dirties the session for any unproven statement in a mixed script", async () => {
|
||||
@@ -202,7 +202,7 @@ describe("queryStore Oracle manual-transaction sticky state", () => {
|
||||
await store.executeTabSql(tabId, "SELECT * FROM DUAL; DELETE FROM EMP");
|
||||
|
||||
const tab = store.tabs.find((item) => item.id === tabId)!;
|
||||
expect(tab.oracleTxnPossiblyDirty).toBe(true);
|
||||
expect(tab.txnPossiblyDirty).toBe(true);
|
||||
});
|
||||
|
||||
it("treats a Core no-op result as neither dirty nor clean-changing", async () => {
|
||||
@@ -217,7 +217,7 @@ describe("queryStore Oracle manual-transaction sticky state", () => {
|
||||
await store.executeTabSql(tabId, "SELECT * FROM EMP");
|
||||
|
||||
const tab = store.tabs.find((item) => item.id === tabId)!;
|
||||
expect(tab.oracleTxnPossiblyDirty).not.toBe(true);
|
||||
expect(tab.txnPossiblyDirty).not.toBe(true);
|
||||
});
|
||||
|
||||
it("passes no classificationSql and changes no sticky state on a cursor-page fetch", async () => {
|
||||
@@ -242,7 +242,7 @@ describe("queryStore Oracle manual-transaction sticky state", () => {
|
||||
expect(args.at(-1)).toBeUndefined();
|
||||
const tab = store.tabs.find((item) => item.id === tabId)!;
|
||||
// Page fetch must neither set nor clear the dirty bit.
|
||||
expect(tab.oracleTxnPossiblyDirty).toBe(true);
|
||||
expect(tab.txnPossiblyDirty).toBe(true);
|
||||
});
|
||||
|
||||
it("resets the sticky state on session-expiry recovery and recomputes from the retry", async () => {
|
||||
@@ -258,7 +258,7 @@ describe("queryStore Oracle manual-transaction sticky state", () => {
|
||||
|
||||
const tab = store.tabs.find((item) => item.id === tabId)!;
|
||||
expect(tab.txnSessionId).toBe("txn-new");
|
||||
expect(tab.oracleTxnPossiblyDirty).not.toBe(true);
|
||||
expect(tab.txnPossiblyDirty).not.toBe(true);
|
||||
});
|
||||
|
||||
it("clears the sticky state together with the session on DBX rollback", async () => {
|
||||
@@ -273,7 +273,7 @@ describe("queryStore Oracle manual-transaction sticky state", () => {
|
||||
|
||||
const tab = store.tabs.find((item) => item.id === tabId)!;
|
||||
expect(tab.txnSessionId).toBeUndefined();
|
||||
expect(tab.oracleTxnPossiblyDirty).not.toBe(true);
|
||||
expect(tab.txnPossiblyDirty).not.toBe(true);
|
||||
});
|
||||
|
||||
it("clears session and sticky state when a statement failure disposes the manual session", async () => {
|
||||
@@ -290,7 +290,7 @@ describe("queryStore Oracle manual-transaction sticky state", () => {
|
||||
// The `rolled back` fragment is the cleanup compatibility contract: the
|
||||
// backend removed the manual session, so both fields must reset with it.
|
||||
expect(tab.txnSessionId).toBeUndefined();
|
||||
expect(tab.oracleTxnPossiblyDirty).not.toBe(true);
|
||||
expect(tab.txnPossiblyDirty).not.toBe(true);
|
||||
expect(tab.txnAutoRolledBack).not.toBe(true);
|
||||
});
|
||||
|
||||
@@ -307,7 +307,7 @@ describe("queryStore Oracle manual-transaction sticky state", () => {
|
||||
// The statement may still have executed server-side while the manual
|
||||
// session survives, so the sticky state must stay fail-closed dirty.
|
||||
expect(tab.txnSessionId).toBe("txn-oracle");
|
||||
expect(tab.oracleTxnPossiblyDirty).toBe(true);
|
||||
expect(tab.txnPossiblyDirty).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps a clean session clean when a cursor-page fetch times out", async () => {
|
||||
@@ -325,34 +325,126 @@ describe("queryStore Oracle manual-transaction sticky state", () => {
|
||||
const tab = store.tabs.find((item) => item.id === tabId)!;
|
||||
// Page fetches never participate in the sticky state, matching aggregation.
|
||||
expect(tab.txnSessionId).toBe("txn-oracle");
|
||||
expect(tab.oracleTxnPossiblyDirty).not.toBe(true);
|
||||
expect(tab.txnPossiblyDirty).not.toBe(true);
|
||||
});
|
||||
|
||||
it("leaves non-Oracle manual sessions using the old rule and no marker handling", async () => {
|
||||
it("keeps the legacy toolbar rule for dialects without sticky state", async () => {
|
||||
mocks.getConnectionConfig.mockReturnValue({
|
||||
id: "pg-1",
|
||||
name: "Postgres",
|
||||
db_type: "postgres",
|
||||
database: "postgres",
|
||||
id: "jdbc-1",
|
||||
name: "JDBC",
|
||||
db_type: "jdbc",
|
||||
database: "appdb",
|
||||
query_timeout_secs: 30,
|
||||
});
|
||||
mocks.beginManualTransaction.mockResolvedValue("txn-pg");
|
||||
// A Postgres manual execution never returns the Oracle marker.
|
||||
mocks.beginManualTransaction.mockResolvedValue("txn-jdbc");
|
||||
mocks.executeInManualTransaction.mockResolvedValue([{ columns: [], rows: [], affected_rows: 0, execution_time_ms: 1 }]);
|
||||
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("pg-1", "postgres", "Query", "query", "public");
|
||||
const tabId = store.createTab("jdbc-1", "appdb", "Query", "query", "main");
|
||||
store.setAutoCommit(tabId, false);
|
||||
|
||||
await store.executeTabSql(tabId, "SELECT 1");
|
||||
store.markManualTransactionDirty(tabId);
|
||||
|
||||
const tab = store.tabs.find((item) => item.id === tabId)!;
|
||||
// No classificationSql was sent and no Oracle sticky state exists.
|
||||
// No classificationSql is sent and no sticky state exists for non-sticky
|
||||
// dialects: commit/rollback follow the legacy always-visible rule.
|
||||
const args = mocks.executeInManualTransaction.mock.calls[0]!;
|
||||
expect(args.at(-1)).toBeUndefined();
|
||||
expect(tab.oracleTxnPossiblyDirty).toBeUndefined();
|
||||
expect(tab.txnSessionId).toBe("txn-pg");
|
||||
expect(tab.txnPossiblyDirty).toBeUndefined();
|
||||
expect(tab.txnSessionId).toBe("txn-jdbc");
|
||||
});
|
||||
});
|
||||
|
||||
describe.each([
|
||||
{ label: "MySQL", configId: "mysql-1", database: "test", dbType: "mysql", txnId: "txn-mysql" },
|
||||
{ label: "PostgreSQL", configId: "pg-1", database: "postgres", dbType: "postgres", txnId: "txn-pg" },
|
||||
])("queryStore $label proven-read-only sticky state", ({ configId, database, dbType, txnId }) => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
installLocalStorage();
|
||||
setActivePinia(createPinia());
|
||||
mocks.getConnectionConfig.mockReturnValue({
|
||||
id: configId,
|
||||
name: dbType,
|
||||
db_type: dbType,
|
||||
database,
|
||||
query_timeout_secs: 30,
|
||||
});
|
||||
mocks.prepareQueryPaginationExecutionPlan.mockImplementation(async (options) => ({
|
||||
sqlToExecute: options.sql,
|
||||
pageSql: undefined,
|
||||
pageLimit: undefined,
|
||||
pageOffset: undefined,
|
||||
countSql: undefined,
|
||||
useAgentResultSession: false,
|
||||
}));
|
||||
mocks.analyzeEditableQueryEditability.mockResolvedValue({ editable: false, reason: "not-select" });
|
||||
mocks.saveOpenTabsState.mockResolvedValue(undefined);
|
||||
mocks.beginManualTransaction.mockResolvedValue(txnId);
|
||||
});
|
||||
|
||||
async function setupStickyManualTab(store: { createTab: (c: string, d: string, t: string, m: string, s: string) => string }) {
|
||||
const tabId = store.createTab(configId, database, "Query", "query", "APP");
|
||||
store.setAutoCommit(tabId, false);
|
||||
return tabId;
|
||||
}
|
||||
|
||||
it("sends classificationSql for the initial manual execution and stays clean on a proven read", async () => {
|
||||
mocks.executeInManualTransaction.mockResolvedValue(cleanSelect());
|
||||
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = await setupStickyManualTab(store);
|
||||
|
||||
await store.executeTabSql(tabId, "SELECT * FROM users");
|
||||
|
||||
const args = mocks.executeInManualTransaction.mock.calls[0]!;
|
||||
expect(args.at(-1)).toBe("SELECT * FROM users");
|
||||
const tab = store.tabs.find((item) => item.id === tabId)!;
|
||||
expect(tab.txnPossiblyDirty).not.toBe(true);
|
||||
expect(tab.txnSessionId).toBe(txnId);
|
||||
});
|
||||
|
||||
it("dirties the session after an unproven statement", async () => {
|
||||
mocks.executeInManualTransaction.mockResolvedValue(dirtyUpdate());
|
||||
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = await setupStickyManualTab(store);
|
||||
|
||||
await store.executeTabSql(tabId, "UPDATE users SET name = 'x'");
|
||||
|
||||
const tab = store.tabs.find((item) => item.id === tabId)!;
|
||||
expect(tab.txnPossiblyDirty).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps a comment-only script from dirtying the session (core no-op)", async () => {
|
||||
mocks.executeInManualTransaction.mockResolvedValue(noOpResult());
|
||||
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = await setupStickyManualTab(store);
|
||||
|
||||
await store.executeTabSql(tabId, " -- only a comment\n");
|
||||
|
||||
const tab = store.tabs.find((item) => item.id === tabId)!;
|
||||
expect(tab.txnPossiblyDirty).not.toBe(true);
|
||||
});
|
||||
|
||||
it("marks the session dirty when the result grid dispatches a mutation", async () => {
|
||||
mocks.executeInManualTransaction.mockResolvedValue(cleanSelect());
|
||||
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = await setupStickyManualTab(store);
|
||||
await store.executeTabSql(tabId, "SELECT * FROM users");
|
||||
|
||||
store.markManualTransactionDirty(tabId);
|
||||
|
||||
const tab = store.tabs.find((item) => item.id === tabId)!;
|
||||
expect(tab.txnPossiblyDirty).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -39,7 +39,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, usesOracleStickyTransactionState } from "@/lib/database/databaseFeatureSupport";
|
||||
import { defaultAutoCommitForDbType, supportsClearableQuerySchema, supportsTransaction, usesOracleStickyTransactionState, usesProvenReadOnlyStickyTransactionState } 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";
|
||||
@@ -87,7 +87,7 @@ import { appendDebugLog } from "@/lib/backend/debugLog";
|
||||
import { BackendErrorException, formatError, isManualTransactionSessionExpired, isUnsupportedManualTransactionMethod, normalizeBackendError, type BackendError } from "@/lib/backend/errorUtils";
|
||||
import { createSavedSqlEditorPosition, initSavedSqlEditorPositions, restoreSavedSqlEditorPosition, saveSavedSqlEditorPosition } from "@/lib/app/savedSqlEditorPosition";
|
||||
import { isDetachedWindow, resolveWindowContext } from "@/lib/app/windowContext";
|
||||
import type { DetachedTabHandoff, DetachedTabRuntimeState } from "@/lib/app/detachedTabHandoff";
|
||||
import { normalizeDetachedTabRuntime, type DetachedTabHandoff, type DetachedTabRuntimeState } from "@/lib/app/detachedTabHandoff";
|
||||
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
import { ensureSqlExtension } from "@/lib/savedSql/savedSqlFileName";
|
||||
import { resolveSavedSqlExecutionTarget, savedSqlExecutionTargetFromTab, type SavedSqlExecutionTarget, type SavedSqlOpenTargetMode } from "@/lib/savedSql/savedSqlExecutionTarget";
|
||||
@@ -2486,7 +2486,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
editorSelection: tab.editorSelection,
|
||||
txnSessionId: tab.txnSessionId,
|
||||
txnAutoRolledBack: tab.txnAutoRolledBack,
|
||||
oracleTxnPossiblyDirty: tab.oracleTxnPossiblyDirty,
|
||||
txnPossiblyDirty: tab.txnPossiblyDirty,
|
||||
...runtime,
|
||||
},
|
||||
...(snapshot ? { resultCacheKey } : {}),
|
||||
@@ -2541,7 +2541,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
restoredTab.originalSql = file.sql;
|
||||
}
|
||||
}
|
||||
Object.assign(restoredTab, handoff.runtime);
|
||||
Object.assign(restoredTab, normalizeDetachedTabRuntime(handoff.runtime));
|
||||
if (handoff.resultCacheKey) {
|
||||
restoredTab.resultCacheKey = handoff.resultCacheKey;
|
||||
const restoredResult = restoreCachedResultPayload(restoredTab, await readTabResultSnapshot(handoff.resultCacheKey));
|
||||
@@ -4614,15 +4614,15 @@ 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 (usesOracleStickyTransactionState(dbType)) tab.oracleTxnPossiblyDirty = true;
|
||||
if (usesProvenReadOnlyStickyTransactionState(dbType)) tab.txnPossiblyDirty = true;
|
||||
}
|
||||
|
||||
/** Reset only the Oracle sticky-dirty bit. Used when a session continues but
|
||||
* the old dirty state must be discarded (e.g. idle-expiry recovery where the
|
||||
* replacement session starts fresh). Full session cleanup goes through
|
||||
* `clearManualTransactionSession`. */
|
||||
function clearOracleTxnPossiblyDirty(tab: { oracleTxnPossiblyDirty?: boolean }) {
|
||||
if (tab.oracleTxnPossiblyDirty !== undefined) tab.oracleTxnPossiblyDirty = false;
|
||||
/** Reset only the sticky proven-read-only dirty bit. Used when a session
|
||||
* continues but the old dirty state must be discarded (e.g. idle-expiry
|
||||
* recovery where the replacement session starts fresh). Full session cleanup
|
||||
* goes through `clearManualTransactionSession`. */
|
||||
function clearTxnPossiblyDirty(tab: { txnPossiblyDirty?: boolean }) {
|
||||
if (tab.txnPossiblyDirty !== undefined) tab.txnPossiblyDirty = false;
|
||||
}
|
||||
|
||||
/** Centralized manual-session cleanup. Clears every field tied to a manual
|
||||
@@ -4631,7 +4631,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
function clearManualTransactionSession(tab: QueryTab) {
|
||||
tab.txnSessionId = undefined;
|
||||
tab.txnAutoRolledBack = false;
|
||||
if (tab.oracleTxnPossiblyDirty !== undefined) tab.oracleTxnPossiblyDirty = false;
|
||||
if (tab.txnPossiblyDirty !== undefined) tab.txnPossiblyDirty = false;
|
||||
}
|
||||
|
||||
function rollbackTabTransaction(tab: QueryTab, options?: { resetAutoCommit?: boolean; resetAutoCommitDbType?: string }) {
|
||||
@@ -4644,7 +4644,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
const dbType = options.resetAutoCommitDbType ?? useConnectionStore().getConfig(tab.connectionId)?.db_type;
|
||||
tab.autoCommit = defaultAutoCommitForDbTypeWithSetting(dbType);
|
||||
}
|
||||
clearOracleTxnPossiblyDirty(tab);
|
||||
clearTxnPossiblyDirty(tab);
|
||||
tab.txnAutoRolledBack = false;
|
||||
}
|
||||
|
||||
@@ -7059,10 +7059,11 @@ export const useQueryStore = defineStore("query", () => {
|
||||
} else {
|
||||
queryExecutionLog("info", "execute-in-txn:invoke", { traceId, txnSessionId: tab.txnSessionId, elapsed: elapsed() });
|
||||
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 = usesOracleStickyTransactionState(effectiveDbType) && !options?.pagination?.sessionId;
|
||||
const classificationSql = isInitialOracleManualExecution ? queryBaseSql : undefined;
|
||||
// Only an initial manual execution classifies the user SQL (sticky
|
||||
// proven-read-only dialects). A later cursor-page fetch must neither
|
||||
// set nor clear the sticky bit.
|
||||
const isInitialStickyClassification = usesProvenReadOnlyStickyTransactionState(effectiveDbType) && !options?.pagination?.sessionId;
|
||||
const classificationSql = isInitialStickyClassification ? queryBaseSql : undefined;
|
||||
let manualTransactionRecoveryAttempted = false;
|
||||
executionPromise = (async () => {
|
||||
const txnSessionId = tab.txnSessionId;
|
||||
@@ -7082,7 +7083,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
manualTransactionRecoveryAttempted = true;
|
||||
// The expired session was discarded by the backend; the replacement
|
||||
// session starts fresh, so the old sticky state resets with it.
|
||||
clearOracleTxnPossiblyDirty(tab);
|
||||
clearTxnPossiblyDirty(tab);
|
||||
tab.txnSessionId = undefined;
|
||||
tab.txnAutoRolledBack = true;
|
||||
queryExecutionLog("info", "manual-txn:expired-recover", { traceId, elapsed: elapsed() });
|
||||
@@ -7107,16 +7108,17 @@ export const useQueryStore = defineStore("query", () => {
|
||||
);
|
||||
const results = offsetBatchQueryResultIndexes(annotatedResults.results, batchResume?.startStatementIndex ?? 0);
|
||||
reconcileBatchSqlResults(tab, executionId, results);
|
||||
// Oracle-only sticky state aggregation. Only the initial manual execution
|
||||
// participates: a later cursor-page fetch (pagination.sessionId present)
|
||||
// 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 && usesOracleStickyTransactionState(effectiveDbType) && !options?.pagination?.sessionId && tab.txnSessionId) {
|
||||
// Sticky proven-read-only aggregation (Oracle/OceanBase-Oracle/MySQL/PG).
|
||||
// Only the initial manual execution participates: a later cursor-page
|
||||
// fetch (pagination.sessionId present) 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 && usesProvenReadOnlyStickyTransactionState(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;
|
||||
tab.txnPossiblyDirty = true;
|
||||
}
|
||||
}
|
||||
const successfulOracleSchemaChanges = usesOracleStickyTransactionState(effectiveDbType) ? results.filter((result) => result.execution_error !== true && isOracleCurrentSchemaStatement(result.sourceStatement)).length : 0;
|
||||
@@ -7341,21 +7343,21 @@ export const useQueryStore = defineStore("query", () => {
|
||||
if (idleTimeout) {
|
||||
// Backend session was removed and rolled back after idle expiry: clear
|
||||
// the sticky dirty state together with the session.
|
||||
clearOracleTxnPossiblyDirty(tab);
|
||||
clearTxnPossiblyDirty(tab);
|
||||
tab.txnSessionId = undefined;
|
||||
tab.txnAutoRolledBack = true;
|
||||
} else if (/rolled.?back/i.test(errMsg) || /transaction session not found/i.test(errMsg) || /agent runtime terminated/i.test(errMsg)) {
|
||||
// Statement failure that disposed the manual session: the `rolled back`
|
||||
// message fragment is a frontend cleanup compatibility contract.
|
||||
clearOracleTxnPossiblyDirty(tab);
|
||||
clearTxnPossiblyDirty(tab);
|
||||
tab.txnSessionId = undefined;
|
||||
tab.txnAutoRolledBack = false;
|
||||
} else if (tab.txnSessionId && executionDispatched && !options?.pagination?.sessionId && usesOracleStickyTransactionState(effectiveDatabaseTypeForConnection(useConnectionStore().getConfig(tab.connectionId)))) {
|
||||
} else if (tab.txnSessionId && executionDispatched && !options?.pagination?.sessionId && usesProvenReadOnlyStickyTransactionState(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
|
||||
// dirty session. Cursor-page fetches stay excluded like the aggregation.
|
||||
tab.oracleTxnPossiblyDirty = true;
|
||||
tab.txnPossiblyDirty = true;
|
||||
}
|
||||
}
|
||||
const current = findExecutionTab(id);
|
||||
|
||||
Reference in new issue
Block a user