-
-
Notifications
You must be signed in to change notification settings - Fork 285
refactor(datagrid): introduce TableRows / Row / Delta value types #930
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
28c5203
refactor(datagrid): introduce Row and Delta value types for Phase C
datlechin 7d0d0dc
refactor(datagrid): introduce TableRows value type alongside RowBuffer
datlechin 8788bc5
docs: note DataGrid Phase C.1 in CHANGELOG
datlechin d2cf3e6
test(datagrid): cover replace offset, editMany OOB column, factory pa…
datlechin File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| // | ||
| // Delta.swift | ||
| // TablePro | ||
| // | ||
|
|
||
| import Foundation | ||
|
|
||
| enum Delta: Equatable { | ||
| case cellChanged(row: Int, column: Int) | ||
| case cellsChanged(Set<CellPosition>) | ||
| case rowsInserted(IndexSet) | ||
| case rowsRemoved(IndexSet) | ||
| case columnsReplaced | ||
| case fullReplace | ||
|
|
||
| static let none = Delta.cellsChanged([]) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| // | ||
| // Row.swift | ||
| // TablePro | ||
| // | ||
|
|
||
| import Foundation | ||
|
|
||
| enum RowID: Hashable, Sendable { | ||
| case existing(Int) | ||
| case inserted(UUID) | ||
|
|
||
| var isInserted: Bool { | ||
| if case .inserted = self { return true } | ||
| return false | ||
| } | ||
| } | ||
|
|
||
| struct Row: Equatable, Sendable { | ||
| var id: RowID | ||
| var values: [String?] | ||
|
|
||
| subscript(column: Int) -> String? { | ||
| get { column >= 0 && column < values.count ? values[column] : nil } | ||
| set { | ||
| guard column >= 0, column < values.count else { return } | ||
| values[column] = newValue | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,186 @@ | ||
| // | ||
| // TableRows.swift | ||
| // TablePro | ||
| // | ||
|
|
||
| import Foundation | ||
|
|
||
| struct TableRows: Sendable { | ||
| var rows: ContiguousArray<Row> | ||
| var columns: [String] | ||
| var columnTypes: [ColumnType] | ||
| var columnDefaults: [String: String?] | ||
| var columnForeignKeys: [String: ForeignKeyInfo] | ||
| var columnEnumValues: [String: [String]] | ||
| var columnNullable: [String: Bool] | ||
|
|
||
| init( | ||
| rows: ContiguousArray<Row> = [], | ||
| columns: [String] = [], | ||
| columnTypes: [ColumnType] = [], | ||
| columnDefaults: [String: String?] = [:], | ||
| columnForeignKeys: [String: ForeignKeyInfo] = [:], | ||
| columnEnumValues: [String: [String]] = [:], | ||
| columnNullable: [String: Bool] = [:] | ||
| ) { | ||
| self.rows = rows | ||
| self.columns = columns | ||
| self.columnTypes = columnTypes | ||
| self.columnDefaults = columnDefaults | ||
| self.columnForeignKeys = columnForeignKeys | ||
| self.columnEnumValues = columnEnumValues | ||
| self.columnNullable = columnNullable | ||
| } | ||
|
|
||
| var count: Int { rows.count } | ||
|
|
||
| func value(at row: Int, column: Int) -> String? { | ||
| guard row >= 0, row < rows.count else { return nil } | ||
| return rows[row][column] | ||
| } | ||
|
|
||
| @discardableResult | ||
| mutating func edit(row: Int, column: Int, value: String?) -> Delta { | ||
| guard row >= 0, row < rows.count else { return .none } | ||
| guard column >= 0, column < columns.count else { return .none } | ||
| guard column < rows[row].values.count else { return .none } | ||
| if rows[row].values[column] == value { return .none } | ||
| rows[row].values[column] = value | ||
| return .cellChanged(row: row, column: column) | ||
| } | ||
|
|
||
| @discardableResult | ||
| mutating func editMany(_ edits: [(row: Int, column: Int, value: String?)]) -> Delta { | ||
| var changed: Set<CellPosition> = [] | ||
| for edit in edits { | ||
| guard edit.row >= 0, edit.row < rows.count else { continue } | ||
| guard edit.column >= 0, edit.column < columns.count else { continue } | ||
| guard edit.column < rows[edit.row].values.count else { continue } | ||
| if rows[edit.row].values[edit.column] == edit.value { continue } | ||
| rows[edit.row].values[edit.column] = edit.value | ||
| changed.insert(CellPosition(row: edit.row, column: edit.column)) | ||
| } | ||
| if changed.isEmpty { return .none } | ||
| return .cellsChanged(changed) | ||
| } | ||
|
|
||
| @discardableResult | ||
| mutating func appendInsertedRow(values: [String?]) -> Delta { | ||
| let normalized = Self.normalize(values: values, toCount: columns.count) | ||
| let row = Row(id: .inserted(UUID()), values: normalized) | ||
| rows.append(row) | ||
| return .rowsInserted(IndexSet(integer: rows.count - 1)) | ||
| } | ||
|
|
||
| @discardableResult | ||
| mutating func appendPage(_ pageRows: [[String?]], startingAt offset: Int) -> Delta { | ||
| guard !pageRows.isEmpty else { return .none } | ||
| let firstIndex = rows.count | ||
| for (idx, values) in pageRows.enumerated() { | ||
| let normalized = Self.normalize(values: values, toCount: columns.count) | ||
| rows.append(Row(id: .existing(offset + idx), values: normalized)) | ||
| } | ||
| let lastIndex = rows.count - 1 | ||
| return .rowsInserted(IndexSet(integersIn: firstIndex...lastIndex)) | ||
| } | ||
|
|
||
| @discardableResult | ||
| mutating func remove(rowIDs: Set<RowID>) -> Delta { | ||
| guard !rowIDs.isEmpty else { return .none } | ||
| var indices = IndexSet() | ||
| for (index, row) in rows.enumerated() where rowIDs.contains(row.id) { | ||
| indices.insert(index) | ||
| } | ||
| return removeIndices(indices) | ||
| } | ||
|
|
||
| @discardableResult | ||
| mutating func remove(at indices: IndexSet) -> Delta { | ||
| let valid = indices.filteredIndexSet { $0 >= 0 && $0 < rows.count } | ||
| return removeIndices(valid) | ||
| } | ||
|
|
||
| @discardableResult | ||
| mutating func replace(rows replacementRows: [[String?]], offset: Int = 0) -> Delta { | ||
| var rebuilt = ContiguousArray<Row>() | ||
| rebuilt.reserveCapacity(replacementRows.count) | ||
| for (idx, values) in replacementRows.enumerated() { | ||
| let normalized = Self.normalize(values: values, toCount: columns.count) | ||
| rebuilt.append(Row(id: .existing(offset + idx), values: normalized)) | ||
| } | ||
| rows = rebuilt | ||
| return .fullReplace | ||
| } | ||
|
|
||
| @discardableResult | ||
| mutating func updateDisplayMetadata( | ||
| columnTypes: [ColumnType]? = nil, | ||
| columnDefaults: [String: String?]? = nil, | ||
| columnForeignKeys: [String: ForeignKeyInfo]? = nil, | ||
| columnEnumValues: [String: [String]]? = nil, | ||
| columnNullable: [String: Bool]? = nil | ||
| ) -> Delta { | ||
| var didChange = false | ||
| if let columnTypes, columnTypes != self.columnTypes { | ||
| self.columnTypes = columnTypes | ||
| didChange = true | ||
| } | ||
| if let columnDefaults, columnDefaults != self.columnDefaults { | ||
| self.columnDefaults = columnDefaults | ||
| didChange = true | ||
| } | ||
| if let columnForeignKeys, columnForeignKeys != self.columnForeignKeys { | ||
| self.columnForeignKeys = columnForeignKeys | ||
| didChange = true | ||
| } | ||
| if let columnEnumValues, columnEnumValues != self.columnEnumValues { | ||
| self.columnEnumValues = columnEnumValues | ||
| didChange = true | ||
| } | ||
| if let columnNullable, columnNullable != self.columnNullable { | ||
| self.columnNullable = columnNullable | ||
| didChange = true | ||
| } | ||
| return didChange ? .columnsReplaced : .none | ||
| } | ||
|
|
||
| static func from( | ||
| queryRows: [[String?]], | ||
| columns: [String], | ||
| columnTypes: [ColumnType], | ||
| columnDefaults: [String: String?] = [:], | ||
| columnForeignKeys: [String: ForeignKeyInfo] = [:], | ||
| columnEnumValues: [String: [String]] = [:], | ||
| columnNullable: [String: Bool] = [:] | ||
| ) -> TableRows { | ||
| var rows = ContiguousArray<Row>() | ||
| rows.reserveCapacity(queryRows.count) | ||
| for (index, values) in queryRows.enumerated() { | ||
| let normalized = normalize(values: values, toCount: columns.count) | ||
| rows.append(Row(id: .existing(index), values: normalized)) | ||
| } | ||
| return TableRows( | ||
| rows: rows, | ||
| columns: columns, | ||
| columnTypes: columnTypes, | ||
| columnDefaults: columnDefaults, | ||
| columnForeignKeys: columnForeignKeys, | ||
| columnEnumValues: columnEnumValues, | ||
| columnNullable: columnNullable | ||
| ) | ||
| } | ||
|
|
||
| private mutating func removeIndices(_ indices: IndexSet) -> Delta { | ||
| guard !indices.isEmpty else { return .none } | ||
| for index in indices.reversed() { | ||
| rows.remove(at: index) | ||
| } | ||
| return .rowsRemoved(indices) | ||
| } | ||
|
|
||
| private static func normalize(values: [String?], toCount targetCount: Int) -> [String?] { | ||
| if values.count == targetCount { return values } | ||
| if values.count > targetCount { return Array(values.prefix(targetCount)) } | ||
| return values + Array(repeating: nil, count: targetCount - values.count) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| // | ||
| // DeltaTests.swift | ||
| // TableProTests | ||
| // | ||
|
|
||
| import Foundation | ||
| @testable import TablePro | ||
| import Testing | ||
|
|
||
| @Suite("Delta") | ||
| struct DeltaTests { | ||
| @Test("cellChanged equality matches on row and column") | ||
| func cellChangedEquality() { | ||
| let lhs = Delta.cellChanged(row: 1, column: 2) | ||
| let rhs = Delta.cellChanged(row: 1, column: 2) | ||
| let other = Delta.cellChanged(row: 2, column: 2) | ||
| #expect(lhs == rhs) | ||
| #expect(lhs != other) | ||
| } | ||
|
|
||
| @Test("cellsChanged equality matches on the underlying set") | ||
| func cellsChangedEquality() { | ||
| let lhs = Delta.cellsChanged([CellPosition(row: 0, column: 1), CellPosition(row: 2, column: 3)]) | ||
| let rhs = Delta.cellsChanged([CellPosition(row: 2, column: 3), CellPosition(row: 0, column: 1)]) | ||
| #expect(lhs == rhs) | ||
| } | ||
|
|
||
| @Test("rowsInserted equality matches on the underlying IndexSet") | ||
| func rowsInsertedEquality() { | ||
| let lhs = Delta.rowsInserted(IndexSet(0...2)) | ||
| let rhs = Delta.rowsInserted(IndexSet(0...2)) | ||
| let other = Delta.rowsInserted(IndexSet(0...3)) | ||
| #expect(lhs == rhs) | ||
| #expect(lhs != other) | ||
| } | ||
|
|
||
| @Test("rowsRemoved equality matches on the underlying IndexSet") | ||
| func rowsRemovedEquality() { | ||
| let lhs = Delta.rowsRemoved(IndexSet([1, 3])) | ||
| let rhs = Delta.rowsRemoved(IndexSet([1, 3])) | ||
| let other = Delta.rowsRemoved(IndexSet([1, 4])) | ||
| #expect(lhs == rhs) | ||
| #expect(lhs != other) | ||
| } | ||
|
|
||
| @Test("columnsReplaced equals itself") | ||
| func columnsReplacedEquality() { | ||
| let lhs = Delta.columnsReplaced | ||
| let rhs = Delta.columnsReplaced | ||
| #expect(lhs == rhs) | ||
| } | ||
|
|
||
| @Test("fullReplace equals itself") | ||
| func fullReplaceEquality() { | ||
| let lhs = Delta.fullReplace | ||
| let rhs = Delta.fullReplace | ||
| #expect(lhs == rhs) | ||
| } | ||
|
|
||
| @Test("Delta.none is an empty cellsChanged set") | ||
| func noneIsEmptyCellsChanged() { | ||
| #expect(Delta.none == Delta.cellsChanged([])) | ||
| } | ||
|
|
||
| @Test("Distinct cases never compare equal") | ||
| func distinctCasesAreUnequal() { | ||
| let single = Delta.cellChanged(row: 0, column: 0) | ||
| let many = Delta.cellsChanged([CellPosition(row: 0, column: 0)]) | ||
| let inserted = Delta.rowsInserted(IndexSet(integer: 0)) | ||
| let removed = Delta.rowsRemoved(IndexSet(integer: 0)) | ||
| #expect(single != many) | ||
| #expect(single != inserted) | ||
| #expect(many != removed) | ||
| #expect(inserted != removed) | ||
| #expect(Delta.columnsReplaced != Delta.fullReplace) | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updateDisplayMetadatacurrently decides whether foreign keys changed by comparing[String: ForeignKeyInfo]directly, butForeignKeyInfohas a synthesized equality that includes itsidUUID (QueryResult.swift), which is regenerated on each construction. In practice, reloading identical FK metadata from the backend will often look different and return.columnsReplaced, causing unnecessary full metadata invalidations/reloads even when nothing semantically changed.Useful? React with 👍 / 👎.