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
@@ -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