From 5605f37702ad5421402a58fa00a1895770ed4a48 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, 29 Apr 2026 13:14:14 +0700 Subject: [PATCH 1/2] refactor(coordinator): sweep selectedTabIndex + bounds-check pattern across all extensions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../MainContentCoordinator+ClickHouse.swift | 10 ++-- ...nContentCoordinator+ColumnVisibility.swift | 2 +- .../MainContentCoordinator+Discard.swift | 6 +-- .../MainContentCoordinator+ExecuteAll.swift | 8 ++-- .../MainContentCoordinator+FKNavigation.swift | 21 +++------ .../MainContentCoordinator+Favorites.swift | 13 +++--- .../MainContentCoordinator+Filtering.swift | 12 ++--- .../MainContentCoordinator+LoadMore.swift | 10 ++-- .../MainContentCoordinator+Navigation.swift | 18 ++++---- ...inContentCoordinator+QueryParameters.swift | 10 ++-- .../MainContentCoordinator+Refresh.swift | 15 +++--- .../Main/MainContentCommandActions.swift | 14 +++--- .../Views/Main/MainContentCoordinator.swift | 46 +++++++++---------- 13 files changed, 81 insertions(+), 104 deletions(-) diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+ClickHouse.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+ClickHouse.swift index 0c933447c..57e744549 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+ClickHouse.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+ClickHouse.swift @@ -25,13 +25,13 @@ extension MainContentCoordinator { /// Run EXPLAIN with a specific variant (e.g. ClickHouse Plan/Pipeline/AST). /// Accepts the plugin-kit `ExplainVariant` type for generic dispatch. func runVariantExplain(_ variant: ExplainVariant) { - guard let index = tabManager.selectedTabIndex else { return } - guard !tabManager.tabs[index].execution.isExecuting else { return } + guard let (tab, _) = tabManager.selectedTabAndIndex, + !tab.execution.isExecuting else { return } - let fullQuery = tabManager.tabs[index].content.query + let fullQuery = tab.content.query let sql: String - if tabManager.tabs[index].tabType == .table { + if tab.tabType == .table { sql = fullQuery } else if let firstCursor = cursorPositions.first, firstCursor.range.length > 0 { @@ -56,7 +56,7 @@ extension MainContentCoordinator { guard let stmt = statements.first else { return } let explainSQL = "\(variant.sqlPrefix) \(stmt)" - let tabId = tabManager.tabs[index].id + let tabId = tab.id Task { guard let driver = DatabaseManager.shared.driver(for: connectionId) else { return } diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+ColumnVisibility.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+ColumnVisibility.swift index 01f7b42ea..fc297a137 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+ColumnVisibility.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+ColumnVisibility.swift @@ -8,7 +8,7 @@ import Foundation extension MainContentCoordinator { /// Save current hidden columns to the active tab's column layout func saveColumnVisibilityToTab() { - guard let index = tabManager.selectedTabIndex else { return } + guard let (_, index) = tabManager.selectedTabAndIndex else { return } tabManager.tabs[index].columnLayout.hiddenColumns = columnVisibilityManager.saveToColumnLayout() } diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+Discard.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+Discard.swift index bc659cc49..e93fb46ab 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+Discard.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+Discard.swift @@ -72,8 +72,8 @@ extension MainContentCoordinator { ) { let originalValues = changeManager.getOriginalValues() var deltas: [Delta] = [] - if let index = tabManager.selectedTabIndex { - let tabId = tabManager.tabs[index].id + if let (tab, _) = tabManager.selectedTabAndIndex { + let tabId = tab.id let insertedIDs = collectInsertedRowIDs( tabId: tabId, indices: changeManager.insertedRowIndices @@ -109,7 +109,7 @@ extension MainContentCoordinator { pendingDeletes.removeAll() changeManager.clearChangesAndUndoHistory() - if let index = tabManager.selectedTabIndex { + if let (_, index) = tabManager.selectedTabAndIndex { tabManager.tabs[index].pendingChanges = TabChangeSnapshot() } diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+ExecuteAll.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+ExecuteAll.swift index 790452cc3..6a0fed944 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+ExecuteAll.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+ExecuteAll.swift @@ -8,11 +8,11 @@ import Foundation extension MainContentCoordinator { func runAllStatements() { - guard let index = tabManager.selectedTabIndex else { return } - guard !tabManager.tabs[index].execution.isExecuting else { return } - guard tabManager.tabs[index].tabType == .query else { return } + guard let (tab, index) = tabManager.selectedTabAndIndex, + !tab.execution.isExecuting, + tab.tabType == .query else { return } - let fullQuery = tabManager.tabs[index].content.query + let fullQuery = tab.content.query guard !fullQuery.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return } let statements = SQLStatementScanner.allStatements(in: fullQuery) diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+FKNavigation.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+FKNavigation.swift index 853c6ee44..416e49824 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+FKNavigation.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+FKNavigation.swift @@ -46,8 +46,7 @@ extension MainContentCoordinator { current.tableContext.databaseName == currentDatabase, current.tableContext.schemaName == targetSchema { applyFKFilter(filter, for: referencedTable) - // Persist so tab switch restore picks it up - if let idx = tabManager.selectedTabIndex { + if let (_, idx) = tabManager.selectedTabAndIndex { tabManager.tabs[idx].filterState = filterStateManager.saveToTabState() } return @@ -82,24 +81,19 @@ extension MainContentCoordinator { schemaName: targetSchema ) - if needsQuery, let tabIndex = tabManager.selectedTabIndex { - let tabId = tabManager.tabs[tabIndex].id - setActiveTableRows(TableRows(), for: tabId) + if needsQuery, let (tab, tabIndex) = tabManager.selectedTabAndIndex { + setActiveTableRows(TableRows(), for: tab.id) tabManager.tabs[tabIndex].pagination.reset() } - // Update editable state for menu items - if let tabIndex = tabManager.selectedTabIndex { - let tab = tabManager.tabs[tabIndex] + if let (tab, _) = tabManager.selectedTabAndIndex { toolbarState.isTableTab = tab.tabType == .table } if needsQuery { NSApp.keyWindow?.title = referencedTable - // New tab — build filtered query directly, run once - guard let tabIndex = tabManager.selectedTabIndex else { return } - let tab = tabManager.tabs[tabIndex] + guard let (tab, tabIndex) = tabManager.selectedTabAndIndex else { return } let tableRows = tableRowsStore.tableRows(for: tab.id) let filteredQuery = queryBuilder.buildFilteredQuery( tableName: referencedTable, @@ -118,11 +112,8 @@ extension MainContentCoordinator { runQuery() } else { - // Reused tab already has data — apply filter (rebuilds query + re-runs) applyFKFilter(filter, for: referencedTable) - - // Persist FK filter to reused tab - if let tabIndex = tabManager.selectedTabIndex { + if let (_, tabIndex) = tabManager.selectedTabAndIndex { tabManager.tabs[tabIndex].filterState = filterStateManager.saveToTabState() } } diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+Favorites.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+Favorites.swift index 9239f92d9..fc16a48e0 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+Favorites.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+Favorites.swift @@ -14,10 +14,9 @@ extension MainContentCoordinator { return } - if let tabIndex = tabManager.selectedTabIndex, - tabManager.tabs[tabIndex].tabType == .query { - let existing = tabManager.tabs[tabIndex].content.query - .trimmingCharacters(in: .whitespacesAndNewlines) + if let (tab, tabIndex) = tabManager.selectedTabAndIndex, + tab.tabType == .query { + let existing = tab.content.query.trimmingCharacters(in: .whitespacesAndNewlines) if existing.isEmpty { tabManager.tabs[tabIndex].content.query = favorite.query } else { @@ -47,9 +46,9 @@ extension MainContentCoordinator { return } - if let tabIndex = tabManager.selectedTabIndex, - tabManager.tabs[tabIndex].tabType == .query, - tabManager.tabs[tabIndex].content.query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + if let (tab, tabIndex) = tabManager.selectedTabAndIndex, + tab.tabType == .query, + tab.content.query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { tabManager.tabs[tabIndex].content.query = favorite.query return } diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+Filtering.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+Filtering.swift index 13db7f2f1..35e362b16 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+Filtering.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+Filtering.swift @@ -11,9 +11,8 @@ extension MainContentCoordinator { // MARK: - Filtering func applyFilters(_ filters: [TableFilter]) { - guard let tabIndex = tabManager.selectedTabIndex, - tabIndex < tabManager.tabs.count, - let tableName = tabManager.tabs[tabIndex].tableContext.tableName else { return } + guard let (tab, tabIndex) = tabManager.selectedTabAndIndex, + let tableName = tab.tableContext.tableName else { return } let capturedTabIndex = tabIndex let capturedTableName = tableName @@ -53,9 +52,8 @@ extension MainContentCoordinator { } func clearFiltersAndReload() { - guard let tabIndex = tabManager.selectedTabIndex, - tabIndex < tabManager.tabs.count, - let tableName = tabManager.tabs[tabIndex].tableContext.tableName else { return } + guard let (tab, tabIndex) = tabManager.selectedTabAndIndex, + let tableName = tab.tableContext.tableName else { return } let capturedTabIndex = tabIndex let capturedTableName = tableName @@ -83,7 +81,7 @@ extension MainContentCoordinator { func restoreFiltersForTable(_ tableName: String) { filterStateManager.restoreLastFilters(for: tableName) - guard let idx = tabManager.selectedTabIndex else { return } + guard let (_, idx) = tabManager.selectedTabAndIndex else { return } tabManager.tabs[idx].filterState = filterStateManager.saveToTabState() if filterStateManager.hasAppliedFilters { rebuildTableQuery(at: idx) diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+LoadMore.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+LoadMore.swift index 2f92cfc48..42adf5b3f 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+LoadMore.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+LoadMore.swift @@ -35,9 +35,8 @@ extension MainContentCoordinator { // MARK: - Load More Rows func loadMoreRows() { - guard let idx = tabManager.selectedTabIndex else { return } - let tab = tabManager.tabs[idx] - guard !tab.pagination.isLoadingMore, + guard let (tab, idx) = tabManager.selectedTabAndIndex, + !tab.pagination.isLoadingMore, !tab.execution.isExecuting, tab.pagination.hasMoreRows, let baseQuery = tab.pagination.baseQueryForMore else { return } @@ -134,9 +133,8 @@ extension MainContentCoordinator { // MARK: - Fetch All Rows func fetchAllRows() { - guard let idx = tabManager.selectedTabIndex else { return } - let tab = tabManager.tabs[idx] - guard !tab.pagination.isLoadingMore, + guard let (tab, idx) = tabManager.selectedTabAndIndex, + !tab.pagination.isLoadingMore, !tab.execution.isExecuting, tab.pagination.hasMoreRows, let baseQuery = tab.pagination.baseQueryForMore else { return } diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift index efec19dd3..4e2572bc5 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift @@ -41,7 +41,7 @@ extension MainContentCoordinator { current.tabType == .table, current.tableContext.tableName == tableName, current.tableContext.databaseName == currentDatabase { - if showStructure, let idx = tabManager.selectedTabIndex { + if showStructure, let (_, idx) = tabManager.selectedTabAndIndex { tabManager.tabs[idx].display.resultsViewMode = .structure } return @@ -91,7 +91,7 @@ extension MainContentCoordinator { databaseName: currentDatabase ) } - if let tabIndex = tabManager.selectedTabIndex { + if let (_, tabIndex) = tabManager.selectedTabAndIndex { tabManager.tabs[tabIndex].tableContext.isView = isView tabManager.tabs[tabIndex].tableContext.isEditable = !isView tabManager.tabs[tabIndex].tableContext.schemaName = currentSchema @@ -123,9 +123,8 @@ extension MainContentCoordinator { schemaName: currentSchema ) { filterStateManager.clearAll() - if let tabIndex = tabManager.selectedTabIndex { - let tabId = tabManager.tabs[tabIndex].id - setActiveTableRows(TableRows(), for: tabId) + if let (tab, tabIndex) = tabManager.selectedTabAndIndex { + setActiveTableRows(TableRows(), for: tab.id) tabManager.tabs[tabIndex].pagination.reset() toolbarState.isTableTab = true } @@ -277,9 +276,8 @@ extension MainContentCoordinator { isPreview: true ) filterStateManager.clearAll() - if let tabIndex = tabManager.selectedTabIndex { - let tabId = tabManager.tabs[tabIndex].id - setActiveTableRows(TableRows(), for: tabId) + if let (tab, tabIndex) = tabManager.selectedTabAndIndex { + setActiveTableRows(TableRows(), for: tab.id) tabManager.tabs[tabIndex].display.resultsViewMode = showStructure ? .structure : .data tabManager.tabs[tabIndex].pagination.reset() toolbarState.isTableTab = true @@ -305,8 +303,8 @@ extension MainContentCoordinator { } func promotePreviewTab() { - guard let tabIndex = tabManager.selectedTabIndex, - tabManager.tabs[tabIndex].isPreview else { return } + guard let (tab, tabIndex) = tabManager.selectedTabAndIndex, + tab.isPreview else { return } tabManager.tabs[tabIndex].isPreview = false if let wid = windowId { diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+QueryParameters.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+QueryParameters.swift index 256f3680e..8cde0a0b6 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+QueryParameters.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+QueryParameters.swift @@ -23,7 +23,7 @@ extension MainContentCoordinator { } func executeQueryWithParameters(_ sql: String, parameters: [QueryParameter]) { - guard let index = tabManager.selectedTabIndex else { return } + guard let (_, index) = tabManager.selectedTabAndIndex else { return } let missing = parameters.filter { !$0.isNull && $0.value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty @@ -59,8 +59,8 @@ extension MainContentCoordinator { parameters: [Any?], originalParameters: [QueryParameter] ) { - guard let index = tabManager.selectedTabIndex else { return } - guard !tabManager.tabs[index].execution.isExecuting else { return } + guard let (selectedTab, index) = tabManager.selectedTabAndIndex, + !selectedTab.execution.isExecuting else { return } if currentQueryTask != nil { currentQueryTask?.cancel() @@ -224,8 +224,8 @@ extension MainContentCoordinator { } func executeMultipleStatementsWithParameters(_ statements: [String], parameters: [QueryParameter]) { - guard let index = tabManager.selectedTabIndex else { return } - guard !tabManager.tabs[index].execution.isExecuting else { return } + guard let (selectedTab, index) = tabManager.selectedTabAndIndex, + !selectedTab.execution.isExecuting else { return } let missing = parameters.filter { !$0.isNull && $0.value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+Refresh.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+Refresh.swift index 8e8386635..4abdbcd70 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+Refresh.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+Refresh.swift @@ -16,8 +16,8 @@ extension MainContentCoordinator { onDiscard: @escaping () -> Void ) { // If showing structure view, let it handle refresh notifications - if let tabIndex = tabManager.selectedTabIndex, - tabManager.tabs[tabIndex].display.resultsViewMode == .structure { + if let (tab, _) = tabManager.selectedTabAndIndex, + tab.display.resultsViewMode == .structure { return } @@ -30,10 +30,9 @@ extension MainContentCoordinator { if confirmed { onDiscard() changeManager.clearChangesAndUndoHistory() - // Only execute query if we're in a table tab // Query tabs should not auto-execute on refresh (use Cmd+Enter to execute) - if let tabIndex = tabManager.selectedTabIndex, - tabManager.tabs[tabIndex].tabType == .table { + if let (tab, tabIndex) = tabManager.selectedTabAndIndex, + tab.tabType == .table { currentQueryTask?.cancel() rebuildTableQuery(at: tabIndex) runQuery() @@ -41,10 +40,8 @@ extension MainContentCoordinator { } } } else { - // Only execute query if we're in a table tab - // Query tabs should not auto-execute on refresh (use Cmd+Enter to execute) - if let tabIndex = tabManager.selectedTabIndex, - tabManager.tabs[tabIndex].tabType == .table { + if let (tab, tabIndex) = tabManager.selectedTabAndIndex, + tab.tabType == .table { currentQueryTask?.cancel() rebuildTableQuery(at: tabIndex) runQuery() diff --git a/TablePro/Views/Main/MainContentCommandActions.swift b/TablePro/Views/Main/MainContentCommandActions.swift index 8b0b19727..694bbf9d6 100644 --- a/TablePro/Views/Main/MainContentCommandActions.swift +++ b/TablePro/Views/Main/MainContentCommandActions.swift @@ -632,8 +632,7 @@ final class MainContentCommandActions { func formatQuery() { guard let coordinator, - let tabIndex = coordinator.tabManager.selectedTabIndex else { return } - let tab = coordinator.tabManager.tabs[tabIndex] + let (tab, tabIndex) = coordinator.tabManager.selectedTabAndIndex else { return } let dbType = connection.type let formatter = SQLFormatterService() let options = SQLFormatterOptions.default @@ -662,14 +661,15 @@ final class MainContentCommandActions { } func toggleResults() { - guard let coordinator, let tabIndex = coordinator.tabManager.selectedTabIndex else { return } + guard let coordinator, + let (_, tabIndex) = coordinator.tabManager.selectedTabAndIndex else { return } coordinator.tabManager.tabs[tabIndex].display.isResultsCollapsed.toggle() coordinator.toolbarState.isResultsCollapsed = coordinator.tabManager.tabs[tabIndex].display.isResultsCollapsed } func previousResultTab() { - guard let coordinator, let tabIndex = coordinator.tabManager.selectedTabIndex else { return } - let tab = coordinator.tabManager.tabs[tabIndex] + guard let coordinator, + let (tab, _) = coordinator.tabManager.selectedTabAndIndex else { return } guard tab.display.resultSets.count > 1, let currentId = tab.display.activeResultSetId ?? tab.display.resultSets.last?.id, let currentIndex = tab.display.resultSets.firstIndex(where: { $0.id == currentId }), @@ -678,8 +678,8 @@ final class MainContentCommandActions { } func nextResultTab() { - guard let coordinator, let tabIndex = coordinator.tabManager.selectedTabIndex else { return } - let tab = coordinator.tabManager.tabs[tabIndex] + guard let coordinator, + let (tab, _) = coordinator.tabManager.selectedTabAndIndex else { return } guard tab.display.resultSets.count > 1, let currentId = tab.display.activeResultSetId ?? tab.display.resultSets.last?.id, let currentIndex = tab.display.resultSets.firstIndex(where: { $0.id == currentId }), diff --git a/TablePro/Views/Main/MainContentCoordinator.swift b/TablePro/Views/Main/MainContentCoordinator.swift index 5d7db8ed6..687909dc9 100644 --- a/TablePro/Views/Main/MainContentCoordinator.swift +++ b/TablePro/Views/Main/MainContentCoordinator.swift @@ -722,13 +722,13 @@ final class MainContentCoordinator { // MARK: - Query Execution func runQuery() { - guard let index = tabManager.selectedTabIndex else { return } - guard !tabManager.tabs[index].execution.isExecuting else { return } + guard let (tab, index) = tabManager.selectedTabAndIndex, + !tab.execution.isExecuting else { return } - let fullQuery = tabManager.tabs[index].content.query + let fullQuery = tab.content.query let sql: String - if tabManager.tabs[index].tabType == .table { + if tab.tabType == .table { sql = fullQuery } else if let firstCursor = cursorPositions.first, firstCursor.range.length > 0 { @@ -790,15 +790,15 @@ final class MainContentCoordinator { /// Table tab queries are always app-generated SELECTs, so they skip dangerous-query /// checks but still respect safe mode levels that apply to all queries. func executeTableTabQueryDirectly() { - guard let index = tabManager.selectedTabIndex else { return } - guard !tabManager.tabs[index].execution.isExecuting else { return } + guard let (tab, index) = tabManager.selectedTabAndIndex, + !tab.execution.isExecuting else { return } - let sql = tabManager.tabs[index].content.query + let sql = tab.content.query guard !sql.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return } let level = safeModeLevel if level.appliesToAllQueries && level.requiresConfirmation, - tabManager.tabs[index].execution.lastExecutedAt == nil + tab.execution.lastExecutedAt == nil { guard !isShowingSafeModePrompt else { return } isShowingSafeModePrompt = true @@ -830,9 +830,8 @@ final class MainContentCoordinator { // MARK: - Editor Query Loading func loadQueryIntoEditor(_ query: String) { - if let tabIndex = tabManager.selectedTabIndex, - tabIndex < tabManager.tabs.count, - tabManager.tabs[tabIndex].tabType == .query { + if let (tab, tabIndex) = tabManager.selectedTabAndIndex, + tab.tabType == .query { tabManager.tabs[tabIndex].content.query = query tabManager.tabs[tabIndex].hasUserInteraction = true } else { @@ -846,10 +845,9 @@ final class MainContentCoordinator { } func insertQueryFromAI(_ query: String) { - if let tabIndex = tabManager.selectedTabIndex, - tabIndex < tabManager.tabs.count, - tabManager.tabs[tabIndex].tabType == .query { - let existingQuery = tabManager.tabs[tabIndex].content.query + if let (tab, tabIndex) = tabManager.selectedTabAndIndex, + tab.tabType == .query { + let existingQuery = tab.content.query if existingQuery.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { tabManager.tabs[tabIndex].content.query = query } else { @@ -870,13 +868,13 @@ final class MainContentCoordinator { /// Run EXPLAIN on the current query (database-type-aware prefix) func runExplainQuery() { - guard let index = tabManager.selectedTabIndex else { return } - guard !tabManager.tabs[index].execution.isExecuting else { return } + guard let (tab, _) = tabManager.selectedTabAndIndex, + !tab.execution.isExecuting else { return } - let fullQuery = tabManager.tabs[index].content.query + let fullQuery = tab.content.query let sql: String - if tabManager.tabs[index].tabType == .table { + if tab.tabType == .table { sql = fullQuery } else if let firstCursor = cursorPositions.first, firstCursor.range.length > 0 { @@ -933,7 +931,7 @@ final class MainContentCoordinator { guard let adapter = DatabaseManager.shared.driver(for: connectionId) as? PluginDriverAdapter, let explainSQL = adapter.buildExplainQuery(stmt) else { - if let index = tabManager.selectedTabIndex { + if let (_, index) = tabManager.selectedTabAndIndex { tabManager.tabs[index].execution.errorMessage = String(localized: "EXPLAIN is not supported for this database type.") } return @@ -965,8 +963,8 @@ final class MainContentCoordinator { internal func executeQueryInternal( _ sql: String ) { - guard let index = tabManager.selectedTabIndex else { return } - guard !tabManager.tabs[index].execution.isExecuting else { return } + guard let (selectedTab, index) = tabManager.selectedTabAndIndex, + !selectedTab.execution.isExecuting else { return } if currentQueryTask != nil { currentQueryTask?.cancel() @@ -1302,10 +1300,8 @@ final class MainContentCoordinator { // MARK: - Sorting func handleSort(columnIndex: Int, ascending: Bool, isMultiSort: Bool = false) { - guard let tabIndex = tabManager.selectedTabIndex, - tabIndex < tabManager.tabs.count else { return } + guard let (tab, tabIndex) = tabManager.selectedTabAndIndex else { return } - let tab = tabManager.tabs[tabIndex] let tableRows = tableRowsStore.tableRows(for: tab.id) guard columnIndex >= 0 && columnIndex < tableRows.columns.count else { return } From 3a22154cc14ab3d3c77ab0bffd1ff05268dae784 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, 29 Apr 2026 13:22:47 +0700 Subject: [PATCH 2/2] refactor(coordinator): standardize selectedTabAndIndex destructuring as (tab, tabIndex) --- .../Extensions/MainContentCoordinator+FKNavigation.swift | 4 ++-- .../Main/Extensions/MainContentCoordinator+Filtering.swift | 6 +++--- .../Main/Extensions/MainContentCoordinator+LoadMore.swift | 6 +++--- .../Main/Extensions/MainContentCoordinator+Navigation.swift | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+FKNavigation.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+FKNavigation.swift index 416e49824..40b8a82fb 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+FKNavigation.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+FKNavigation.swift @@ -46,8 +46,8 @@ extension MainContentCoordinator { current.tableContext.databaseName == currentDatabase, current.tableContext.schemaName == targetSchema { applyFKFilter(filter, for: referencedTable) - if let (_, idx) = tabManager.selectedTabAndIndex { - tabManager.tabs[idx].filterState = filterStateManager.saveToTabState() + if let (_, tabIndex) = tabManager.selectedTabAndIndex { + tabManager.tabs[tabIndex].filterState = filterStateManager.saveToTabState() } return } diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+Filtering.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+Filtering.swift index 35e362b16..324eab239 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+Filtering.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+Filtering.swift @@ -81,10 +81,10 @@ extension MainContentCoordinator { func restoreFiltersForTable(_ tableName: String) { filterStateManager.restoreLastFilters(for: tableName) - guard let (_, idx) = tabManager.selectedTabAndIndex else { return } - tabManager.tabs[idx].filterState = filterStateManager.saveToTabState() + guard let (_, tabIndex) = tabManager.selectedTabAndIndex else { return } + tabManager.tabs[tabIndex].filterState = filterStateManager.saveToTabState() if filterStateManager.hasAppliedFilters { - rebuildTableQuery(at: idx) + rebuildTableQuery(at: tabIndex) } } diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+LoadMore.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+LoadMore.swift index 42adf5b3f..295d6e18e 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+LoadMore.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+LoadMore.swift @@ -35,7 +35,7 @@ extension MainContentCoordinator { // MARK: - Load More Rows func loadMoreRows() { - guard let (tab, idx) = tabManager.selectedTabAndIndex, + guard let (tab, tabIndex) = tabManager.selectedTabAndIndex, !tab.pagination.isLoadingMore, !tab.execution.isExecuting, tab.pagination.hasMoreRows, @@ -47,7 +47,7 @@ extension MainContentCoordinator { let capturedGeneration = queryGeneration let storedParamValues = tab.pagination.baseQueryParameterValues - tabManager.tabs[idx].pagination.isLoadingMore = true + tabManager.tabs[tabIndex].pagination.isLoadingMore = true toolbarState.setExecuting(true) currentQueryTask = Task { [weak self] in @@ -133,7 +133,7 @@ extension MainContentCoordinator { // MARK: - Fetch All Rows func fetchAllRows() { - guard let (tab, idx) = tabManager.selectedTabAndIndex, + guard let (tab, _) = tabManager.selectedTabAndIndex, !tab.pagination.isLoadingMore, !tab.execution.isExecuting, tab.pagination.hasMoreRows, diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift index 4e2572bc5..2df2d23dd 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift @@ -41,8 +41,8 @@ extension MainContentCoordinator { current.tabType == .table, current.tableContext.tableName == tableName, current.tableContext.databaseName == currentDatabase { - if showStructure, let (_, idx) = tabManager.selectedTabAndIndex { - tabManager.tabs[idx].display.resultsViewMode = .structure + if showStructure, let (_, tabIndex) = tabManager.selectedTabAndIndex { + tabManager.tabs[tabIndex].display.resultsViewMode = .structure } return }