Skip to content

Commit 402f20a

Browse files
committed
Restore SQLDataTable feature and apply fixes for Swift 6, memory safety, and decimal handling
1 parent 0d687eb commit 402f20a

3 files changed

Lines changed: 141 additions & 46 deletions

File tree

Sources/SQLClientSwift/SQLClient.swift

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,11 @@ public struct SQLClientConnectionOptions: Sendable {
7979

8080
public struct SQLRow: Sendable {
8181
private let storage: [(key: String, value: Sendable)]
82+
internal let columnTypes: [String: Int32]
8283

83-
internal init(_ dict: [(key: String, value: Sendable)]) {
84+
internal init(_ dict: [(key: String, value: Sendable)], columnTypes: [String: Int32]) {
8485
self.storage = dict
86+
self.columnTypes = columnTypes
8587
}
8688

8789
public var columns: [String] { storage.map(\.key) }
@@ -295,16 +297,20 @@ public actor SQLClient {
295297

296298
if numCols > 0 {
297299
var colMeta: [(name: String, type: Int32)] = []
300+
var columnTypes: [String: Int32] = [:]
298301
for i in 1...numCols {
299-
colMeta.append((name: String(cString: dbcolname(conn, Int32(i))), type: dbcoltype(conn, Int32(i))))
302+
let name = String(cString: dbcolname(conn, Int32(i)))
303+
let type = dbcoltype(conn, Int32(i))
304+
colMeta.append((name: name, type: type))
305+
columnTypes[name] = type
300306
}
301307
while dbnextrow(conn) != NO_MORE_ROWS {
302308
var storage: [(key: String, value: Sendable)] = []
303309
for (idx, col) in colMeta.enumerated() {
304310
let colIdx = Int32(idx + 1)
305311
storage.append((key: col.name, value: columnValue(conn: conn, column: colIdx, type: col.type)))
306312
}
307-
table.append(SQLRow(storage))
313+
table.append(SQLRow(storage, columnTypes: columnTypes))
308314
}
309315
}
310316
tables.append(table)
@@ -344,7 +350,7 @@ public actor SQLClient {
344350
return legacyDate(conn: conn, type: type, data: data, len: len)
345351
case 40, 41, 42, 43, 187, 188: // SYBMSDATE, SYBMSTIME, SYBMSDATETIME2, SYBMSDATETIMEOFFSET, SYBBIGDATETIME, SYBBIGTIME
346352
return msDateTime(conn: conn, type: type, data: data, len: len)
347-
case 55, 63, 60, 122, 110: // SYBDECIMAL, SYBNUMERIC, SYBMONEY, SYBMONEY4, SYBMONEYN
353+
case 55, 63, 60, 122, 110, 106, 108: // SYBDECIMAL, SYBNUMERIC, SYBMONEY, SYBMONEY4, SYBMONEYN, SYBDECIMALN, SYBNUMERICN
348354
return convertToDecimal(conn: conn, type: type, data: data, len: len)
349355
case 36: // SYBUNIQUE
350356
guard len == 16 else { return NSNull() }
@@ -369,9 +375,11 @@ public actor SQLClient {
369375

370376
private nonisolated func legacyDate(conn: OpaquePointer, type: Int32, data: UnsafeRawPointer, len: Int32) -> Sendable {
371377
var dbdt = DBDATETIME()
372-
_ = dbconvert(conn, type, data, len, 61, // SYBDATETIME
373-
UnsafeMutableRawPointer(&dbdt).assumingMemoryBound(to: UInt8.self),
374-
Int32(MemoryLayout<DBDATETIME>.size))
378+
_ = withUnsafeMutableBytes(of: &dbdt) { ptr in
379+
dbconvert(conn, type, data, len, 61, // SYBDATETIME
380+
ptr.baseAddress!.assumingMemoryBound(to: UInt8.self),
381+
Int32(MemoryLayout<DBDATETIME>.size))
382+
}
375383
var rec = DBDATEREC()
376384
dbdatecrack(conn, &rec, &dbdt)
377385
var c = DateComponents()
@@ -382,21 +390,27 @@ public actor SQLClient {
382390
}
383391

384392
private nonisolated func msDateTime(conn: OpaquePointer, type: Int32, data: UnsafeRawPointer, len: Int32) -> Sendable {
385-
var buf = [CChar](repeating: 0, count: 64)
386-
let rc = dbconvert(conn, type, data, len, 47, // SYBCHAR
387-
UnsafeMutableRawPointer(&buf).assumingMemoryBound(to: UInt8.self),
388-
Int32(buf.count))
393+
var buf = [CChar](repeating: 0, count: 65)
394+
let count = Int32(64) // Leave last byte as null
395+
let rc = buf.withUnsafeMutableBytes { ptr in
396+
dbconvert(conn, type, data, len, 47, // SYBCHAR
397+
ptr.baseAddress!.assumingMemoryBound(to: UInt8.self),
398+
count)
399+
}
389400
guard rc != FAIL else { return NSNull() }
390401
let str = String(cString: buf).trimmingCharacters(in: .whitespaces)
391402
for fmt in SQLClient.isoFormatters { if let d = fmt.date(from: str) { return d as Sendable } }
392403
return str
393404
}
394405

395406
private nonisolated func convertToDecimal(conn: OpaquePointer, type: Int32, data: UnsafeRawPointer, len: Int32) -> Sendable {
396-
var buf = [CChar](repeating: 0, count: 64)
397-
_ = dbconvert(conn, type, data, len, 47, // SYBCHAR
398-
UnsafeMutableRawPointer(&buf).assumingMemoryBound(to: UInt8.self),
399-
Int32(buf.count))
407+
var buf = [CChar](repeating: 0, count: 65)
408+
let count = Int32(64) // Leave last byte as null
409+
_ = buf.withUnsafeMutableBytes { ptr in
410+
dbconvert(conn, type, data, len, 47, // SYBCHAR
411+
ptr.baseAddress!.assumingMemoryBound(to: UInt8.self),
412+
count)
413+
}
400414
return NSDecimalNumber(string: String(cString: buf).trimmingCharacters(in: .whitespaces))
401415
}
402416

Sources/SQLClientSwift/SQLDataTable.swift

Lines changed: 48 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ public enum SQLCellValue: Codable, Sendable, Equatable {
6060
case uuid(UUID)
6161
case object(String) // JSON-encoded fallback
6262

63-
/// Underlying value as Any? for compatibility with existing code.
64-
public var anyValue: Any? {
63+
/// Underlying value as Sendable? for compatibility with existing code.
64+
public var anyValue: (any Sendable)? {
6565
switch self {
6666
case .null: return nil
6767
case .string(let v): return v
@@ -178,8 +178,10 @@ public enum SQLCellValue: Codable, Sendable, Equatable {
178178
return (raw as? NSNumber).map { .double($0.doubleValue) } ?? .null
179179
case 50, 104: // SYBBIT / SYBBITN
180180
return (raw as? NSNumber).map { .bool($0.boolValue) } ?? .null
181-
case 55, 63, 60, 122, 110: // decimal / numeric / money
182-
return (raw as? NSDecimalNumber).map { .decimal($0.decimalValue) } ?? .null
181+
case 55, 63, 60, 122, 110, 106, 108: // decimal / numeric / money
182+
if let dec = raw as? NSDecimalNumber { return .decimal(dec.decimalValue) }
183+
if let data = raw as? Data { return .bytes(data) }
184+
return .null
183185
case 36: // SYBUNIQUE
184186
return (raw as? UUID).map { .uuid($0) } ?? .null
185187
case 45, 37, 34, 173, 174, 167: // binary types
@@ -201,13 +203,31 @@ public enum SQLCellValue: Codable, Sendable, Equatable {
201203
case 59: return .float
202204
case 62: return .double
203205
case 50, 104: return .boolean
204-
case 55, 63, 60, 122, 110: return .decimal
206+
case 55, 63, 60, 122, 110, 106, 108: return .decimal
205207
case 36: return .guid
206208
case 45, 37, 34, 173, 174, 167: return .byteArray
207209
case 61, 58, 111, 40, 41, 42, 43, 187, 188: return .dateTime
208210
default: return .string
209211
}
210212
}
213+
214+
/// Reverse mapping of SQLColumnType to a representative FreeTDS type code.
215+
internal static func freeTDSType(for type: SQLColumnType) -> Int32 {
216+
switch type {
217+
case .byte: return 48
218+
case .int16: return 52
219+
case .int32: return 56
220+
case .int64: return 127
221+
case .float: return 59
222+
case .double: return 62
223+
case .boolean: return 50
224+
case .decimal: return 55
225+
case .guid: return 36
226+
case .byteArray: return 45
227+
case .dateTime: return 61
228+
default: return 47 // SYBCHAR
229+
}
230+
}
211231
}
212232

213233
// MARK: - SQLDataTable
@@ -286,29 +306,29 @@ public struct SQLDataTable: Codable, Sendable {
286306

287307
/// Decodes each row into a `Decodable` type using column names as coding keys.
288308
public func decode<T: Decodable>(as type: T.Type = T.self) throws -> [T] {
289-
return try rows.map { rowCells in
290-
// Map column names exactly as returned from SQL
291-
var dict: [String: SQLCellValue] = [:]
292-
for (ci, col) in columns.enumerated() where ci < rowCells.count {
293-
dict[col.name] = rowCells[ci]
309+
let colTypes = Dictionary(uniqueKeysWithValues: columns.map { ($0.name, SQLCellValue.freeTDSType(for: $0.type)) })
310+
return try rows.map { rowCells in
311+
let storage: [(key: String, value: Sendable)] = rowCells.enumerated().compactMap { (ci, cell) in
312+
guard ci < columns.count else { return nil }
313+
let value: Sendable = cell.anyValue ?? NSNull()
314+
return (key: columns[ci].name, value: value)
315+
}
316+
return try T(from: SQLRowDecoder(row: SQLRow(storage, columnTypes: colTypes)))
294317
}
295-
let row = SQLRow(dict)
296-
// Use CodingKeys matching struct properties
297-
return try T(from: SQLRowDecoder(row: row))
298318
}
299-
}
300319

301320
// MARK: SQLRow compatibility
302321

303322
/// Converts to `[SQLRow]` for compatibility with existing query methods.
304323
public func toSQLRows() -> [SQLRow] {
305-
rows.map { rowCells in
324+
let colTypes = Dictionary(uniqueKeysWithValues: columns.map { ($0.name, SQLCellValue.freeTDSType(for: $0.type)) })
325+
return rows.map { rowCells in
306326
let storage: [(key: String, value: Sendable)] = rowCells.enumerated().compactMap { (ci, cell) in
307327
guard ci < columns.count else { return nil }
308-
let value: Sendable = cell.anyValue.map { $0 as AnyObject } ?? NSNull()
328+
let value: Sendable = cell.anyValue ?? NSNull()
309329
return (key: columns[ci].name, value: value)
310330
}
311-
return SQLRow(storage)
331+
return SQLRow(storage, columnTypes: colTypes)
312332
}
313333
}
314334
}
@@ -331,11 +351,7 @@ public struct SQLDataSet: Codable, Sendable {
331351

332352
internal init(tables: [SQLDataTable]) { self.tables = tables }
333353
}
334-
extension String {
335-
func caseInsensitiveCompare(_ other: String) -> Bool {
336-
return self.lowercased() == other.lowercased()
337-
}
338-
}
354+
339355
// MARK: - SQLClientResult extension
340356

341357
extension SQLClientResult {
@@ -354,13 +370,22 @@ extension SQLClientResult {
354370
return SQLDataTable(name: "Table\(idx + 1)", columns: [], rows: [])
355371
}
356372
let cols = first.columns.map { name in
357-
// Infer type from first non-null value in the column
373+
// Use FreeTDS type if available in SQLRow
374+
if let tdsType = first.columnTypes[name] {
375+
return SQLDataColumn(name: name, type: SQLCellValue.columnType(for: tdsType))
376+
}
377+
// Infer type from first non-null value in the column (fallback)
358378
let sample = sqlRows.compactMap({ $0[name] }).first(where: { !($0 is NSNull) })
359379
return SQLDataColumn(name: name, type: inferColumnType(from: sample))
360380
}
361381
let dataRows: [[SQLCellValue]] = sqlRows.map { sqlRow in
362382
cols.map { col in
363383
guard let raw = sqlRow[col.name] else { return .null }
384+
// If we have FreeTDS type for this row/column, use it
385+
if let tdsType = sqlRow.columnTypes[col.name] {
386+
let val: Sendable = (raw as AnyObject) as! Sendable
387+
return SQLCellValue.from(raw: val, freeTDSType: tdsType)
388+
}
364389
return cellValueFromAny(raw, columnType: col.type)
365390
}
366391
}

0 commit comments

Comments
 (0)