Skip to content

Commit 709c027

Browse files
authored
refactor(coordinator): sweep selectedTabIndex + bounds-check pattern across all extensions (#941)
* refactor(coordinator): sweep selectedTabIndex + bounds-check pattern across all extensions Migrates 12 files / 30+ call sites of the duplicated 'guard let tabIndex = tabManager.selectedTabIndex, tabIndex < tabManager.tabs.count' pattern to selectedTabAndIndex (write-back paths) or selectedTab (read-only paths). Both helpers were introduced in PR #939 and #940; this PR completes the migration outside Pagination and RowOperations. Files touched: - MainContentCoordinator.swift: runQuery, executeTableTabQueryDirectly, loadQueryIntoEditor, insertQueryFromAI, runExplainQuery, executeQueryInternal, handleSort, EXPLAIN error fallback - MainContentCoordinator+ClickHouse.swift: runVariantExplain - MainContentCoordinator+ColumnVisibility.swift: saveColumnVisibilityToTab - MainContentCoordinator+Discard.swift: handleDiscard (two sites) - MainContentCoordinator+ExecuteAll.swift: runAllStatements - MainContentCoordinator+Favorites.swift: insertFavorite, runFavoriteInNewTab - MainContentCoordinator+Filtering.swift: applyFilters, clearFiltersAndReload, restoreFiltersForTable - MainContentCoordinator+FKNavigation.swift: navigateToFKReference (four sites) - MainContentCoordinator+LoadMore.swift: loadMoreRows, fetchAllRows - MainContentCoordinator+Navigation.swift: openTableTab (four sites), promotePreviewTab - MainContentCoordinator+QueryParameters.swift: executeQueryWithParameters, executeQueryInternalParameterized, executeMultipleStatementsWithParameters - MainContentCoordinator+Refresh.swift: handleRefresh (three sites) - MainContentCommandActions.swift: formatQuery, toggleResults, previousResultTab, nextResultTab Behavior unchanged. Each migration preserves the original tab-not-selected and out-of-bounds short-circuits via selectedTabAndIndex's atomic check. Read-only paths use selectedTab (no fallback divergence — these don't use the index, so the strict-bounds requirement is moot). The dispatchParameterizedStatements / dispatchStatements helpers (called via tabIndex parameter, not selectedTabIndex) were left alone — they take the index from the caller's already-validated lookup. ExecuteAll's tabIndex parameter pattern preserved. Smoke-tested across query execution, EXPLAIN, table open from sidebar, FK navigation, filter apply/clear, refresh, discard dialog, pagination, format query, toggle results, insert favorite. Targeted tests pass. * refactor(coordinator): standardize selectedTabAndIndex destructuring as (tab, tabIndex)
1 parent cf97b96 commit 709c027

13 files changed

Lines changed: 86 additions & 109 deletions

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@ extension MainContentCoordinator {
2525
/// Run EXPLAIN with a specific variant (e.g. ClickHouse Plan/Pipeline/AST).
2626
/// Accepts the plugin-kit `ExplainVariant` type for generic dispatch.
2727
func runVariantExplain(_ variant: ExplainVariant) {
28-
guard let index = tabManager.selectedTabIndex else { return }
29-
guard !tabManager.tabs[index].execution.isExecuting else { return }
28+
guard let (tab, _) = tabManager.selectedTabAndIndex,
29+
!tab.execution.isExecuting else { return }
3030

31-
let fullQuery = tabManager.tabs[index].content.query
31+
let fullQuery = tab.content.query
3232

3333
let sql: String
34-
if tabManager.tabs[index].tabType == .table {
34+
if tab.tabType == .table {
3535
sql = fullQuery
3636
} else if let firstCursor = cursorPositions.first,
3737
firstCursor.range.length > 0 {
@@ -56,7 +56,7 @@ extension MainContentCoordinator {
5656
guard let stmt = statements.first else { return }
5757

5858
let explainSQL = "\(variant.sqlPrefix) \(stmt)"
59-
let tabId = tabManager.tabs[index].id
59+
let tabId = tab.id
6060

6161
Task {
6262
guard let driver = DatabaseManager.shared.driver(for: connectionId) else { return }

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import Foundation
88
extension MainContentCoordinator {
99
/// Save current hidden columns to the active tab's column layout
1010
func saveColumnVisibilityToTab() {
11-
guard let index = tabManager.selectedTabIndex else { return }
11+
guard let (_, index) = tabManager.selectedTabAndIndex else { return }
1212
tabManager.tabs[index].columnLayout.hiddenColumns = columnVisibilityManager.saveToColumnLayout()
1313
}
1414

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ extension MainContentCoordinator {
7272
) {
7373
let originalValues = changeManager.getOriginalValues()
7474
var deltas: [Delta] = []
75-
if let index = tabManager.selectedTabIndex {
76-
let tabId = tabManager.tabs[index].id
75+
if let (tab, _) = tabManager.selectedTabAndIndex {
76+
let tabId = tab.id
7777
let insertedIDs = collectInsertedRowIDs(
7878
tabId: tabId,
7979
indices: changeManager.insertedRowIndices
@@ -109,7 +109,7 @@ extension MainContentCoordinator {
109109
pendingDeletes.removeAll()
110110
changeManager.clearChangesAndUndoHistory()
111111

112-
if let index = tabManager.selectedTabIndex {
112+
if let (_, index) = tabManager.selectedTabAndIndex {
113113
tabManager.tabs[index].pendingChanges = TabChangeSnapshot()
114114
}
115115

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import Foundation
88

99
extension MainContentCoordinator {
1010
func runAllStatements() {
11-
guard let index = tabManager.selectedTabIndex else { return }
12-
guard !tabManager.tabs[index].execution.isExecuting else { return }
13-
guard tabManager.tabs[index].tabType == .query else { return }
11+
guard let (tab, index) = tabManager.selectedTabAndIndex,
12+
!tab.execution.isExecuting,
13+
tab.tabType == .query else { return }
1414

15-
let fullQuery = tabManager.tabs[index].content.query
15+
let fullQuery = tab.content.query
1616
guard !fullQuery.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
1717

1818
let statements = SQLStatementScanner.allStatements(in: fullQuery)

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

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,8 @@ extension MainContentCoordinator {
4646
current.tableContext.databaseName == currentDatabase,
4747
current.tableContext.schemaName == targetSchema {
4848
applyFKFilter(filter, for: referencedTable)
49-
// Persist so tab switch restore picks it up
50-
if let idx = tabManager.selectedTabIndex {
51-
tabManager.tabs[idx].filterState = filterStateManager.saveToTabState()
49+
if let (_, tabIndex) = tabManager.selectedTabAndIndex {
50+
tabManager.tabs[tabIndex].filterState = filterStateManager.saveToTabState()
5251
}
5352
return
5453
}
@@ -82,24 +81,19 @@ extension MainContentCoordinator {
8281
schemaName: targetSchema
8382
)
8483

85-
if needsQuery, let tabIndex = tabManager.selectedTabIndex {
86-
let tabId = tabManager.tabs[tabIndex].id
87-
setActiveTableRows(TableRows(), for: tabId)
84+
if needsQuery, let (tab, tabIndex) = tabManager.selectedTabAndIndex {
85+
setActiveTableRows(TableRows(), for: tab.id)
8886
tabManager.tabs[tabIndex].pagination.reset()
8987
}
9088

91-
// Update editable state for menu items
92-
if let tabIndex = tabManager.selectedTabIndex {
93-
let tab = tabManager.tabs[tabIndex]
89+
if let (tab, _) = tabManager.selectedTabAndIndex {
9490
toolbarState.isTableTab = tab.tabType == .table
9591
}
9692

9793
if needsQuery {
9894
NSApp.keyWindow?.title = referencedTable
9995

100-
// New tab — build filtered query directly, run once
101-
guard let tabIndex = tabManager.selectedTabIndex else { return }
102-
let tab = tabManager.tabs[tabIndex]
96+
guard let (tab, tabIndex) = tabManager.selectedTabAndIndex else { return }
10397
let tableRows = tableRowsStore.tableRows(for: tab.id)
10498
let filteredQuery = queryBuilder.buildFilteredQuery(
10599
tableName: referencedTable,
@@ -118,11 +112,8 @@ extension MainContentCoordinator {
118112

119113
runQuery()
120114
} else {
121-
// Reused tab already has data — apply filter (rebuilds query + re-runs)
122115
applyFKFilter(filter, for: referencedTable)
123-
124-
// Persist FK filter to reused tab
125-
if let tabIndex = tabManager.selectedTabIndex {
116+
if let (_, tabIndex) = tabManager.selectedTabAndIndex {
126117
tabManager.tabs[tabIndex].filterState = filterStateManager.saveToTabState()
127118
}
128119
}

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

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,9 @@ extension MainContentCoordinator {
1414
return
1515
}
1616

17-
if let tabIndex = tabManager.selectedTabIndex,
18-
tabManager.tabs[tabIndex].tabType == .query {
19-
let existing = tabManager.tabs[tabIndex].content.query
20-
.trimmingCharacters(in: .whitespacesAndNewlines)
17+
if let (tab, tabIndex) = tabManager.selectedTabAndIndex,
18+
tab.tabType == .query {
19+
let existing = tab.content.query.trimmingCharacters(in: .whitespacesAndNewlines)
2120
if existing.isEmpty {
2221
tabManager.tabs[tabIndex].content.query = favorite.query
2322
} else {
@@ -47,9 +46,9 @@ extension MainContentCoordinator {
4746
return
4847
}
4948

50-
if let tabIndex = tabManager.selectedTabIndex,
51-
tabManager.tabs[tabIndex].tabType == .query,
52-
tabManager.tabs[tabIndex].content.query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
49+
if let (tab, tabIndex) = tabManager.selectedTabAndIndex,
50+
tab.tabType == .query,
51+
tab.content.query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
5352
tabManager.tabs[tabIndex].content.query = favorite.query
5453
return
5554
}

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

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,8 @@ extension MainContentCoordinator {
1111
// MARK: - Filtering
1212

1313
func applyFilters(_ filters: [TableFilter]) {
14-
guard let tabIndex = tabManager.selectedTabIndex,
15-
tabIndex < tabManager.tabs.count,
16-
let tableName = tabManager.tabs[tabIndex].tableContext.tableName else { return }
14+
guard let (tab, tabIndex) = tabManager.selectedTabAndIndex,
15+
let tableName = tab.tableContext.tableName else { return }
1716

1817
let capturedTabIndex = tabIndex
1918
let capturedTableName = tableName
@@ -53,9 +52,8 @@ extension MainContentCoordinator {
5352
}
5453

5554
func clearFiltersAndReload() {
56-
guard let tabIndex = tabManager.selectedTabIndex,
57-
tabIndex < tabManager.tabs.count,
58-
let tableName = tabManager.tabs[tabIndex].tableContext.tableName else { return }
55+
guard let (tab, tabIndex) = tabManager.selectedTabAndIndex,
56+
let tableName = tab.tableContext.tableName else { return }
5957

6058
let capturedTabIndex = tabIndex
6159
let capturedTableName = tableName
@@ -83,10 +81,10 @@ extension MainContentCoordinator {
8381

8482
func restoreFiltersForTable(_ tableName: String) {
8583
filterStateManager.restoreLastFilters(for: tableName)
86-
guard let idx = tabManager.selectedTabIndex else { return }
87-
tabManager.tabs[idx].filterState = filterStateManager.saveToTabState()
84+
guard let (_, tabIndex) = tabManager.selectedTabAndIndex else { return }
85+
tabManager.tabs[tabIndex].filterState = filterStateManager.saveToTabState()
8886
if filterStateManager.hasAppliedFilters {
89-
rebuildTableQuery(at: idx)
87+
rebuildTableQuery(at: tabIndex)
9088
}
9189
}
9290

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

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,8 @@ extension MainContentCoordinator {
3535
// MARK: - Load More Rows
3636

3737
func loadMoreRows() {
38-
guard let idx = tabManager.selectedTabIndex else { return }
39-
let tab = tabManager.tabs[idx]
40-
guard !tab.pagination.isLoadingMore,
38+
guard let (tab, tabIndex) = tabManager.selectedTabAndIndex,
39+
!tab.pagination.isLoadingMore,
4140
!tab.execution.isExecuting,
4241
tab.pagination.hasMoreRows,
4342
let baseQuery = tab.pagination.baseQueryForMore else { return }
@@ -48,7 +47,7 @@ extension MainContentCoordinator {
4847
let capturedGeneration = queryGeneration
4948
let storedParamValues = tab.pagination.baseQueryParameterValues
5049

51-
tabManager.tabs[idx].pagination.isLoadingMore = true
50+
tabManager.tabs[tabIndex].pagination.isLoadingMore = true
5251
toolbarState.setExecuting(true)
5352

5453
currentQueryTask = Task { [weak self] in
@@ -134,9 +133,8 @@ extension MainContentCoordinator {
134133
// MARK: - Fetch All Rows
135134

136135
func fetchAllRows() {
137-
guard let idx = tabManager.selectedTabIndex else { return }
138-
let tab = tabManager.tabs[idx]
139-
guard !tab.pagination.isLoadingMore,
136+
guard let (tab, _) = tabManager.selectedTabAndIndex,
137+
!tab.pagination.isLoadingMore,
140138
!tab.execution.isExecuting,
141139
tab.pagination.hasMoreRows,
142140
let baseQuery = tab.pagination.baseQueryForMore else { return }

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

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ extension MainContentCoordinator {
4141
current.tabType == .table,
4242
current.tableContext.tableName == tableName,
4343
current.tableContext.databaseName == currentDatabase {
44-
if showStructure, let idx = tabManager.selectedTabIndex {
45-
tabManager.tabs[idx].display.resultsViewMode = .structure
44+
if showStructure, let (_, tabIndex) = tabManager.selectedTabAndIndex {
45+
tabManager.tabs[tabIndex].display.resultsViewMode = .structure
4646
}
4747
return
4848
}
@@ -91,7 +91,7 @@ extension MainContentCoordinator {
9191
databaseName: currentDatabase
9292
)
9393
}
94-
if let tabIndex = tabManager.selectedTabIndex {
94+
if let (_, tabIndex) = tabManager.selectedTabAndIndex {
9595
tabManager.tabs[tabIndex].tableContext.isView = isView
9696
tabManager.tabs[tabIndex].tableContext.isEditable = !isView
9797
tabManager.tabs[tabIndex].tableContext.schemaName = currentSchema
@@ -123,9 +123,8 @@ extension MainContentCoordinator {
123123
schemaName: currentSchema
124124
) {
125125
filterStateManager.clearAll()
126-
if let tabIndex = tabManager.selectedTabIndex {
127-
let tabId = tabManager.tabs[tabIndex].id
128-
setActiveTableRows(TableRows(), for: tabId)
126+
if let (tab, tabIndex) = tabManager.selectedTabAndIndex {
127+
setActiveTableRows(TableRows(), for: tab.id)
129128
tabManager.tabs[tabIndex].pagination.reset()
130129
toolbarState.isTableTab = true
131130
}
@@ -277,9 +276,8 @@ extension MainContentCoordinator {
277276
isPreview: true
278277
)
279278
filterStateManager.clearAll()
280-
if let tabIndex = tabManager.selectedTabIndex {
281-
let tabId = tabManager.tabs[tabIndex].id
282-
setActiveTableRows(TableRows(), for: tabId)
279+
if let (tab, tabIndex) = tabManager.selectedTabAndIndex {
280+
setActiveTableRows(TableRows(), for: tab.id)
283281
tabManager.tabs[tabIndex].display.resultsViewMode = showStructure ? .structure : .data
284282
tabManager.tabs[tabIndex].pagination.reset()
285283
toolbarState.isTableTab = true
@@ -305,8 +303,8 @@ extension MainContentCoordinator {
305303
}
306304

307305
func promotePreviewTab() {
308-
guard let tabIndex = tabManager.selectedTabIndex,
309-
tabManager.tabs[tabIndex].isPreview else { return }
306+
guard let (tab, tabIndex) = tabManager.selectedTabAndIndex,
307+
tab.isPreview else { return }
310308
tabManager.tabs[tabIndex].isPreview = false
311309

312310
if let wid = windowId {

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ extension MainContentCoordinator {
2323
}
2424

2525
func executeQueryWithParameters(_ sql: String, parameters: [QueryParameter]) {
26-
guard let index = tabManager.selectedTabIndex else { return }
26+
guard let (_, index) = tabManager.selectedTabAndIndex else { return }
2727

2828
let missing = parameters.filter {
2929
!$0.isNull && $0.value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
@@ -59,8 +59,8 @@ extension MainContentCoordinator {
5959
parameters: [Any?],
6060
originalParameters: [QueryParameter]
6161
) {
62-
guard let index = tabManager.selectedTabIndex else { return }
63-
guard !tabManager.tabs[index].execution.isExecuting else { return }
62+
guard let (selectedTab, index) = tabManager.selectedTabAndIndex,
63+
!selectedTab.execution.isExecuting else { return }
6464

6565
if currentQueryTask != nil {
6666
currentQueryTask?.cancel()
@@ -224,8 +224,8 @@ extension MainContentCoordinator {
224224
}
225225

226226
func executeMultipleStatementsWithParameters(_ statements: [String], parameters: [QueryParameter]) {
227-
guard let index = tabManager.selectedTabIndex else { return }
228-
guard !tabManager.tabs[index].execution.isExecuting else { return }
227+
guard let (selectedTab, index) = tabManager.selectedTabAndIndex,
228+
!selectedTab.execution.isExecuting else { return }
229229

230230
let missing = parameters.filter {
231231
!$0.isNull && $0.value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty

0 commit comments

Comments
 (0)