diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+MultiStatement.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+MultiStatement.swift index 865f34bf..264c4438 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+MultiStatement.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+MultiStatement.swift @@ -51,10 +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 { break } + guard !Task.isCancelled else { + await rollbackAndResetState() + return + } guard capturedGeneration == queryGeneration else { - try? await driver.rollbackTransaction() + await rollbackAndResetState() return } @@ -123,7 +136,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 +214,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) } }