Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 0 additions & 23 deletions Sources/SQLiteData/StructuredQueries+GRDB/QueryCursor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -248,29 +248,6 @@ extension String {
}
}

extension UUID {
func withLowercasedUTF8Text<R>(_ body: (UnsafePointer<CChar>, Int32) -> R) -> R {
withUnsafeTemporaryAllocation(of: UInt8.self, capacity: 36) { utf8 in
withUnsafeBytes(of: uuid) { bytes in
var offset = 0
for (byteIndex, byte) in bytes.enumerated() {
if byteIndex == 4 || byteIndex == 6 || byteIndex == 8 || byteIndex == 10 {
utf8[offset] = UInt8(ascii: "-")
offset += 1
}
utf8[offset] = hexDigits[Int(byte >> 4)]
utf8[offset + 1] = hexDigits[Int(byte & 0xF)]
offset += 2
}
}
return utf8.baseAddress!.withMemoryRebound(to: CChar.self, capacity: 36) {
body($0, 36)
}
}
}
}

private let hexDigits = Array("0123456789abcdef".utf8)

@usableFromInline
struct Int64OverflowError: Error {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,22 @@ struct SQLiteFunctionDecoder: QueryDecoder {

@usableFromInline
mutating func decode(_ columnType: UUID.Type) throws(QueryDecodingError) -> UUID? {
guard let uuidString = try decode(String.self) else { return nil }
return UUID(uuidString: uuidString)
precondition(argumentCount > currentIndex)
let value = arguments?[Int(currentIndex)]
switch sqlite3_value_type(value) {
case SQLITE_NULL:
currentIndex += 1
return nil
case SQLITE_TEXT:
break
default:
try reportTypeMismatch(UUID.self)
}
defer { currentIndex += 1 }
let text = sqlite3_value_text(value)
let byteCount = Int(sqlite3_value_bytes(value))
let utf8 = UnsafeBufferPointer(start: text, count: byteCount)
return UUID(uuidUTF8: utf8) ?? UUID(uuidString: String(decoding: utf8, as: UTF8.self))
}

@usableFromInline
Expand Down
18 changes: 16 additions & 2 deletions Sources/SQLiteData/StructuredQueries+GRDB/SQLiteQueryDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,22 @@ struct SQLiteQueryDecoder: QueryDecoder {

@inlinable
mutating func decode(_ columnType: UUID.Type) throws(QueryDecodingError) -> UUID? {
guard let uuidString = try decode(String.self) else { return nil }
guard let uuid = UUID(uuidString: uuidString) else { throw .other(InvalidUUID()) }
switch sqlite3_column_type(statement, currentIndex) {
case SQLITE_NULL:
currentIndex += 1
return nil
case SQLITE_TEXT:
break
default:
try reportTypeMismatch(UUID.self)
}
defer { currentIndex += 1 }
let text = sqlite3_column_text(statement, currentIndex)
let byteCount = Int(sqlite3_column_bytes(statement, currentIndex))
let utf8 = UnsafeBufferPointer(start: text, count: byteCount)
if let uuid = UUID(uuidUTF8: utf8) { return uuid }
guard let uuid = UUID(uuidString: String(decoding: utf8, as: UTF8.self))
else { throw .other(InvalidUUID()) }
return uuid
}

Expand Down
56 changes: 56 additions & 0 deletions Sources/SQLiteData/StructuredQueries+GRDB/UUID+UUIDString.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
public import Foundation

extension UUID {
@usableFromInline
init?(uuidUTF8 utf8: UnsafeBufferPointer<UInt8>) {
guard utf8.count == 36 else { return nil }
var raw: uuid_t = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
let parsed = withUnsafeMutableBytes(of: &raw) { bytes in
var index = 0
for byteIndex in 0..<16 {
if byteIndex == 4 || byteIndex == 6 || byteIndex == 8 || byteIndex == 10 {
guard utf8[index] == UInt8(ascii: "-") else { return false }
index += 1
}
guard let high = hexValue(utf8[index]), let low = hexValue(utf8[index + 1])
else { return false }
bytes[byteIndex] = high << 4 | low
index += 2
}
return true
}
guard parsed else { return nil }
self.init(uuid: raw)
}

func withLowercasedUTF8Text<R>(_ body: (UnsafePointer<CChar>, Int32) -> R) -> R {
withUnsafeTemporaryAllocation(of: UInt8.self, capacity: 36) { utf8 in
withUnsafeBytes(of: uuid) { bytes in
var offset = 0
for (byteIndex, byte) in bytes.enumerated() {
if byteIndex == 4 || byteIndex == 6 || byteIndex == 8 || byteIndex == 10 {
utf8[offset] = UInt8(ascii: "-")
offset += 1
}
utf8[offset] = hexDigits[Int(byte >> 4)]
utf8[offset + 1] = hexDigits[Int(byte & 0xF)]
offset += 2
}
}
return utf8.baseAddress!.withMemoryRebound(to: CChar.self, capacity: 36) {
body($0, 36)
}
}
}
}

private func hexValue(_ byte: UInt8) -> UInt8? {
switch byte {
case UInt8(ascii: "0")...UInt8(ascii: "9"): byte - UInt8(ascii: "0")
case UInt8(ascii: "a")...UInt8(ascii: "f"): byte - UInt8(ascii: "a") + 10
case UInt8(ascii: "A")...UInt8(ascii: "F"): byte - UInt8(ascii: "A") + 10
default: nil
}
}

private let hexDigits = Array("0123456789abcdef".utf8)
31 changes: 31 additions & 0 deletions Tests/SQLiteDataTests/UUIDTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import DependenciesTestSupport
import Foundation
import SQLiteData
import Testing

@Suite(.dependency(\.defaultDatabase, try DatabaseQueue()))
struct UUIDTests {
@Dependency(\.defaultDatabase) var database

@Test func `decode matches Foundation parsing`() throws {
try database.read { db in
for text in [
"deadbeef-dead-beef-dead-beefdeadbeef",
"DEADBEEF-DEAD-BEEF-DEAD-BEEFDEADBEEF",
"A1b2C3d4-E5f6-7890-aB12-Cd34eF567890",
"00000000-0000-0000-0000-000000000000",
] {
let decoded = try #sql("SELECT \(bind: text)", as: UUID.self).fetchOne(db)
#expect(decoded == UUID(uuidString: text), "\(text)")
}
}
}

@Test func roundtrip() throws {
let uuid = UUID()
try database.read { db in
let decoded = try #sql("SELECT \(bind: uuid)", as: UUID.self).fetchOne(db)
#expect(decoded == uuid)
}
}
}