Skip to content

Commit db754f0

Browse files
vkuttypCopilot
andcommitted
perf: optimize toJson() and decode<T>() in Swift
- toJson(): pre-compute column names once (not per-row), remove .sortedKeys - decode<T>(): add _CellDecoder fast path — skips toSQLRows() round-trip, uses shared pre-built [String:Int] column index (O(1) vs O(n) linear scan) - Static _isoFormatter: mark nonisolated(unsafe) for Swift 6 strict concurrency - Extract _coerce() as file-scope helper shared by both keyed containers Result (20 iterations, 46-row Accounts table): decode<T>(): 2.91ms → 1.59ms (-45%) toJson(): 5.86ms → 2.07ms (-65%) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent d756f51 commit db754f0

2 files changed

Lines changed: 283 additions & 81 deletions

File tree

Sources/CosmoSQLCore/SQLDataTable.swift

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,16 @@ public enum SQLCellValue: Sendable, Equatable {
6666
case .string(let v): return v
6767
case .bytes(let v): return Data(v).base64EncodedString()
6868
case .uuid(let v): return v.uuidString
69-
case .date(let v):
70-
let f = ISO8601DateFormatter()
71-
f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
72-
return f.string(from: v)
69+
case .date(let v): return SQLCellValue._isoFormatter.string(from: v)
7370
}
7471
}
7572

73+
private nonisolated(unsafe) static let _isoFormatter: ISO8601DateFormatter = {
74+
let f = ISO8601DateFormatter()
75+
f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
76+
return f
77+
}()
78+
7679
/// Human-readable string for display / Markdown rendering.
7780
public var displayString: String {
7881
switch self {
@@ -85,10 +88,7 @@ public enum SQLCellValue: Sendable, Equatable {
8588
case .string(let v): return v
8689
case .bytes(let v): return "0x" + v.map { String(format: "%02X", $0) }.joined()
8790
case .uuid(let v): return v.uuidString
88-
case .date(let v):
89-
let f = ISO8601DateFormatter()
90-
f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
91-
return f.string(from: v)
91+
case .date(let v): return SQLCellValue._isoFormatter.string(from: v)
9292
}
9393
}
9494
}
@@ -223,23 +223,33 @@ public struct SQLDataTable: Sendable {
223223

224224
/// Decode all rows into an array of the given `Decodable` type.
225225
public func decode<T: Decodable>(as type: T.Type = T.self) throws -> [T] {
226+
// Build the shared column-name → index map once for all rows.
227+
// This avoids O(columns) linear search per field per row in _RowDecoder.
228+
let colIndex = Dictionary(
229+
columns.enumerated().map { (i, col) in (col.name.lowercased(), i) },
230+
uniquingKeysWith: { first, _ in first }
231+
)
226232
let decoder = SQLRowDecoder()
227-
return try toSQLRows().map { try decoder.decode(T.self, from: $0) }
233+
return try rows.map { cells in
234+
try decoder.decode(T.self, cells: cells, columns: columns, columnIndex: colIndex)
235+
}
228236
}
229237

230238
// MARK: JSON rendering
231239

232240
/// Renders the table as a JSON array of objects (column name → native value).
233241
/// SQL NULL becomes JSON `null`. Dates are ISO-8601 strings.
234242
public func toJson(pretty: Bool = true) -> String {
235-
let array = rows.map { row -> [String: Any?] in
236-
Dictionary(uniqueKeysWithValues: zip(columns.map(\.name), row.map(\.jsonValue)))
237-
}
238-
// JSONSerialization needs [String: Any] with NSNull for nulls
239-
let sanitized = array.map { dict in
240-
dict.mapValues { $0 ?? NSNull() as Any }
243+
// Pre-compute column names once — not per row.
244+
let names = columns.map(\.name)
245+
let sanitized = rows.map { row -> [String: Any] in
246+
var dict = [String: Any](minimumCapacity: names.count)
247+
for (i, cell) in row.enumerated() where i < names.count {
248+
dict[names[i]] = cell.jsonValue ?? NSNull()
249+
}
250+
return dict
241251
}
242-
let opts: JSONSerialization.WritingOptions = pretty ? [.prettyPrinted, .sortedKeys] : []
252+
let opts: JSONSerialization.WritingOptions = pretty ? [.prettyPrinted] : []
243253
let data = (try? JSONSerialization.data(withJSONObject: sanitized, options: opts)) ?? Data()
244254
return String(data: data, encoding: .utf8) ?? "[]"
245255
}

0 commit comments

Comments
 (0)