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
14 changes: 14 additions & 0 deletions TablePro/Core/KeyboardHandling/KeyCode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,20 @@ public enum KeyCode: UInt16 {
/// Right arrow
case rightArrow = 124

// MARK: - Navigation Keys

/// Home key
case home = 115

/// End key
case end = 119

/// Page Up key
case pageUp = 116

/// Page Down key
case pageDown = 121

// MARK: - Letter Keys (for Cmd+ shortcuts)

case a = 0
Expand Down
30 changes: 24 additions & 6 deletions TablePro/Models/Query/ResultSet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,30 @@ final class ResultSet: Identifiable {
var pagination = PaginationState()
var columnLayout = ColumnLayoutState()

// Column metadata
var columnTypes: [ColumnType] = []
var columnDefaults: [String: String?] = [:]
var columnForeignKeys: [String: ForeignKeyInfo] = [:]
var columnEnumValues: [String: [String]] = [:]
var columnNullable: [String: Bool] = [:]
var columnTypes: [ColumnType] {
get { rowBuffer.columnTypes }
set { rowBuffer.columnTypes = newValue }
}

var columnDefaults: [String: String?] {
get { rowBuffer.columnDefaults }
set { rowBuffer.columnDefaults = newValue }
}

var columnForeignKeys: [String: ForeignKeyInfo] {
get { rowBuffer.columnForeignKeys }
set { rowBuffer.columnForeignKeys = newValue }
}

var columnEnumValues: [String: [String]] {
get { rowBuffer.columnEnumValues }
set { rowBuffer.columnEnumValues = newValue }
}

var columnNullable: [String: Bool] {
get { rowBuffer.columnNullable }
set { rowBuffer.columnNullable = newValue }
}

var resultColumns: [String] { rowBuffer.columns }
var resultRows: [[String?]] { rowBuffer.rows }
Expand Down
137 changes: 0 additions & 137 deletions TablePro/Models/Query/RowProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -354,140 +354,3 @@ final class InMemoryRowProvider: RowProvider {
return safeBuffer.rows[displayIndex]
}
}

// MARK: - Database Row Provider (for virtualized access via driver)

/// Row provider that fetches data on-demand from database.
/// Cache is bounded to `maxCacheSize` entries; oldest entries by row index
/// are evicted when the limit is exceeded.
final class DatabaseRowProvider: RowProvider {
private static let logger = Logger(subsystem: "com.TablePro", category: "RowProvider")
private static let maxCacheSize = 10_000

private let driver: DatabaseDriver
private let baseQuery: String
private var cache: [Int: TableRowData] = [:]
private let pageSize: Int
private var prefetchTask: Task<Void, Never>?
private var inFlightRange: Range<Int>?

private(set) var totalRowCount: Int = 0
private(set) var columns: [String]
private(set) var columnDefaults: [String: String?]

private var isInitialized = false

init(driver: DatabaseDriver, query: String, columns: [String], columnDefaults: [String: String?] = [:], pageSize: Int = 200) {
self.driver = driver
self.baseQuery = query
self.columns = columns
self.columnDefaults = columnDefaults
self.pageSize = pageSize
}

/// Initialize by fetching total row count
func initialize() async throws {
guard !isInitialized else { return }

totalRowCount = try await driver.fetchRowCount(query: baseQuery)
isInitialized = true
}

func fetchRows(offset: Int, limit: Int) -> [TableRowData] {
var result: [TableRowData] = []

for i in offset..<min(offset + limit, totalRowCount) {
if let cached = cache[i] {
result.append(cached)
} else {
// Return placeholder - actual data filled via prefetch
let placeholder = TableRowData(index: i, values: Array(repeating: "...", count: columns.count))
result.append(placeholder)
}
}

return result
}

func prefetchRows(at indices: [Int]) {
let missingIndices = indices.filter { cache[$0] == nil }
guard !missingIndices.isEmpty else { return }

guard let minIndex = missingIndices.min(),
let maxIndex = missingIndices.max() else { return }

let offset = minIndex
let limit = min(maxIndex - minIndex + pageSize, totalRowCount - offset)
let fetchRange = offset..<(offset + limit)

if let inFlight = inFlightRange,
inFlight.contains(offset) && inFlight.contains(offset + limit - 1) {
return
}

prefetchTask?.cancel()
let driver = self.driver
let baseQuery = self.baseQuery

inFlightRange = fetchRange
prefetchTask = Task { [weak self] in
do {
let result = try await driver.fetchRows(query: baseQuery, offset: offset, limit: limit)
guard !Task.isCancelled else { return }
await MainActor.run { [weak self] in
guard let self else { return }
for (i, row) in result.rows.enumerated() {
self.cache[offset + i] = TableRowData(index: offset + i, values: row)
}
self.evictCacheIfNeeded(nearIndex: offset)
self.inFlightRange = nil
}
} catch {
guard !Task.isCancelled else { return }
Self.logger.error("Prefetch error: \(error)")
await MainActor.run { [weak self] in
self?.inFlightRange = nil
}
}
}
}

func invalidateCache() {
prefetchTask?.cancel()
prefetchTask = nil
inFlightRange = nil
cache.removeAll()
isInitialized = false
}

/// Synchronously fetch and cache rows (for initial load)
func loadRows(offset: Int, limit: Int) async throws {
let result = try await driver.fetchRows(query: baseQuery, offset: offset, limit: limit)
for (i, row) in result.rows.enumerated() {
let rowData = TableRowData(index: offset + i, values: row)
cache[offset + i] = rowData
}
evictCacheIfNeeded(nearIndex: offset)
}

/// Get row data at index (nil if not cached)
func row(at index: Int) -> TableRowData? {
cache[index]
}

/// Update a cached cell value
func updateValue(_ value: String?, at rowIndex: Int, columnIndex: Int) {
cache[rowIndex]?.setValue(value, at: columnIndex)
}

// MARK: - Private

/// Evict entries when cache exceeds `maxCacheSize`.
/// Keeps the half of entries closest to `nearIndex` (the current access window)
/// and discards the rest.
private func evictCacheIfNeeded(nearIndex: Int) {
guard cache.count > Self.maxCacheSize else { return }
let halfSize = Self.maxCacheSize / 2
cache = cache.filter { abs($0.key - nearIndex) <= halfSize }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -293,11 +293,6 @@ extension MainContentCoordinator {
rs.isEditable = updatedTab.tableContext.isEditable
rs.resultVersion = updatedTab.resultVersion
rs.metadataVersion = updatedTab.metadataVersion
rs.columnTypes = updatedTab.columnTypes
rs.columnDefaults = updatedTab.columnDefaults
rs.columnForeignKeys = updatedTab.columnForeignKeys
rs.columnEnumValues = updatedTab.columnEnumValues
rs.columnNullable = updatedTab.columnNullable

// Keep pinned results, replace unpinned
let pinned = updatedTab.display.resultSets.filter(\.isPinned)
Expand Down
Loading
Loading