Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
da111ec
refactor(datagrid): introduce TableRowsStore alongside RowDataStore
datlechin Apr 28, 2026
3e98e3c
refactor(datagrid): introduce TableRowsController to drive NSTableVie…
datlechin Apr 28, 2026
b7bd7a4
refactor(datagrid): mirror result delivery writes into TableRowsStore
datlechin Apr 28, 2026
9b62697
refactor(datagrid): switch JSON view and export to read TableRows
datlechin Apr 28, 2026
5e1ba86
refactor(datagrid): switch sidebar reads to TableRows
datlechin Apr 28, 2026
a2f5a4c
refactor(datagrid): switch NSTableView delegate reads to TableRows
datlechin Apr 28, 2026
387dc8e
refactor(datagrid): route cell edits through TableRows.edit and Delta
datlechin Apr 28, 2026
b4c1643
refactor(datagrid): RowOperationsManager mutates TableRows and return…
datlechin Apr 28, 2026
45c5181
refactor(datagrid): undo replay returns Delta and supports non-tail i…
datlechin Apr 28, 2026
0b7ea83
refactor(datagrid): sort moves to controller keyed by Row.id
datlechin Apr 28, 2026
2d941b4
refactor(datagrid): display cache moves to coordinator keyed by Row.id
datlechin Apr 28, 2026
e190a7a
refactor(datagrid): replace RowBuffer with TableRows; delete legacy r…
datlechin Apr 28, 2026
566314a
test(datagrid): update fixtures for RowDeltaApplying.applyDelta and S…
datlechin Apr 28, 2026
bcd85e7
fix(favorites): qualify ORDER BY columns in FTS-joined search query
datlechin Apr 28, 2026
417cee7
fix(datagrid): guard commitCellEdit against re-entry from reload-driv…
datlechin Apr 28, 2026
0c39e0b
fix(datagrid): dispatch rowsInserted delta from loadMoreRows and read…
datlechin Apr 28, 2026
7c829c7
fix(datagrid): dispatch fullReplace delta from fetchAllRows
datlechin Apr 28, 2026
79f8703
fix(datagrid): dispatch cellChanged delta from updateCellInTab
datlechin Apr 28, 2026
133bd3d
fix(datagrid): remove TableRows entry on individual tab close
datlechin Apr 28, 2026
08d977a
refactor(datagrid): rebuildColumnMetadataCache reads live TableRows
datlechin Apr 28, 2026
0990dbb
style(datagrid): drop doc comments per no-comments rule
datlechin Apr 28, 2026
7f67c41
fix(datagrid): sync ResultSet snapshot with store on every mutation
datlechin Apr 28, 2026
826a970
fix(datagrid): unwrap optional result from addNewRow/duplicateRow
datlechin Apr 28, 2026
ed45183
fix(datagrid): clear modified cell highlight on undo, keep FK metadat…
datlechin Apr 28, 2026
59acea1
perf(datagrid): stop syncing ResultSet snapshot on every mutation
datlechin Apr 28, 2026
d8c143c
test(datagrid): fix EvictionTests — selected tab is intentionally not…
datlechin Apr 29, 2026
5b633b3
perf(datagrid): updateCache reads live tableRowsProvider so post-Delt…
datlechin Apr 29, 2026
9897050
refactor(datagrid): replace editingCell binding with direct beginEdit…
datlechin Apr 29, 2026
23dccc0
chore: drop DATAGRID_REFACTOR.md handoff doc
datlechin Apr 29, 2026
0e967c2
fix(datagrid): clear cell display cache when row data is replaced
datlechin Apr 29, 2026
87525fc
refactor(datagrid): rename RowDeltaApplying to TableViewCoordinating,…
datlechin Apr 29, 2026
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
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Introduced TableRows, Row, and Delta value types in TablePro/Models/Query/ as the foundation for the data grid row model rewrite. No callers migrated yet (Phase C.1 of the DataGrid refactor).
- Replaced RowBuffer / InMemoryRowProvider / RowDataStore with TableRows / TableRowsStore / TableRowsController. Mutations emit Delta events; the controller drives NSTableView via insertRows / removeRows / reloadData(forRowIndexes:). Sort and the display cache moved off the row provider into the data grid coordinator, keyed by Row.id.
- Routed every TableRows mutation through `mutateActiveTableRows` on MainContentCoordinator so the active ResultSet's snapshot stays in sync with the store. The snapshot now refreshes only when the user switches result sets (saving the outgoing tab, loading the incoming one), so each insert / undo / paste no longer triggers an `@Observable` re-render of the whole editor. Fixes empty cells on Load More and CPU spikes when adding or undoing rows.
- Undo of a cell edit clears the modified-cell highlight: `DataChangeManager.applyDataUndo` now bumps `reloadVersion` so the data grid rebuilds its visual state cache.
- Reloading a table tab keeps cached column metadata (defaults, foreign keys, nullability, enum values) when no fresh schema fetch was needed, so the FK arrow and dropdown chevron stay visible across reloads instead of toggling.
- DataChangeManager extracted a PendingChanges value type that owns cross-collection invariants for cell edits, row insertions, and deletions. DataChangeManager kept undo/redo registration, plugin SQL generation, and the `@Observable` boundary, dropping from ~960 to ~190 lines. The serialization DTO `TabPendingChanges` is renamed to `TabChangeSnapshot` to distinguish it from the live tracker.
- AnyChangeManager uses ChangeManaging protocol instead of closure-based type erasure, removing all runtime `[Any]` downcasts
- Row selection state moved from MainContentView @State to GridSelectionState @Observable class, preventing full view tree invalidation on every row click
Expand Down
2 changes: 2 additions & 0 deletions TablePro/Core/ChangeTracking/AnyChangeManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ protocol ChangeManaging: AnyObject {
var reloadVersion: Int { get }
var canRedo: Bool { get }
var rowChanges: [RowChange] { get }
var insertedRowIndices: Set<Int> { get }
func isRowDeleted(_ rowIndex: Int) -> Bool
func recordCellChange(
rowIndex: Int,
Expand All @@ -30,6 +31,7 @@ final class AnyChangeManager {
var reloadVersion: Int { wrapped.reloadVersion }
var canRedo: Bool { wrapped.canRedo }
var rowChanges: [RowChange] { wrapped.rowChanges }
var insertedRowIndices: Set<Int> { wrapped.insertedRowIndices }

func isRowDeleted(_ rowIndex: Int) -> Bool {
wrapped.isRowDeleted(rowIndex)
Expand Down
45 changes: 36 additions & 9 deletions TablePro/Core/ChangeTracking/DataChangeManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,21 @@ struct UndoResult {
let needsRowRemoval: Bool
let needsRowRestore: Bool
let restoreRow: [String?]?
let delta: Delta

init(
action: UndoAction,
needsRowRemoval: Bool,
needsRowRestore: Bool,
restoreRow: [String?]?,
delta: Delta = .none
) {
self.action = action
self.needsRowRemoval = needsRowRemoval
self.needsRowRestore = needsRowRestore
self.restoreRow = restoreRow
self.delta = delta
}
}

/// Manager for tracking and applying data changes
Expand Down Expand Up @@ -259,7 +274,10 @@ final class DataChangeManager: ChangeManaging {
originalDBValue: newValue, newValue: previousValue, originalRow: originalRow
)
}
lastUndoResult = UndoResult(action: action, needsRowRemoval: false, needsRowRestore: false, restoreRow: nil)
lastUndoResult = UndoResult(
action: action, needsRowRemoval: false, needsRowRestore: false, restoreRow: nil,
delta: .cellChanged(row: rowIndex, column: columnIndex)
)
}

private func applyRowInsertionUndo(rowIndex: Int, action: UndoAction) {
Expand All @@ -274,12 +292,14 @@ final class DataChangeManager: ChangeManaging {
if pending.isRowInserted(rowIndex) {
_ = pending.undoRowInsertion(rowIndex: rowIndex)
lastUndoResult = UndoResult(
action: action, needsRowRemoval: true, needsRowRestore: false, restoreRow: nil
action: action, needsRowRemoval: true, needsRowRestore: false, restoreRow: nil,
delta: .rowsRemoved(IndexSet(integer: rowIndex))
)
} else {
pending.reinsertRow(rowIndex: rowIndex, columns: columns, savedValues: savedValues)
lastUndoResult = UndoResult(
action: action, needsRowRemoval: false, needsRowRestore: true, restoreRow: savedValues
action: action, needsRowRemoval: false, needsRowRestore: true, restoreRow: savedValues,
delta: .rowsInserted(IndexSet(integer: rowIndex))
)
}
}
Expand All @@ -292,12 +312,14 @@ final class DataChangeManager: ChangeManaging {
if pending.isRowDeleted(rowIndex) {
_ = pending.undoRowDeletion(rowIndex: rowIndex)
lastUndoResult = UndoResult(
action: action, needsRowRemoval: false, needsRowRestore: true, restoreRow: originalRow
action: action, needsRowRemoval: false, needsRowRestore: true, restoreRow: originalRow,
delta: .fullReplace
)
} else {
pending.reapplyRowDeletion(rowIndex: rowIndex, originalRow: originalRow)
lastUndoResult = UndoResult(
action: action, needsRowRemoval: true, needsRowRestore: false, restoreRow: nil
action: action, needsRowRemoval: true, needsRowRestore: false, restoreRow: nil,
delta: .fullReplace
)
}
}
Expand All @@ -315,14 +337,16 @@ final class DataChangeManager: ChangeManaging {
_ = pending.undoRowDeletion(rowIndex: rowIndex)
}
lastUndoResult = UndoResult(
action: action, needsRowRemoval: false, needsRowRestore: true, restoreRow: nil
action: action, needsRowRemoval: false, needsRowRestore: true, restoreRow: nil,
delta: .fullReplace
)
} else {
for (rowIndex, originalRow) in rows {
pending.reapplyRowDeletion(rowIndex: rowIndex, originalRow: originalRow)
}
lastUndoResult = UndoResult(
action: action, needsRowRemoval: true, needsRowRestore: false, restoreRow: nil
action: action, needsRowRemoval: true, needsRowRestore: false, restoreRow: nil,
delta: .fullReplace
)
}
}
Expand All @@ -335,15 +359,18 @@ final class DataChangeManager: ChangeManaging {
}

let firstInserted = rowIndices.first.map { pending.isRowInserted($0) } ?? false
let indices = IndexSet(rowIndices)
if firstInserted {
_ = pending.undoBatchRowInsertion(rowIndices: rowIndices, columnCount: columns.count)
lastUndoResult = UndoResult(
action: action, needsRowRemoval: true, needsRowRestore: false, restoreRow: nil
action: action, needsRowRemoval: true, needsRowRestore: false, restoreRow: nil,
delta: .rowsRemoved(indices)
)
} else {
pending.reinsertBatch(rowIndices: rowIndices, rowValues: rowValues, columns: columns)
lastUndoResult = UndoResult(
action: action, needsRowRemoval: false, needsRowRestore: true, restoreRow: nil
action: action, needsRowRemoval: false, needsRowRestore: true, restoreRow: nil,
delta: .rowsInserted(indices)
)
}
}
Expand Down
13 changes: 4 additions & 9 deletions TablePro/Core/Plugins/QueryResultExportDataSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ import Foundation
import os
import TableProPluginKit

