Skip to content

Commit 51529db

Browse files
authored
refactor(datagrid): column index helpers, metadata dedup, keyboard nav, overlay panel (#917) (#917)
1 parent 50e778a commit 51529db

15 files changed

Lines changed: 284 additions & 285 deletions

TablePro/Core/KeyboardHandling/KeyCode.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,20 @@ public enum KeyCode: UInt16 {
6868
/// Right arrow
6969
case rightArrow = 124
7070

71+
// MARK: - Navigation Keys
72+
73+
/// Home key
74+
case home = 115
75+
76+
/// End key
77+
case end = 119
78+
79+
/// Page Up key
80+
case pageUp = 116
81+
82+
/// Page Down key
83+
case pageDown = 121
84+
7185
// MARK: - Letter Keys (for Cmd+ shortcuts)
7286

7387
case a = 0

TablePro/Models/Query/ResultSet.swift

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,30 @@ final class ResultSet: Identifiable {
2828
var pagination = PaginationState()
2929
var columnLayout = ColumnLayoutState()
3030

31-
// Column metadata
32-
var columnTypes: [ColumnType] = []
33-
var columnDefaults: [String: String?] = [:]
34-
var columnForeignKeys: [String: ForeignKeyInfo] = [:]
35-
var columnEnumValues: [String: [String]] = [:]
36-
var columnNullable: [String: Bool] = [:]
31+
var columnTypes: [ColumnType] {
32+
get { rowBuffer.columnTypes }
33+
set { rowBuffer.columnTypes = newValue }
34+
}
35+
36+
var columnDefaults: [String: String?] {
37+
get { rowBuffer.columnDefaults }
38+
set { rowBuffer.columnDefaults = newValue }
39+
}
40+
41+
var columnForeignKeys: [String: ForeignKeyInfo] {
42+
get { rowBuffer.columnForeignKeys }
43+
set { rowBuffer.columnForeignKeys = newValue }
44+
}
45+
46+
var columnEnumValues: [String: [String]] {
47+
get { rowBuffer.columnEnumValues }
48+
set { rowBuffer.columnEnumValues = newValue }
49+
}
50+
51+
var columnNullable: [String: Bool] {
52+
get { rowBuffer.columnNullable }
53+
set { rowBuffer.columnNullable = newValue }
54+
}
3755

3856
var resultColumns: [String] { rowBuffer.columns }
3957
var resultRows: [[String?]] { rowBuffer.rows }

TablePro/Models/Query/RowProvider.swift

Lines changed: 0 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -354,140 +354,3 @@ final class InMemoryRowProvider: RowProvider {
354354
return safeBuffer.rows[displayIndex]
355355
}
356356
}
357-
358-
// MARK: - Database Row Provider (for virtualized access via driver)
359-
360-
/// Row provider that fetches data on-demand from database.
361-
/// Cache is bounded to `maxCacheSize` entries; oldest entries by row index
362-
/// are evicted when the limit is exceeded.
363-
final class DatabaseRowProvider: RowProvider {
364-
private static let logger = Logger(subsystem: "com.TablePro", category: "RowProvider")
365-
private static let maxCacheSize = 10_000
366-
367-
private let driver: DatabaseDriver
368-
private let baseQuery: String
369-
private var cache: [Int: TableRowData] = [:]
370-
private let pageSize: Int
371-
private var prefetchTask: Task<Void, Never>?
372-
private var inFlightRange: Range<Int>?
373-
374-
private(set) var totalRowCount: Int = 0
375-
private(set) var columns: [String]
376-
private(set) var columnDefaults: [String: String?]
377-
378-
private var isInitialized = false
379-
380-
init(driver: DatabaseDriver, query: String, columns: [String], columnDefaults: [String: String?] = [:], pageSize: Int = 200) {
381-
self.driver = driver
382-
self.baseQuery = query
383-
self.columns = columns
384-
self.columnDefaults = columnDefaults
385-
self.pageSize = pageSize
386-
}
387-
388-
/// Initialize by fetching total row count
389-
func initialize() async throws {
390-
guard !isInitialized else { return }
391-
392-
totalRowCount = try await driver.fetchRowCount(query: baseQuery)
393-
isInitialized = true
394-
}
395-
396-
func fetchRows(offset: Int, limit: Int) -> [TableRowData] {
397-
var result: [TableRowData] = []
398-
399-
for i in offset..<min(offset + limit, totalRowCount) {
400-
if let cached = cache[i] {
401-
result.append(cached)
402-
} else {
403-
// Return placeholder - actual data filled via prefetch
404-
let placeholder = TableRowData(index: i, values: Array(repeating: "...", count: columns.count))
405-
result.append(placeholder)
406-
}
407-
}
408-
409-
return result
410-
}
411-
412-
func prefetchRows(at indices: [Int]) {
413-
let missingIndices = indices.filter { cache[$0] == nil }
414-
guard !missingIndices.isEmpty else { return }
415-
416-
guard let minIndex = missingIndices.min(),
417-
let maxIndex = missingIndices.max() else { return }
418-
419-
let offset = minIndex
420-
let limit = min(maxIndex - minIndex + pageSize, totalRowCount - offset)
421-
let fetchRange = offset..<(offset + limit)
422-
423-
if let inFlight = inFlightRange,
424-
inFlight.contains(offset) && inFlight.contains(offset + limit - 1) {
425-
return
426-
}
427-
428-
prefetchTask?.cancel()
429-
let driver = self.driver
430-
let baseQuery = self.baseQuery
431-
432-
inFlightRange = fetchRange
433-
prefetchTask = Task { [weak self] in
434-
do {
435-
let result = try await driver.fetchRows(query: baseQuery, offset: offset, limit: limit)
436-
guard !Task.isCancelled else { return }
437-
await MainActor.run { [weak self] in
438-
guard let self else { return }
439-
for (i, row) in result.rows.enumerated() {
440-
self.cache[offset + i] = TableRowData(index: offset + i, values: row)
441-
}
442-
self.evictCacheIfNeeded(nearIndex: offset)
443-
self.inFlightRange = nil
444-
}
445-
} catch {
446-
guard !Task.isCancelled else { return }
447-
Self.logger.error("Prefetch error: \(error)")
448-
await MainActor.run { [weak self] in
449-
self?.inFlightRange = nil
450-
}
451-
}
452-
}
453-
}
454-
455-
func invalidateCache() {
456-
prefetchTask?.cancel()
457-
prefetchTask = nil
458-
inFlightRange = nil
459-
cache.removeAll()
460-
isInitialized = false
461-
}
462-
463-
/// Synchronously fetch and cache rows (for initial load)
464-
func loadRows(offset: Int, limit: Int) async throws {
465-
let result = try await driver.fetchRows(query: baseQuery, offset: offset, limit: limit)
466-
for (i, row) in result.rows.enumerated() {
467-
let rowData = TableRowData(index: offset + i, values: row)
468-
cache[offset + i] = rowData
469-
}
470-
evictCacheIfNeeded(nearIndex: offset)
471-
}
472-
473-
/// Get row data at index (nil if not cached)
474-
func row(at index: Int) -> TableRowData? {
475-
cache[index]
476-
}
477-
478-
/// Update a cached cell value
479-
func updateValue(_ value: String?, at rowIndex: Int, columnIndex: Int) {
480-
cache[rowIndex]?.setValue(value, at: columnIndex)
481-
}
482-
483-
// MARK: - Private
484-
485-
/// Evict entries when cache exceeds `maxCacheSize`.
486-
/// Keeps the half of entries closest to `nearIndex` (the current access window)
487-
/// and discards the rest.
488-
private func evictCacheIfNeeded(nearIndex: Int) {
489-
guard cache.count > Self.maxCacheSize else { return }
490-
let halfSize = Self.maxCacheSize / 2
491-
cache = cache.filter { abs($0.key - nearIndex) <= halfSize }
492-
}
493-
}

TablePro/Views/Main/Extensions/MainContentCoordinator+QueryHelpers.swift

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -293,11 +293,6 @@ extension MainContentCoordinator {
293293
rs.isEditable = updatedTab.tableContext.isEditable
294294
rs.resultVersion = updatedTab.resultVersion
295295
rs.metadataVersion = updatedTab.metadataVersion
296-
rs.columnTypes = updatedTab.columnTypes
297-
rs.columnDefaults = updatedTab.columnDefaults
298-
rs.columnForeignKeys = updatedTab.columnForeignKeys
299-
rs.columnEnumValues = updatedTab.columnEnumValues
300-
rs.columnNullable = updatedTab.columnNullable
301296

302297
// Keep pinned results, replace unpinned
303298
let pinned = updatedTab.display.resultSets.filter(\.isPinned)

0 commit comments

Comments
 (0)