Skip to content

Commit 732a250

Browse files
authored
fix: always reset isExecuting on query completion to prevent stuck tabs (#548)
* fix: always reset isExecuting on query completion to prevent stuck tabs * refactor: extract rollbackAndResetState helper to deduplicate cleanup
1 parent 51a1775 commit 732a250

File tree

2 files changed

+52
-8
lines changed

2 files changed

+52
-8
lines changed

TablePro/Views/Main/Extensions/MainContentCoordinator+MultiStatement.swift

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,23 @@ extension MainContentCoordinator {
5151
// Wrap in a transaction for atomicity
5252
try await driver.beginTransaction()
5353

54+
/// Rollback transaction and reset executing state for early exits.
55+
@MainActor func rollbackAndResetState() async {
56+
try? await driver.rollbackTransaction()
57+
if let idx = tabManager.tabs.firstIndex(where: { $0.id == tabId }) {
58+
tabManager.tabs[idx].isExecuting = false
59+
}
60+
currentQueryTask = nil
61+
toolbarState.setExecuting(false)
62+
}
63+
5464
for (stmtIndex, sql) in statements.enumerated() {
55-
guard !Task.isCancelled else { break }
65+
guard !Task.isCancelled else {
66+
await rollbackAndResetState()
67+
return
68+
}
5669
guard capturedGeneration == queryGeneration else {
57-
try? await driver.rollbackTransaction()
70+
await rollbackAndResetState()
5871
return
5972
}
6073

@@ -123,7 +136,19 @@ extension MainContentCoordinator {
123136
try? await driver.rollbackTransaction()
124137
}
125138

126-
guard capturedGeneration == queryGeneration else { return }
139+
// Always reset isExecuting even if generation is stale —
140+
// skipping this leaves the tab permanently stuck in "executing" state.
141+
if capturedGeneration != queryGeneration {
142+
await MainActor.run { [weak self] in
143+
guard let self else { return }
144+
if let idx = tabManager.tabs.firstIndex(where: { $0.id == tabId }) {
145+
tabManager.tabs[idx].isExecuting = false
146+
}
147+
currentQueryTask = nil
148+
toolbarState.setExecuting(false)
149+
}
150+
return
151+
}
127152

128153
let failedStmtIndex = executedCount + 1
129154
let contextMsg = "Statement \(failedStmtIndex)/\(totalCount) failed: "
@@ -189,7 +214,13 @@ extension MainContentCoordinator {
189214
toolbarState.setExecuting(false)
190215
toolbarState.lastQueryDuration = cumulativeTime
191216

192-
guard capturedGeneration == queryGeneration else { return }
217+
// Always reset isExecuting even if generation is stale
218+
if capturedGeneration != queryGeneration {
219+
if let idx = tabManager.tabs.firstIndex(where: { $0.id == tabId }) {
220+
tabManager.tabs[idx].isExecuting = false
221+
}
222+
return
223+
}
193224
guard let idx = tabManager.tabs.firstIndex(where: { $0.id == tabId }) else {
194225
return
195226
}

TablePro/Views/Main/MainContentCoordinator.swift

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -911,6 +911,7 @@ final class MainContentCoordinator {
911911
if let idx = tabManager.tabs.firstIndex(where: { $0.id == tabId }) {
912912
tabManager.tabs[idx].isExecuting = false
913913
}
914+
currentQueryTask = nil
914915
toolbarState.setExecuting(false)
915916
toolbarState.lastQueryDuration = safeExecutionTime
916917
}
@@ -939,8 +940,13 @@ final class MainContentCoordinator {
939940
toolbarState.setExecuting(false)
940941
toolbarState.lastQueryDuration = safeExecutionTime
941942

942-
guard capturedGeneration == queryGeneration else { return }
943-
guard !Task.isCancelled else { return }
943+
// Always reset isExecuting even if generation is stale
944+
if capturedGeneration != queryGeneration || Task.isCancelled {
945+
if let idx = tabManager.tabs.firstIndex(where: { $0.id == tabId }) {
946+
tabManager.tabs[idx].isExecuting = false
947+
}
948+
return
949+
}
944950

945951
applyPhase1Result(
946952
tabId: tabId,
@@ -987,10 +993,17 @@ final class MainContentCoordinator {
987993
}
988994
}
989995
} catch {
990-
guard capturedGeneration == queryGeneration else { return }
991-
996+
// Always reset isExecuting even if generation is stale —
997+
// skipping this leaves the tab permanently stuck in "executing"
998+
// state, requiring a reconnect to recover.
992999
await MainActor.run { [weak self] in
9931000
guard let self else { return }
1001+
if let idx = tabManager.tabs.firstIndex(where: { $0.id == tabId }) {
1002+
tabManager.tabs[idx].isExecuting = false
1003+
}
1004+
currentQueryTask = nil
1005+
toolbarState.setExecuting(false)
1006+
guard capturedGeneration == queryGeneration else { return }
9941007
handleQueryExecutionError(error, sql: sql, tabId: tabId, connection: conn)
9951008
}
9961009
}

0 commit comments

Comments
 (0)