From e06db84e3d103ebb0b2496da6d667fe2da8b80f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Wed, 1 Apr 2026 14:51:31 +0700 Subject: [PATCH 1/2] fix: always reset isExecuting on query completion to prevent stuck tabs --- ...ainContentCoordinator+MultiStatement.swift | 43 +++++++++++++++++-- .../Views/Main/MainContentCoordinator.swift | 21 +++++++-- 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+MultiStatement.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+MultiStatement.swift index 865f34bf..1035bcb1 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+MultiStatement.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+MultiStatement.swift @@ -52,9 +52,28 @@ extension MainContentCoordinator { try await driver.beginTransaction() for (stmtIndex, sql) in statements.enumerated() { - guard !Task.isCancelled else { break } + guard !Task.isCancelled else { + try? await driver.rollbackTransaction() + await MainActor.run { [weak self] in + guard let self else { return } + if let idx = tabManager.tabs.firstIndex(where: { $0.id == tabId }) { + tabManager.tabs[idx].isExecuting = false + } + currentQueryTask = nil + toolbarState.setExecuting(false) + } + return + } guard capturedGeneration == queryGeneration else { try? await driver.rollbackTransaction() + await MainActor.run { [weak self] in + guard let self else { return } + if let idx = tabManager.tabs.firstIndex(where: { $0.id == tabId }) { + tabManager.tabs[idx].isExecuting = false + } + currentQueryTask = nil + toolbarState.setExecuting(false) + } return } @@ -123,7 +142,19 @@ extension MainContentCoordinator { try? await driver.rollbackTransaction() } - guard capturedGeneration == queryGeneration else { return } + // Always reset isExecuting even if generation is stale — + // skipping this leaves the tab permanently stuck in "executing" state. + if capturedGeneration != queryGeneration { + await MainActor.run { [weak self] in + guard let self else { return } + if let idx = tabManager.tabs.firstIndex(where: { $0.id == tabId }) { + tabManager.tabs[idx].isExecuting = false + } + currentQueryTask = nil + toolbarState.setExecuting(false) + } + return + } let failedStmtIndex = executedCount + 1 let contextMsg = "Statement \(failedStmtIndex)/\(totalCount) failed: " @@ -189,7 +220,13 @@ extension MainContentCoordinator { toolbarState.setExecuting(false) toolbarState.lastQueryDuration = cumulativeTime - guard capturedGeneration == queryGeneration else { return } + // Always reset isExecuting even if generation is stale + if capturedGeneration != queryGeneration { + if let idx = tabManager.tabs.firstIndex(where: { $0.id == tabId }) { + tabManager.tabs[idx].isExecuting = false + } + return + } guard let idx = tabManager.tabs.firstIndex(where: { $0.id == tabId }) else { return } diff --git a/TablePro/Views/Main/MainContentCoordinator.swift b/TablePro/Views/Main/MainContentCoordinator.swift index d7276c9e..1fe8b41e 100644 --- a/TablePro/Views/Main/MainContentCoordinator.swift +++ b/TablePro/Views/Main/MainContentCoordinator.swift @@ -911,6 +911,7 @@ final class MainContentCoordinator { if let idx = tabManager.tabs.firstIndex(where: { $0.id == tabId }) { tabManager.tabs[idx].isExecuting = false } + currentQueryTask = nil toolbarState.setExecuting(false) toolbarState.lastQueryDuration = safeExecutionTime } @@ -939,8 +940,13 @@ final class MainContentCoordinator { toolbarState.setExecuting(false) toolbarState.lastQueryDuration = safeExecutionTime - guard capturedGeneration == queryGeneration else { return } - guard !Task.isCancelled else { return } + // Always reset isExecuting even if generation is stale + if capturedGeneration != queryGeneration || Task.isCancelled { + if let idx = tabManager.tabs.firstIndex(where: { $0.id == tabId }) { + tabManager.tabs[idx].isExecuting = false + } + return + } applyPhase1Result( tabId: tabId, @@ -987,10 +993,17 @@ final class MainContentCoordinator { } } } catch { - guard capturedGeneration == queryGeneration else { return } - + // Always reset isExecuting even if generation is stale — + // skipping this leaves the tab permanently stuck in "executing" + // state, requiring a reconnect to recover. await MainActor.run { [weak self] in guard let self else { return } + if let idx = tabManager.tabs.firstIndex(where: { $0.id == tabId }) { + tabManager.tabs[idx].isExecuting = false + } + currentQueryTask = nil + toolbarState.setExecuting(false) + guard capturedGeneration == queryGeneration else { return } handleQueryExecutionError(error, sql: sql, tabId: tabId, connection: conn) } } From b7d9ee8bb35753bf0ab8011ec85b3917dc1073f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Wed, 1 Apr 2026 14:54:07 +0700 Subject: [PATCH 2/2] refactor: extract rollbackAndResetState helper to deduplicate cleanup --- ...ainContentCoordinator+MultiStatement.swift | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+MultiStatement.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+MultiStatement.swift index 1035bcb1..264c4438 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+MultiStatement.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+MultiStatement.swift @@ -51,29 +51,23 @@ extension MainContentCoordinator { // Wrap in a transaction for atomicity try await driver.beginTransaction() + /// Rollback transaction and reset executing state for early exits. + @MainActor func rollbackAndResetState() async { + try? await driver.rollbackTransaction() + if let idx = tabManager.tabs.firstIndex(where: { $0.id == tabId }) { + tabManager.tabs[idx].isExecuting = false + } + currentQueryTask = nil + toolbarState.setExecuting(false) + } + for (stmtIndex, sql) in statements.enumerated() { guard !Task.isCancelled else { - try? await driver.rollbackTransaction() - await MainActor.run { [weak self] in - guard let self else { return } - if let idx = tabManager.tabs.firstIndex(where: { $0.id == tabId }) { - tabManager.tabs[idx].isExecuting = false - } - currentQueryTask = nil - toolbarState.setExecuting(false) - } + await rollbackAndResetState() return } guard capturedGeneration == queryGeneration else { - try? await driver.rollbackTransaction() - await MainActor.run { [weak self] in - guard let self else { return } - if let idx = tabManager.tabs.firstIndex(where: { $0.id == tabId }) { - tabManager.tabs[idx].isExecuting = false - } - currentQueryTask = nil - toolbarState.setExecuting(false) - } + await rollbackAndResetState() return }