|
| 1 | +// |
| 2 | +// TableRows.swift |
| 3 | +// TablePro |
| 4 | +// |
| 5 | + |
| 6 | +import Foundation |
| 7 | + |
| 8 | +struct TableRows: Sendable { |
| 9 | + var rows: ContiguousArray<Row> |
| 10 | + var columns: [String] |
| 11 | + var columnTypes: [ColumnType] |
| 12 | + var columnDefaults: [String: String?] |
| 13 | + var columnForeignKeys: [String: ForeignKeyInfo] |
| 14 | + var columnEnumValues: [String: [String]] |
| 15 | + var columnNullable: [String: Bool] |
| 16 | + |
| 17 | + init( |
| 18 | + rows: ContiguousArray<Row> = [], |
| 19 | + columns: [String] = [], |
| 20 | + columnTypes: [ColumnType] = [], |
| 21 | + columnDefaults: [String: String?] = [:], |
| 22 | + columnForeignKeys: [String: ForeignKeyInfo] = [:], |
| 23 | + columnEnumValues: [String: [String]] = [:], |
| 24 | + columnNullable: [String: Bool] = [:] |
| 25 | + ) { |
| 26 | + self.rows = rows |
| 27 | + self.columns = columns |
| 28 | + self.columnTypes = columnTypes |
| 29 | + self.columnDefaults = columnDefaults |
| 30 | + self.columnForeignKeys = columnForeignKeys |
| 31 | + self.columnEnumValues = columnEnumValues |
| 32 | + self.columnNullable = columnNullable |
| 33 | + } |
| 34 | + |
| 35 | + var count: Int { rows.count } |
| 36 | + |
| 37 | + func value(at row: Int, column: Int) -> String? { |
| 38 | + guard row >= 0, row < rows.count else { return nil } |
| 39 | + return rows[row][column] |
| 40 | + } |
| 41 | + |
| 42 | + @discardableResult |
| 43 | + mutating func edit(row: Int, column: Int, value: String?) -> Delta { |
| 44 | + guard row >= 0, row < rows.count else { return .none } |
| 45 | + guard column >= 0, column < columns.count else { return .none } |
| 46 | + guard column < rows[row].values.count else { return .none } |
| 47 | + if rows[row].values[column] == value { return .none } |
| 48 | + rows[row].values[column] = value |
| 49 | + return .cellChanged(row: row, column: column) |
| 50 | + } |
| 51 | + |
| 52 | + @discardableResult |
| 53 | + mutating func editMany(_ edits: [(row: Int, column: Int, value: String?)]) -> Delta { |
| 54 | + var changed: Set<CellPosition> = [] |
| 55 | + for edit in edits { |
| 56 | + guard edit.row >= 0, edit.row < rows.count else { continue } |
| 57 | + guard edit.column >= 0, edit.column < columns.count else { continue } |
| 58 | + guard edit.column < rows[edit.row].values.count else { continue } |
| 59 | + if rows[edit.row].values[edit.column] == edit.value { continue } |
| 60 | + rows[edit.row].values[edit.column] = edit.value |
| 61 | + changed.insert(CellPosition(row: edit.row, column: edit.column)) |
| 62 | + } |
| 63 | + if changed.isEmpty { return .none } |
| 64 | + return .cellsChanged(changed) |
| 65 | + } |
| 66 | + |
| 67 | + @discardableResult |
| 68 | + mutating func appendInsertedRow(values: [String?]) -> Delta { |
| 69 | + let normalized = Self.normalize(values: values, toCount: columns.count) |
| 70 | + let row = Row(id: .inserted(UUID()), values: normalized) |
| 71 | + rows.append(row) |
| 72 | + return .rowsInserted(IndexSet(integer: rows.count - 1)) |
| 73 | + } |
| 74 | + |
| 75 | + @discardableResult |
| 76 | + mutating func appendPage(_ pageRows: [[String?]], startingAt offset: Int) -> Delta { |
| 77 | + guard !pageRows.isEmpty else { return .none } |
| 78 | + let firstIndex = rows.count |
| 79 | + for (idx, values) in pageRows.enumerated() { |
| 80 | + let normalized = Self.normalize(values: values, toCount: columns.count) |
| 81 | + rows.append(Row(id: .existing(offset + idx), values: normalized)) |
| 82 | + } |
| 83 | + let lastIndex = rows.count - 1 |
| 84 | + return .rowsInserted(IndexSet(integersIn: firstIndex...lastIndex)) |
| 85 | + } |
| 86 | + |
| 87 | + @discardableResult |
| 88 | + mutating func remove(rowIDs: Set<RowID>) -> Delta { |
| 89 | + guard !rowIDs.isEmpty else { return .none } |
| 90 | + var indices = IndexSet() |
| 91 | + for (index, row) in rows.enumerated() where rowIDs.contains(row.id) { |
| 92 | + indices.insert(index) |
| 93 | + } |
| 94 | + return removeIndices(indices) |
| 95 | + } |
| 96 | + |
| 97 | + @discardableResult |
| 98 | + mutating func remove(at indices: IndexSet) -> Delta { |
| 99 | + let valid = indices.filteredIndexSet { $0 >= 0 && $0 < rows.count } |
| 100 | + return removeIndices(valid) |
| 101 | + } |
| 102 | + |
| 103 | + @discardableResult |
| 104 | + mutating func replace(rows replacementRows: [[String?]], offset: Int = 0) -> Delta { |
| 105 | + var rebuilt = ContiguousArray<Row>() |
| 106 | + rebuilt.reserveCapacity(replacementRows.count) |
| 107 | + for (idx, values) in replacementRows.enumerated() { |
| 108 | + let normalized = Self.normalize(values: values, toCount: columns.count) |
| 109 | + rebuilt.append(Row(id: .existing(offset + idx), values: normalized)) |
| 110 | + } |
| 111 | + rows = rebuilt |
| 112 | + return .fullReplace |
| 113 | + } |
| 114 | + |
| 115 | + @discardableResult |
| 116 | + mutating func updateDisplayMetadata( |
| 117 | + columnTypes: [ColumnType]? = nil, |
| 118 | + columnDefaults: [String: String?]? = nil, |
| 119 | + columnForeignKeys: [String: ForeignKeyInfo]? = nil, |
| 120 | + columnEnumValues: [String: [String]]? = nil, |
| 121 | + columnNullable: [String: Bool]? = nil |
| 122 | + ) -> Delta { |
| 123 | + var didChange = false |
| 124 | + if let columnTypes, columnTypes != self.columnTypes { |
| 125 | + self.columnTypes = columnTypes |
| 126 | + didChange = true |
| 127 | + } |
| 128 | + if let columnDefaults, columnDefaults != self.columnDefaults { |
| 129 | + self.columnDefaults = columnDefaults |
| 130 | + didChange = true |
| 131 | + } |
| 132 | + if let columnForeignKeys, columnForeignKeys != self.columnForeignKeys { |
| 133 | + self.columnForeignKeys = columnForeignKeys |
| 134 | + didChange = true |
| 135 | + } |
| 136 | + if let columnEnumValues, columnEnumValues != self.columnEnumValues { |
| 137 | + self.columnEnumValues = columnEnumValues |
| 138 | + didChange = true |
| 139 | + } |
| 140 | + if let columnNullable, columnNullable != self.columnNullable { |
| 141 | + self.columnNullable = columnNullable |
| 142 | + didChange = true |
| 143 | + } |
| 144 | + return didChange ? .columnsReplaced : .none |
| 145 | + } |
| 146 | + |
| 147 | + static func from( |
| 148 | + queryRows: [[String?]], |
| 149 | + columns: [String], |
| 150 | + columnTypes: [ColumnType], |
| 151 | + columnDefaults: [String: String?] = [:], |
| 152 | + columnForeignKeys: [String: ForeignKeyInfo] = [:], |
| 153 | + columnEnumValues: [String: [String]] = [:], |
| 154 | + columnNullable: [String: Bool] = [:] |
| 155 | + ) -> TableRows { |
| 156 | + var rows = ContiguousArray<Row>() |
| 157 | + rows.reserveCapacity(queryRows.count) |
| 158 | + for (index, values) in queryRows.enumerated() { |
| 159 | + let normalized = normalize(values: values, toCount: columns.count) |
| 160 | + rows.append(Row(id: .existing(index), values: normalized)) |
| 161 | + } |
| 162 | + return TableRows( |
| 163 | + rows: rows, |
| 164 | + columns: columns, |
| 165 | + columnTypes: columnTypes, |
| 166 | + columnDefaults: columnDefaults, |
| 167 | + columnForeignKeys: columnForeignKeys, |
| 168 | + columnEnumValues: columnEnumValues, |
| 169 | + columnNullable: columnNullable |
| 170 | + ) |
| 171 | + } |
| 172 | + |
| 173 | + private mutating func removeIndices(_ indices: IndexSet) -> Delta { |
| 174 | + guard !indices.isEmpty else { return .none } |
| 175 | + for index in indices.reversed() { |
| 176 | + rows.remove(at: index) |
| 177 | + } |
| 178 | + return .rowsRemoved(indices) |
| 179 | + } |
| 180 | + |
| 181 | + private static func normalize(values: [String?], toCount targetCount: Int) -> [String?] { |
| 182 | + if values.count == targetCount { return values } |
| 183 | + if values.count > targetCount { return Array(values.prefix(targetCount)) } |
| 184 | + return values + Array(repeating: nil, count: targetCount - values.count) |
| 185 | + } |
| 186 | +} |
0 commit comments