/// In-memory `PluginExportDataSource` backed by a RowBuffer snapshot.
/// Allows export plugins (CSV, JSON, SQL, XLSX, MQL) to export query results
/// without modification to the plugins themselves.
final class QueryResultExportDataSource: PluginExportDataSource, @unchecked Sendable {
let databaseTypeId: String

Expand All @@ -20,14 +17,12 @@ final class QueryResultExportDataSource: PluginExportDataSource, @unchecked Send

private static let logger = Logger(subsystem: "com.TablePro", category: "QueryResultExportDataSource")

init(rowBuffer: RowBuffer, databaseType: DatabaseType, driver: DatabaseDriver?) {
init(tableRows: TableRows, databaseType: DatabaseType, driver: DatabaseDriver?) {
self.databaseTypeId = databaseType.rawValue
self.driver = driver

// Snapshot data at init time for thread safety
self.columns = rowBuffer.columns
self.columnTypeNames = rowBuffer.columnTypes.map { $0.rawType ?? "" }
self.rows = rowBuffer.rows
self.columns = tableRows.columns
self.columnTypeNames = tableRows.columnTypes.map { $0.rawType ?? "" }
self.rows = tableRows.rows.map(\.values)
}

func streamRows(table: String, databaseName: String) -> AsyncThrowingStream<PluginStreamElement, Error> {
Expand Down
2 changes: 1 addition & 1 deletion TablePro/Core/Plugins/StreamingQueryExportDataSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
//
// Streaming export data source for query results.
// Re-executes the query and streams rows directly from the database to the export plugin,
// bypassing RowBuffer. Allows exporting large result sets without loading all rows into memory.
// bypassing in-memory storage. Allows exporting large result sets without loading all rows into memory.
//

import Foundation
Expand Down
2 changes: 2 additions & 0 deletions TablePro/Core/SchemaTracking/StructureChangeManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -887,6 +887,8 @@ final class StructureChangeManager: ChangeManaging {

var rowChanges: [RowChange] { [] }

var insertedRowIndices: Set<Int> { [] }

func isRowDeleted(_ rowIndex: Int) -> Bool { false }

func recordCellChange(
Expand Down
6 changes: 3 additions & 3 deletions TablePro/Core/Services/Export/ExportService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -181,15 +181,15 @@ final class ExportService {
// MARK: - Query Results Export

func exportQueryResults(
rowBuffer: RowBuffer,
tableRows: TableRows,
config: ExportConfiguration,
to url: URL
) async throws {
guard let plugin = PluginManager.shared.exportPlugins[config.formatId] else {
throw ExportError.formatNotFound(config.formatId)
}

let totalRows = rowBuffer.rows.count
let totalRows = tableRows.count
state = ExportState(isExporting: true, totalTables: 1, totalRows: totalRows)
isCancelled = false

Expand All @@ -201,7 +201,7 @@ final class ExportService {
}

let dataSource = QueryResultExportDataSource(
rowBuffer: rowBuffer,
tableRows: tableRows,
databaseType: databaseType,
driver: driver
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// TablePro
//
// Pure formatter that transforms raw cell values into display-ready strings.
// Used by InMemoryRowProvider's display cache to compute values once per cell.
// Used by the data grid coordinator's display cache to compute values once per cell.
//

import Foundation
Expand Down
44 changes: 0 additions & 44 deletions TablePro/Core/Services/Query/RowDataStore.swift

This file was deleted.

Loading
Loading