Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions TablePro/Models/Query/QueryTab.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion TablePro/Models/Settings/AppSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion TablePro/Views/Inspector/InspectorViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down
20 changes: 15 additions & 5 deletions TableProTests/Core/Services/PersistedTabRoundTripTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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")
Expand All @@ -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")
Expand All @@ -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")
Expand All @@ -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])
}

Expand Down
23 changes: 23 additions & 0 deletions TableProTests/Core/Validation/SettingsValidationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion TableProTests/Models/PreviewTabTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
40 changes: 36 additions & 4 deletions TableProTests/Views/Main/DefaultSortInitialQueryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading