diff --git a/CHANGELOG.md b/CHANGELOG.md index 983388baa..56f52c07d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Switching between sidebar tables no longer leaves extra blank space above the list. (#1675) - SSH tunnels no longer pin a CPU core after the connection drops. A dropped tunnel is now detected and torn down instead of spinning in its relay loop. (#1769) +- Restored table tabs now load with the current page size instead of the page size from the previous session. ## [0.53.0] - 2026-06-25 diff --git a/TablePro/Core/Services/Infrastructure/TabPersistenceCoordinator.swift b/TablePro/Core/Services/Infrastructure/TabPersistenceCoordinator.swift index ef4245914..5163b9d49 100644 --- a/TablePro/Core/Services/Infrastructure/TabPersistenceCoordinator.swift +++ b/TablePro/Core/Services/Infrastructure/TabPersistenceCoordinator.swift @@ -141,7 +141,8 @@ internal final class TabPersistenceCoordinator { return RestoreResult(tabs: [], selectedTabId: nil, source: .none) } - var restoredTabs = state.tabs.map { QueryTab(from: $0) } + let defaultPageSize = AppSettingsManager.shared.dataGrid.defaultPageSize + var restoredTabs = state.tabs.map { QueryTab(from: $0, defaultPageSize: defaultPageSize) } for index in restoredTabs.indices { guard let url = restoredTabs[index].content.sourceFileURL else { continue } if let loaded = FileTextLoader.load(url) { diff --git a/TablePro/Models/Query/QueryTab.swift b/TablePro/Models/Query/QueryTab.swift index 8d1aa14fe..a55ee4c50 100644 --- a/TablePro/Models/Query/QueryTab.swift +++ b/TablePro/Models/Query/QueryTab.swift @@ -72,7 +72,7 @@ struct QueryTab: Identifiable, Equatable { self.restoredCursorOffset = nil } - init(from persisted: PersistedTab) { + init(from persisted: PersistedTab, defaultPageSize: Int) { self.id = persisted.id self.title = persisted.title self.tabType = persisted.tabType @@ -96,7 +96,7 @@ struct QueryTab: Identifiable, Equatable { self.sortState = SortState() self.filterState = TabFilterState() self.columnLayout = ColumnLayoutState(columnWidths: persisted.columnWidths ?? [:]) - self.pagination = PaginationState() + self.pagination = PaginationState(pageSize: defaultPageSize) self.hasUserInteraction = false self.schemaVersion = 0 self.metadataVersion = 0 diff --git a/TablePro/Models/Settings/AppSettings.swift b/TablePro/Models/Settings/AppSettings.swift index 56f97e46c..2628a3744 100644 --- a/TablePro/Models/Settings/AppSettings.swift +++ b/TablePro/Models/Settings/AppSettings.swift @@ -205,7 +205,8 @@ struct DataGridSettings: Codable, Equatable { rowHeight = try container.decodeIfPresent(DataGridRowHeight.self, forKey: .rowHeight) ?? .normal dateFormat = try container.decodeIfPresent(DateFormatOption.self, forKey: .dateFormat) ?? .iso8601 nullDisplay = try container.decodeIfPresent(String.self, forKey: .nullDisplay) ?? "NULL" - defaultPageSize = try container.decodeIfPresent(Int.self, forKey: .defaultPageSize) ?? 1_000 + defaultPageSize = (try container.decodeIfPresent(Int.self, forKey: .defaultPageSize) ?? 1_000) + .clamped(to: SettingsValidationRules.defaultPageSizeRange) showAlternateRows = try container.decodeIfPresent(Bool.self, forKey: .showAlternateRows) ?? true showRowNumbers = try container.decodeIfPresent(Bool.self, forKey: .showRowNumbers) ?? true autoShowInspector = try container.decodeIfPresent(Bool.self, forKey: .autoShowInspector) ?? false diff --git a/TablePro/Views/Inspector/InspectorViewController.swift b/TablePro/Views/Inspector/InspectorViewController.swift index 37814f77b..1945bdea6 100644 --- a/TablePro/Views/Inspector/InspectorViewController.swift +++ b/TablePro/Views/Inspector/InspectorViewController.swift @@ -40,7 +40,7 @@ final class InspectorViewController: NSViewController, NSUserInterfaceValidation self.gridDelegate = InspectorGridDelegate() super.init(nibName: nil, bundle: nil) gridDelegate.owner = self - state.pageSize = max(1, AppSettingsManager.shared.dataGrid.defaultPageSize) + state.pageSize = AppSettingsManager.shared.dataGrid.defaultPageSize inspectorDocument.onChange = { [weak self] in guard let self else { return } if self.isApplyingGridCellEdit, self.displayIndices == nil { diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+TableFirstLoad.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+TableFirstLoad.swift index bcc985cb2..c05d06ad4 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+TableFirstLoad.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+TableFirstLoad.swift @@ -20,7 +20,12 @@ extension MainContentCoordinator { let tableName = tab.tableContext.tableName, !tableName.isEmpty else { return false } let hint = PluginManager.shared.defaultSortHint(for: connection.type, table: tableName) - guard firstLoadNeedsSchemaColumns(for: tab, hint: hint) else { return true } + guard firstLoadNeedsSchemaColumns(for: tab, hint: hint) else { + if let index = tabManager.tabs.firstIndex(where: { $0.id == tabId }) { + filterCoordinator.rebuildTableQuery(at: index) + } + return true + } await loadSchemaColumns(for: tableName, schema: tab.tableContext.schemaName) @@ -52,7 +57,7 @@ extension MainContentCoordinator { tab.pendingRestoredSort ?? [], in: effectiveResultColumns(for: tab) ) - let pageSize = max(1, AppSettingsManager.shared.dataGrid.defaultPageSize) + let pageSize = AppSettingsManager.shared.dataGrid.defaultPageSize let page = max(1, tab.restoredPage ?? 1) tabManager.mutate(at: index) { tab in diff --git a/TableProTests/Core/Services/PersistedTabRoundTripTests.swift b/TableProTests/Core/Services/PersistedTabRoundTripTests.swift index 53fbc0b5d..bcff3eb73 100644 --- a/TableProTests/Core/Services/PersistedTabRoundTripTests.swift +++ b/TableProTests/Core/Services/PersistedTabRoundTripTests.swift @@ -28,7 +28,7 @@ struct PersistedTabRoundTripTests { source: .user ) - let restored = QueryTab(from: tab.toPersistedTab()) + let restored = QueryTab(from: tab.toPersistedTab(), defaultPageSize: 1_000) #expect(restored.pendingRestoredSort?.count == 2) #expect(restored.pendingRestoredSort?[0].columnName == "created_at") @@ -52,7 +52,17 @@ struct PersistedTabRoundTripTests { let persisted = tab.toPersistedTab() #expect(persisted.restoredPage == 3) - #expect(QueryTab(from: persisted).restoredPage == 3) + #expect(QueryTab(from: persisted, defaultPageSize: 1_000).restoredPage == 3) + } + + @Test("Restored table tab seeds pagination from the live page size, not the persisted query") + func paginationSeedsFromLivePageSize() { + var tab = tableTab(query: "SELECT * FROM users LIMIT 500 OFFSET 0") + tab.pagination.pageSize = 500 + + let restored = QueryTab(from: tab.toPersistedTab(), defaultPageSize: 1_000) + + #expect(restored.pagination.pageSize == 1_000) } @Test("Page 1 is not persisted") @@ -70,7 +80,7 @@ struct PersistedTabRoundTripTests { let persisted = tab.toPersistedTab() #expect(persisted.cursorOffset == 7) - #expect(QueryTab(from: persisted).restoredCursorOffset == 7) + #expect(QueryTab(from: persisted, defaultPageSize: 1_000).restoredCursorOffset == 7) } @Test("Cursor offset is clamped to query length on restore") @@ -84,7 +94,7 @@ struct PersistedTabRoundTripTests { cursorOffset: 10_000 ) - #expect(QueryTab(from: persisted).restoredCursorOffset == ("SELECT" as NSString).length) + #expect(QueryTab(from: persisted, defaultPageSize: 1_000).restoredCursorOffset == ("SELECT" as NSString).length) } @Test("A truncated query drops the persisted cursor offset") @@ -107,7 +117,7 @@ struct PersistedTabRoundTripTests { var tab = tableTab() tab.columnLayout.columnWidths = ["id": 80, "name": 220.5] - let restored = QueryTab(from: tab.toPersistedTab()) + let restored = QueryTab(from: tab.toPersistedTab(), defaultPageSize: 1_000) #expect(restored.columnLayout.columnWidths == ["id": 80, "name": 220.5]) } diff --git a/TableProTests/Core/Validation/SettingsValidationTests.swift b/TableProTests/Core/Validation/SettingsValidationTests.swift index 51a26aa3e..dfcd2daeb 100644 --- a/TableProTests/Core/Validation/SettingsValidationTests.swift +++ b/TableProTests/Core/Validation/SettingsValidationTests.swift @@ -267,6 +267,29 @@ struct SettingsValidationTests { #expect(range.upperBound == 100_000) } + @Test("Decoding clamps an out-of-range stored page size to the valid range") + func decodingClampsDefaultPageSize() throws { + let range = SettingsValidationRules.defaultPageSizeRange + + let zero = try JSONDecoder().decode( + DataGridSettings.self, + from: Data(#"{"defaultPageSize":0}"#.utf8) + ) + #expect(zero.defaultPageSize == range.lowerBound) + + let tooLarge = try JSONDecoder().decode( + DataGridSettings.self, + from: Data(#"{"defaultPageSize":9999999}"#.utf8) + ) + #expect(tooLarge.defaultPageSize == range.upperBound) + + let valid = try JSONDecoder().decode( + DataGridSettings.self, + from: Data(#"{"defaultPageSize":1000}"#.utf8) + ) + #expect(valid.defaultPageSize == 1_000) + } + @Test("Validation rules min non-negative is correct") func validationRulesMinNonNegative() { #expect(SettingsValidationRules.minNonNegative == 0) diff --git a/TableProTests/Models/PreviewTabTests.swift b/TableProTests/Models/PreviewTabTests.swift index 58889d411..66edb74b1 100644 --- a/TableProTests/Models/PreviewTabTests.swift +++ b/TableProTests/Models/PreviewTabTests.swift @@ -28,7 +28,7 @@ struct PreviewTabTests { tabType: .table, tableName: "users" ) - let tab = QueryTab(from: persisted) + let tab = QueryTab(from: persisted, defaultPageSize: 1_000) #expect(tab.isPreview == false) } diff --git a/TableProTests/Views/Main/DefaultSortInitialQueryTests.swift b/TableProTests/Views/Main/DefaultSortInitialQueryTests.swift index 2c2e75b1a..9c5e00259 100644 --- a/TableProTests/Views/Main/DefaultSortInitialQueryTests.swift +++ b/TableProTests/Views/Main/DefaultSortInitialQueryTests.swift @@ -103,20 +103,52 @@ struct DefaultSortInitialQueryTests { #expect(tabManager.tabs[index].content.query == originalQuery) } - @Test("None behavior takes the fast path without touching the query") - func noneBehaviorSkipsSchemaWait() async { + @Test("None behavior takes the fast path and regenerates the browse query from current state") + func noneBehaviorRegeneratesQueryOnFastPath() async { let (coordinator, tabManager, index) = makeCoordinator(tableName: "users") - let originalQuery = tabManager.tabs[index].content.query + let pageSize = tabManager.tabs[index].pagination.pageSize await withDefaultSortBehavior(DefaultSortBehavior.none) { let ready = await coordinator.prepareTableTabFirstLoad(tabId: tabManager.tabs[index].id) #expect(ready) } - #expect(tabManager.tabs[index].content.query == originalQuery) + let query = tabManager.tabs[index].content.query + #expect(query.contains("LIMIT \(pageSize)")) + #expect(!query.localizedCaseInsensitiveContains("ORDER BY")) #expect(!tabManager.tabs[index].sortState.isSorting) } + @Test("A restored table tab loads with the live page size, not the persisted query LIMIT") + func restoredTableTabUsesLivePageSize() async { + let tabManager = QueryTabManager() + let coordinator = MainContentCoordinator( + connection: TestFixtures.makeConnection(), + tabManager: tabManager, + changeManager: DataChangeManager(), + toolbarState: ConnectionToolbarState() + ) + let persisted = PersistedTab( + id: UUID(), + title: "users", + query: "SELECT * FROM `users` LIMIT 500 OFFSET 0", + tabType: .table, + tableName: "users" + ) + let tab = QueryTab(from: persisted, defaultPageSize: 1_000) + tabManager.tabs.append(tab) + tabManager.selectedTabId = tab.id + + await withDefaultSortBehavior(DefaultSortBehavior.none) { + let ready = await coordinator.prepareTableTabFirstLoad(tabId: tab.id) + #expect(ready) + } + + let query = tabManager.tabs[0].content.query + #expect(query.contains("LIMIT 1000")) + #expect(!query.contains("500")) + } + @Test("A restored user sort is never overwritten by the default sort") func userSortSurvivesFirstLoad() async { let (coordinator, tabManager, index) = makeCoordinator(tableName: "users")