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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Fill Column: right-click a column header and choose Fill Column to set one value across all loaded rows. The change is staged like a normal edit, so you review it and Save before it applies, and one undo reverts the whole fill. Not available on primary key columns. (#1304)
- AWS IAM authentication for PostgreSQL and MySQL connections to RDS and Aurora. Pick AWS IAM in the connection's Authentication field and use an access key, a named AWS profile, or SSO. TablePro generates a fresh login token on every connect and reconnect, so you never paste an expiring token, and SSL is required automatically. (#1291)
- Pagination bar for table tabs with a rows-per-page menu (5, 10, 20, 100, 500, 1,000, All rows, or a custom size) and First, Previous, Next, and Last page buttons. (#1364)
- Click the page indicator in the pagination bar to jump to a specific page. (#1364)
- Pagination now appears for filtered tables whose total row count is unknown, so you can page through them instead of seeing only the first page. (#1364)
- First Page and Last Page keyboard actions, unbound by default and assignable in Settings > Keyboard. (#1364)

## [0.44.0] - 2026-05-23

Expand Down
48 changes: 40 additions & 8 deletions TablePro/Core/Coordinators/PaginationCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ final class PaginationCoordinator {
// MARK: - Pagination

func goToNextPage() {
paginateIfPossible(where: \.hasNextPage) { $0.goToNextPage() }
guard let (tab, tabIndex) = parent.tabManager.selectedTabAndIndex else { return }
let loadedRowCount = parent.tabSessionRegistry.tableRows(for: tab.id).rows.count
guard tab.pagination.canGoToNextPage(loadedRowCount: loadedRowCount) else { return }
paginateAfterConfirmation(tabIndex: tabIndex) { $0.goToNextPage(loadedRowCount: loadedRowCount) }
}

func goToPreviousPage() {
Expand All @@ -33,21 +36,50 @@ final class PaginationCoordinator {
}

func goToLastPage() {
paginateIfPossible(where: { $0.currentPage != $0.totalPages }) { $0.goToLastPage() }
paginateIfPossible(where: { $0.isLastPageKnown && $0.currentPage != $0.totalPages }) { $0.goToLastPage() }
}

func goToPage(_ page: Int) {
paginateIfPossible(where: { $0.isLastPageKnown && page > 0 && page <= $0.totalPages }) { $0.goToPage(page) }
}

func updatePageSize(_ newSize: Int) {
guard newSize > 0 else { return }
paginateIfPossible { $0.updatePageSize(newSize) }
}

func updateOffset(_ newOffset: Int) {
guard newOffset >= 0 else { return }
paginateIfPossible { $0.updateOffset(newOffset) }
}
func showAllRows() {
guard let (tab, _) = parent.tabManager.selectedTabAndIndex,
let total = tab.pagination.totalRowCount, total > 0 else { return }

func applyPaginationSettings() {
reloadCurrentPage()
let tabId = tab.id
let alert = NSAlert()
alert.messageText = String(localized: "Show All Rows")
alert.informativeText = String(
format: String(localized: "This will load all %@ rows on a single page. Large result sets use significant memory. Continue?"),
total.formatted()
)
alert.alertStyle = .warning
alert.addButton(withTitle: String(localized: "Show All"))
alert.addButton(withTitle: String(localized: "Cancel"))

let apply: () -> Void = { [weak self] in
guard let self,
let tabIndex = parent.tabManager.tabs.firstIndex(where: { $0.id == tabId }) else { return }
paginateAfterConfirmation(tabIndex: tabIndex) { pagination in
pagination.updatePageSize(max(total, 1))
pagination.goToFirstPage()
}
}

if let window = parent.contentWindow ?? NSApp.keyWindow {
alert.beginSheetModal(for: window) { response in
guard response == .alertFirstButtonReturn else { return }
apply()
}
} else if alert.runModal() == .alertFirstButtonReturn {
apply()
}
}

private func paginateIfPossible(
Expand Down
15 changes: 15 additions & 0 deletions TablePro/Models/Query/QueryTabState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,15 @@ struct PaginationState: Equatable {
currentPage < totalPages
}

var isLastPageKnown: Bool {
totalRowCount != nil
}

func canGoToNextPage(loadedRowCount: Int) -> Bool {
if hasNextPage { return true }
return totalRowCount == nil && loadedRowCount >= pageSize
}

/// Whether there is a previous page available
var hasPreviousPage: Bool {
currentPage > 1
Expand Down Expand Up @@ -169,6 +178,12 @@ struct PaginationState: Equatable {
currentOffset = (currentPage - 1) * pageSize
}

mutating func goToNextPage(loadedRowCount: Int) {
guard canGoToNextPage(loadedRowCount: loadedRowCount) else { return }
currentPage += 1
currentOffset = (currentPage - 1) * pageSize
}

/// Navigate to previous page
mutating func goToPreviousPage() {
guard hasPreviousPage else { return }
Expand Down
6 changes: 5 additions & 1 deletion TablePro/Models/UI/KeyboardShortcutModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ enum ShortcutAction: String, Codable, CaseIterable, Identifiable {
// Navigation
case previousPage
case nextPage
case firstPage
case lastPage

// Edit
case undo
Expand Down Expand Up @@ -103,7 +105,7 @@ enum ShortcutAction: String, Codable, CaseIterable, Identifiable {
.saveChanges, .saveAs, .previewSQL, .closeTab, .refresh,
.executeQuery, .executeAllStatements, .cancelQuery, .explainQuery, .formatQuery,
.export, .importData, .quickSwitcher,
.previousPage, .nextPage, .saveAsFavorite:
.previousPage, .nextPage, .firstPage, .lastPage, .saveAsFavorite:
return .file
case .undo, .redo, .cut, .copy, .copyRowsExplicit, .copyWithHeaders, .copyAsJson, .paste,
.delete, .selectAll, .clearSelection, .addRow,
Expand Down Expand Up @@ -150,6 +152,8 @@ enum ShortcutAction: String, Codable, CaseIterable, Identifiable {
case .quickSwitcher: return String(localized: "Quick Switcher")
case .previousPage: return String(localized: "Previous Page")
case .nextPage: return String(localized: "Next Page")
case .firstPage: return String(localized: "First Page")
case .lastPage: return String(localized: "Last Page")
case .undo: return String(localized: "Undo")
case .redo: return String(localized: "Redo")
case .cut: return String(localized: "Cut")
Expand Down
Loading
Loading