fix(editor): skip idle rollback notice for read-only manual transactions
This commit is contained in:
2 files changed
+71
-3
No files matched your search
@@ -76,6 +76,10 @@ function successfulSelect() {
|
||||
return [{ columns: ["VALUE"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 }];
|
||||
}
|
||||
|
||||
function successfulReadOnlySelect() {
|
||||
return [{ columns: ["VALUE"], rows: [[1]], affected_rows: 0, execution_time_ms: 1, manual_transaction_proven_read_only: true }];
|
||||
}
|
||||
|
||||
describe("queryStore manual transaction expiry recovery", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -125,6 +129,51 @@ describe("queryStore manual transaction expiry recovery", () => {
|
||||
expect(tab.result?.affected_rows).toBe(1);
|
||||
});
|
||||
|
||||
it("restarts an expired read-only transaction without the rollback notice", async () => {
|
||||
mocks.beginManualTransaction.mockResolvedValueOnce("txn-old").mockResolvedValueOnce("txn-new");
|
||||
mocks.executeInManualTransaction.mockResolvedValueOnce(successfulReadOnlySelect()).mockRejectedValueOnce(expiredTransactionError()).mockResolvedValueOnce(successfulReadOnlySelect());
|
||||
|
||||
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 VALUE FROM DUAL");
|
||||
await store.executeTabSql(tabId, "SELECT VALUE FROM DUAL");
|
||||
|
||||
const tab = store.tabs.find((item) => item.id === tabId)!;
|
||||
expect(tab.txnSessionId).toBe("txn-new");
|
||||
expect(tab.result?.rows).toEqual([[1]]);
|
||||
// 只读会话没有任何未提交改动,空闲回收后静默重建即可,不应提示「事务已自动回滚」(#9831)
|
||||
expect(tab.txnAutoRolledBack).not.toBe(true);
|
||||
});
|
||||
|
||||
it("keeps the rollback notice for a manual dialect without sticky read-only tracking", async () => {
|
||||
mocks.getConnectionConfig.mockReturnValue({
|
||||
id: "dameng-1",
|
||||
name: "DM",
|
||||
db_type: "dameng",
|
||||
database: "DMHR",
|
||||
query_timeout_secs: 30,
|
||||
});
|
||||
mocks.beginManualTransaction.mockResolvedValueOnce("txn-old").mockResolvedValueOnce("txn-new");
|
||||
mocks.executeInManualTransaction.mockResolvedValueOnce(successfulSelect()).mockRejectedValueOnce(expiredTransactionError()).mockResolvedValueOnce(successfulSelect());
|
||||
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("dameng-1", "DMHR", "Query", "query", "SYSDBA");
|
||||
store.setAutoCommit(tabId, false);
|
||||
|
||||
await store.executeTabSql(tabId, "SELECT 1 FROM DUAL");
|
||||
await store.executeTabSql(tabId, "SELECT 1 FROM DUAL");
|
||||
|
||||
const tab = store.tabs.find((item) => item.id === tabId)!;
|
||||
expect(tab.txnSessionId).toBe("txn-new");
|
||||
expect(tab.result?.rows).toEqual([[1]]);
|
||||
// 不跟踪只读状态的方言无法判断会话是否干净,保留原有提示。
|
||||
expect(tab.txnAutoRolledBack).toBe(true);
|
||||
});
|
||||
|
||||
it("normalizes a stale manual tab for a non-transactional database before query dispatch", async () => {
|
||||
mocks.getConnectionConfig.mockReturnValue({
|
||||
id: "sqlite-1",
|
||||
|
||||
@@ -4931,6 +4931,20 @@ export const useQueryStore = defineStore("query", () => {
|
||||
if (tab.txnPossiblyDirty !== undefined) tab.txnPossiblyDirty = false;
|
||||
}
|
||||
|
||||
/** Whether an expired manual transaction still owes the user the
|
||||
* `toolbar.txnAutoRolledBack` notice.
|
||||
*
|
||||
* Sticky proven-read-only dialects (Oracle / OceanBase-Oracle / MySQL /
|
||||
* PostgreSQL) track whether the session ever ran a statement that is not
|
||||
* proven read-only: `txnPossiblyDirty` stays unset while every batch was
|
||||
* proven read-only, so an idle expiry discarded no uncommitted work and the
|
||||
* session can be rebuilt silently (#9831). Dialects without that tracking
|
||||
* cannot tell a clean session from a dirty one, so they keep the notice. */
|
||||
function manualTransactionRollbackNoticeRequired(tab: { txnPossiblyDirty?: boolean }, dbType?: string): boolean {
|
||||
if (!usesProvenReadOnlyStickyTransactionState(dbType)) return true;
|
||||
return tab.txnPossiblyDirty === true;
|
||||
}
|
||||
|
||||
/** Auto-commit tabs mirror the backend's report of an open explicit
|
||||
* transaction. The flag is dropped whenever the tab stops pointing at the
|
||||
* connection that reported it (target switch, tab close), so a stale badge
|
||||
@@ -7523,11 +7537,14 @@ export const useQueryStore = defineStore("query", () => {
|
||||
if (options?.pagination?.sessionId || manualTransactionRecoveryAttempted || !isManualTransactionSessionExpired(error)) throw error;
|
||||
if (tab.executionId !== executionId || tab.autoCommit !== false || manualTransactionTargetEpoch(tab) !== executionTargetEpoch) throw error;
|
||||
manualTransactionRecoveryAttempted = true;
|
||||
// A session that only ever ran proven read-only statements lost
|
||||
// nothing to the idle rollback, so it restarts without the notice.
|
||||
const rollbackNoticeRequired = manualTransactionRollbackNoticeRequired(tab, effectiveDbType);
|
||||
// The expired session was discarded by the backend; the replacement
|
||||
// session starts fresh, so the old sticky state resets with it.
|
||||
clearTxnPossiblyDirty(tab);
|
||||
tab.txnSessionId = undefined;
|
||||
tab.txnAutoRolledBack = true;
|
||||
tab.txnAutoRolledBack = rollbackNoticeRequired;
|
||||
queryExecutionLog("info", "manual-txn:expired-recover", { traceId, elapsed: elapsed() });
|
||||
const refreshedSessionId = await ensureManualTransactionSession(id, executionDatabase, executionSchema, executionCatalog);
|
||||
if (tab.executionId !== executionId || tab.autoCommit !== false || manualTransactionTargetEpoch(tab) !== executionTargetEpoch) {
|
||||
@@ -7809,10 +7826,12 @@ export const useQueryStore = defineStore("query", () => {
|
||||
const idleTimeout = /5 minutes of inactivity/i.test(errMsg) || errMsg.includes("5 分钟无操作") || errMsg.includes("已自动回滚");
|
||||
if (idleTimeout) {
|
||||
// Backend session was removed and rolled back after idle expiry: clear
|
||||
// the sticky dirty state together with the session.
|
||||
// the sticky dirty state together with the session. Same rule as the
|
||||
// restart path: a session proven read-only reports no lost work.
|
||||
const rollbackNoticeRequired = manualTransactionRollbackNoticeRequired(tab, effectiveDatabaseTypeForConnection(useConnectionStore().getConfig(tab.connectionId)));
|
||||
clearTxnPossiblyDirty(tab);
|
||||
tab.txnSessionId = undefined;
|
||||
tab.txnAutoRolledBack = true;
|
||||
tab.txnAutoRolledBack = rollbackNoticeRequired;
|
||||
} 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.
|
||||
|
||||
Reference in new issue
Block a user