Skip to content

Commit 2d941b4

Browse files
committed
refactor(datagrid): display cache moves to coordinator keyed by Row.id
1 parent 0b7ea83 commit 2d941b4

13 files changed

Lines changed: 238 additions & 163 deletions

TablePro/Views/Main/Child/MainEditorContentView.swift

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,7 @@ struct MainEditorContentView: View {
581581
hiddenColumns: columnVisibilityManager.hiddenColumns
582582
),
583583
sortedIDs: sortedIDsForTab(tab),
584+
displayFormats: displayFormats(for: tab),
584585
delegate: dataTabDelegate,
585586
selectedRowIndices: Binding(
586587
get: { selectionState.indices },
@@ -656,23 +657,21 @@ struct MainEditorContentView: View {
656657
)
657658
}
658659

659-
applyDisplayFormats(to: provider, tab: tab)
660660
return provider
661661
}
662662

663-
private func applyDisplayFormats(to provider: InMemoryRowProvider, tab: QueryTab) {
664-
let columns = provider.columns
665-
let columnTypes = provider.columnTypes
666-
guard !columns.isEmpty else { return }
663+
private func displayFormats(for tab: QueryTab) -> [ValueDisplayFormat?] {
664+
let tableRows = coordinator.tableRowsStore.existingTableRows(for: tab.id)
665+
let columns = tableRows?.columns ?? []
666+
let columnTypes = tableRows?.columnTypes ?? []
667+
guard !columns.isEmpty else { return [] }
667668

668669
let settings = AppSettingsManager.shared.dataGrid
669670
let service = ValueDisplayFormatService.shared
670671

671-
// Auto-detect formats when the setting is enabled
672672
var detected: [ValueDisplayFormat?] = Array(repeating: nil, count: columns.count)
673673
if settings.enableSmartValueDetection {
674674
let sampleRows: [[String?]]? = {
675-
let tableRows = coordinator.tableRowsStore.existingTableRows(for: tab.id)
676675
let rows = tableRows?.rows.prefix(10).map(\.values) ?? []
677676
return rows.isEmpty ? nil : Array(rows)
678677
}()
@@ -682,7 +681,6 @@ struct MainEditorContentView: View {
682681
sampleValues: sampleRows
683682
)
684683

685-
// Update service's auto-detected formats
686684
var autoMap: [String: ValueDisplayFormat] = [:]
687685
for (i, format) in detected.enumerated() where i < columns.count {
688686
if let format {
@@ -694,7 +692,6 @@ struct MainEditorContentView: View {
694692
service.clearAutoDetectedFormats(connectionId: connectionId, tableName: tab.tableContext.tableName)
695693
}
696694

697-
// Merge with stored overrides (override > detection > nil)
698695
let connId = connectionId
699696
let tblName = tab.tableContext.tableName
700697
var merged = detected
@@ -710,10 +707,7 @@ struct MainEditorContentView: View {
710707
}
711708
}
712709

713-
// Only set if there's at least one non-nil format
714-
if merged.contains(where: { $0 != nil }) {
715-
provider.updateDisplayFormats(merged)
716-
}
710+
return merged.contains(where: { $0 != nil }) ? merged : []
717711
}
718712

719713
/// Returns the display order as a permutation of `RowID`, or nil when no sort applies.

TablePro/Views/Results/DataGridCoordinator.swift

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
2121
var changeManager: AnyChangeManager
2222
var isEditable: Bool
2323
var sortedIDs: [RowID]?
24+
private(set) var columnDisplayFormats: [ValueDisplayFormat?] = []
25+
private var displayCache: [RowID: [String?]] = [:]
2426
weak var delegate: (any DataGridViewDelegate)?
2527
weak var activeFKPreviewPopover: NSPopover?
2628
var dropdownColumns: Set<Int>?
@@ -150,11 +152,11 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
150152
// When smart detection is toggled off, clear display formats so they stop being applied
151153
if prev.enableSmartValueDetection != settings.enableSmartValueDetection
152154
&& !settings.enableSmartValueDetection {
153-
self.rowProvider.updateDisplayFormats([])
155+
self.updateDisplayFormats([])
154156
}
155157

156158
if dataChanged {
157-
self.rowProvider.invalidateDisplayCache()
159+
self.invalidateDisplayCache()
158160
let visibleRect = tableView.visibleRect
159161
let visibleRange = tableView.rows(in: visibleRect)
160162
if visibleRange.length > 0 {
@@ -200,6 +202,8 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
200202
overlayEditor?.dismiss(commit: false)
201203
rowProvider = InMemoryRowProvider(rows: [], columns: [])
202204
rowVisualStateCache.removeAll()
205+
displayCache.removeAll()
206+
columnDisplayFormats = []
203207
cachedRowCount = 0
204208
cachedColumnCount = 0
205209
sortedIDs = nil
@@ -254,7 +258,7 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
254258

255259
func applyFullReplace() {
256260
guard let tableView else { return }
257-
rowProvider.invalidateDisplayCache()
261+
displayCache.removeAll()
258262
rebuildVisualStateCache()
259263
updateCache()
260264
tableView.reloadData()
@@ -281,13 +285,90 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
281285
return displayIndex
282286
}
283287

288+
func displayValue(forID id: RowID, column: Int, rawValue: String?, columnType: ColumnType?) -> String? {
289+
if let cachedRow = displayCache[id], column >= 0, column < cachedRow.count, let cached = cachedRow[column] {
290+
return cached
291+
}
292+
let format = column >= 0 && column < columnDisplayFormats.count ? columnDisplayFormats[column] : nil
293+
let formatted = CellDisplayFormatter.format(rawValue, columnType: columnType, displayFormat: format) ?? rawValue
294+
295+
var rowCache = displayCache[id] ?? []
296+
let neededCount = max(column + 1, columnDisplayFormats.count)
297+
if rowCache.count < neededCount {
298+
rowCache.append(contentsOf: Array(repeating: nil, count: neededCount - rowCache.count))
299+
}
300+
if column >= 0, column < rowCache.count {
301+
rowCache[column] = formatted
302+
}
303+
displayCache[id] = rowCache
304+
return formatted
305+
}
306+
307+
func invalidateDisplayCache() {
308+
displayCache.removeAll()
309+
}
310+
311+
func updateDisplayFormats(_ formats: [ValueDisplayFormat?]) {
312+
columnDisplayFormats = formats
313+
displayCache.removeAll()
314+
}
315+
316+
func syncDisplayFormats(_ formats: [ValueDisplayFormat?]) {
317+
guard formats != columnDisplayFormats else { return }
318+
columnDisplayFormats = formats
319+
displayCache.removeAll()
320+
}
321+
322+
func preWarmDisplayCache(upTo rowCount: Int) {
323+
let tableRows = tableRowsProvider()
324+
let displayCount = sortedIDs?.count ?? tableRows.count
325+
let count = min(rowCount, displayCount)
326+
guard count > 0 else { return }
327+
for displayIndex in 0..<count {
328+
guard let row = displayRow(at: displayIndex) else { continue }
329+
let id = row.id
330+
guard displayCache[id] == nil else { continue }
331+
let columnCount = tableRows.columns.count
332+
var rowCache = [String?](repeating: nil, count: columnCount)
333+
for col in 0..<min(row.values.count, columnCount) {
334+
let columnType = col < tableRows.columnTypes.count ? tableRows.columnTypes[col] : nil
335+
let format = col < columnDisplayFormats.count ? columnDisplayFormats[col] : nil
336+
rowCache[col] = CellDisplayFormatter.format(
337+
row.values[col],
338+
columnType: columnType,
339+
displayFormat: format
340+
) ?? row.values[col]
341+
}
342+
displayCache[id] = rowCache
343+
}
344+
}
345+
346+
private func pruneDisplayCacheToAliveIDs() {
347+
guard !displayCache.isEmpty else { return }
348+
let tableRows = tableRowsProvider()
349+
var aliveIDs = Set<RowID>()
350+
aliveIDs.reserveCapacity(tableRows.count)
351+
for row in tableRows.rows {
352+
aliveIDs.insert(row.id)
353+
}
354+
displayCache = displayCache.filter { aliveIDs.contains($0.key) }
355+
}
356+
357+
private func invalidateDisplayCache(forDisplayRow displayIndex: Int, column: Int) {
358+
guard let row = displayRow(at: displayIndex) else { return }
359+
guard var rowCache = displayCache[row.id], column >= 0, column < rowCache.count else { return }
360+
rowCache[column] = nil
361+
displayCache[row.id] = rowCache
362+
}
363+
284364
func applyDelta(_ delta: Delta) {
285365
switch delta {
286366
case .cellChanged(let row, let column):
287367
guard let tableView else { return }
288368
let tableColumn = DataGridView.tableColumnIndex(for: column)
289369
guard row >= 0, row < tableView.numberOfRows else { return }
290370
guard tableColumn >= 0, tableColumn < tableView.numberOfColumns else { return }
371+
invalidateDisplayCache(forDisplayRow: row, column: column)
291372
tableView.reloadData(
292373
forRowIndexes: IndexSet(integer: row),
293374
columnIndexes: IndexSet(integer: tableColumn)
@@ -304,6 +385,7 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
304385
if tableColumn >= 0, tableColumn < tableView.numberOfColumns {
305386
colSet.insert(tableColumn)
306387
}
388+
invalidateDisplayCache(forDisplayRow: position.row, column: position.column)
307389
}
308390
guard !rowSet.isEmpty, !colSet.isEmpty else { return }
309391
tableView.reloadData(forRowIndexes: rowSet, columnIndexes: colSet)
@@ -314,9 +396,11 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
314396
case .rowsRemoved(let indices):
315397
guard !indices.isEmpty else { return }
316398
removeMissingIDsFromSortedIDs()
399+
pruneDisplayCacheToAliveIDs()
317400
applyRemovedRows(indices)
318401
case .columnsReplaced, .fullReplace:
319402
sortedIDs = nil
403+
displayCache.removeAll()
320404
applyFullReplace()
321405
}
322406
}
@@ -341,7 +425,7 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
341425
}
342426

343427
func invalidateCachesForUndoRedo() {
344-
rowProvider.invalidateDisplayCache()
428+
displayCache.removeAll()
345429
rebuildVisualStateCache()
346430
updateCache()
347431
}

TablePro/Views/Results/DataGridView+RowActions.swift

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,13 @@ extension TableViewCoordinator {
3333

3434
func copyRows(at indices: Set<Int>) {
3535
let sortedIndices = indices.sorted()
36-
let columnTypes = rowProvider.columnTypes
36+
let tableRows = tableRowsProvider()
37+
let columnTypes = tableRows.columnTypes
3738
var tsvRows: [String] = []
3839
var htmlRows: [[String]] = []
3940

4041
for index in sortedIndices {
41-
guard let values = rowProvider.rowValues(at: index) else { continue }
42+
guard let values = displayRow(at: index)?.values else { continue }
4243
let formatted = formatRowValues(values: values, columnTypes: columnTypes)
4344
tsvRows.append(formatted.joined(separator: "\t"))
4445
htmlRows.append(formatted)
@@ -51,13 +52,14 @@ extension TableViewCoordinator {
5152

5253
func copyRowsWithHeaders(at indices: Set<Int>) {
5354
let sortedIndices = indices.sorted()
54-
let columnTypes = rowProvider.columnTypes
55-
let columns = rowProvider.columns
55+
let tableRows = tableRowsProvider()
56+
let columnTypes = tableRows.columnTypes
57+
let columns = tableRows.columns
5658
var tsvRows: [String] = [columns.joined(separator: "\t")]
5759
var htmlRows: [[String]] = []
5860

5961
for index in sortedIndices {
60-
guard let values = rowProvider.rowValues(at: index) else { continue }
62+
guard let values = displayRow(at: index)?.values else { continue }
6163
let formatted = formatRowValues(values: values, columnTypes: columnTypes)
6264
tsvRows.append(formatted.joined(separator: "\t"))
6365
htmlRows.append(formatted)
@@ -82,15 +84,15 @@ extension TableViewCoordinator {
8284
}
8385

8486
func copyCellValue(at rowIndex: Int, columnIndex: Int) {
85-
guard columnIndex >= 0 && columnIndex < rowProvider.columns.count else { return }
87+
let tableRows = tableRowsProvider()
88+
guard columnIndex >= 0 && columnIndex < tableRows.columns.count else { return }
89+
guard let row = displayRow(at: rowIndex), columnIndex < row.values.count else { return }
8690

87-
let value = rowProvider.value(atRow: rowIndex, column: columnIndex) ?? "NULL"
88-
let columnTypes = rowProvider.columnTypes
91+
let value = row.values[columnIndex] ?? "NULL"
92+
let columnTypes = tableRows.columnTypes
8993
let columnType = columnTypes.indices.contains(columnIndex) ? columnTypes[columnIndex] : nil
9094

91-
// Use formatted value when a display format is active
92-
let formats = rowProvider.columnDisplayFormats
93-
if columnIndex < formats.count, let format = formats[columnIndex], format != .raw {
95+
if columnIndex < columnDisplayFormats.count, let format = columnDisplayFormats[columnIndex], format != .raw {
9496
let formatted = ValueDisplayFormatService.applyFormat(value, format: format)
9597
ClipboardService.shared.writeText(formatted)
9698
return
@@ -102,41 +104,44 @@ extension TableViewCoordinator {
102104

103105
func copyRowsAsInsert(at indices: Set<Int>) {
104106
guard let tableName, let databaseType else { return }
107+
let tableRows = tableRowsProvider()
105108
let driver = resolveDriver()
106109
let converter = SQLRowToStatementConverter(
107110
tableName: tableName,
108-
columns: rowProvider.columns,
111+
columns: tableRows.columns,
109112
primaryKeyColumn: primaryKeyColumn,
110113
databaseType: databaseType,
111114
quoteIdentifier: driver?.quoteIdentifier,
112115
escapeStringLiteral: driver?.escapeStringLiteral
113116
)
114-
let rows = indices.sorted().compactMap { rowProvider.rowValues(at: $0) }
117+
let rows = indices.sorted().compactMap { displayRow(at: $0)?.values }
115118
guard !rows.isEmpty else { return }
116119
ClipboardService.shared.writeText(converter.generateInserts(rows: rows))
117120
}
118121

119122
func copyRowsAsUpdate(at indices: Set<Int>) {
120123
guard let tableName, let databaseType else { return }
124+
let tableRows = tableRowsProvider()
121125
let driver = resolveDriver()
122126
let converter = SQLRowToStatementConverter(
123127
tableName: tableName,
124-
columns: rowProvider.columns,
128+
columns: tableRows.columns,
125129
primaryKeyColumn: primaryKeyColumn,
126130
databaseType: databaseType,
127131
quoteIdentifier: driver?.quoteIdentifier,
128132
escapeStringLiteral: driver?.escapeStringLiteral
129133
)
130-
let rows = indices.sorted().compactMap { rowProvider.rowValues(at: $0) }
134+
let rows = indices.sorted().compactMap { displayRow(at: $0)?.values }
131135
guard !rows.isEmpty else { return }
132136
ClipboardService.shared.writeText(converter.generateUpdates(rows: rows))
133137
}
134138

135139
func copyRowsAsJson(at indices: Set<Int>) {
136-
let rows = indices.sorted().compactMap { rowProvider.rowValues(at: $0) }
140+
let rows = indices.sorted().compactMap { displayRow(at: $0)?.values }
137141
guard !rows.isEmpty else { return }
138-
let columnTypes = rowProvider.columnTypes
139-
let converter = JsonRowConverter(columns: rowProvider.columns, columnTypes: columnTypes)
142+
let tableRows = tableRowsProvider()
143+
let columnTypes = tableRows.columnTypes
144+
let converter = JsonRowConverter(columns: tableRows.columns, columnTypes: columnTypes)
140145
ClipboardService.shared.writeText(converter.generateJson(rows: rows))
141146
}
142147

@@ -162,11 +167,12 @@ extension TableViewCoordinator {
162167
let item = NSPasteboardItem()
163168
item.setString(String(row), forType: Self.rowDragType)
164169

165-
if let values = rowProvider.rowValues(at: row) {
166-
let formatted = formatRowValues(values: values, columnTypes: rowProvider.columnTypes)
170+
if let values = displayRow(at: row)?.values {
171+
let tableRows = tableRowsProvider()
172+
let formatted = formatRowValues(values: values, columnTypes: tableRows.columnTypes)
167173
item.setString(formatted.joined(separator: "\t"), forType: .string)
168174
item.setString(
169-
HtmlTableEncoder.encode(rows: [formatted], headers: rowProvider.columns),
175+
HtmlTableEncoder.encode(rows: [formatted], headers: tableRows.columns),
170176
forType: .html
171177
)
172178
}

TablePro/Views/Results/DataGridView+TypePicker.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ extension TableViewCoordinator {
1717
) {
1818
guard tableView.view(atColumn: column, row: row, makeIfNecessary: false) != nil else { return }
1919

20-
let currentValue = rowProvider.value(atRow: row, column: columnIndex) ?? ""
20+
let currentValue = cellValue(at: row, column: columnIndex) ?? ""
2121
let dbType = databaseType ?? .mysql
2222

2323
let cellRect = tableView.rect(ofRow: row).intersection(tableView.rect(ofColumn: column))

0 commit comments

Comments
 (0)