From e58e323ea8d52d3a682128a527da3ea1e0d174a9 Mon Sep 17 00:00:00 2001 From: Adam Wulf Date: Wed, 20 Oct 2021 18:04:37 -0500 Subject: [PATCH 1/6] Support unsigned int32/64 types for columns. UInt64 is stored as a Blob type --- Sources/SQLite/Core/Value.swift | 35 +++++++++++++++++++++++++++++ Tests/SQLiteTests/SchemaTests.swift | 8 +++++++ Tests/SQLiteTests/SelectTests.swift | 17 +++++++++++--- Tests/SQLiteTests/TestHelpers.swift | 6 +++++ 4 files changed, 63 insertions(+), 3 deletions(-) diff --git a/Sources/SQLite/Core/Value.swift b/Sources/SQLite/Core/Value.swift index 9c463f0c..64541d57 100644 --- a/Sources/SQLite/Core/Value.swift +++ b/Sources/SQLite/Core/Value.swift @@ -73,6 +73,27 @@ extension Int64: Number, Value { } +extension UInt64: Number, Value { + + public static let declaredDatatype = Blob.declaredDatatype + + public static func fromDatatypeValue(_ datatypeValue: Blob) -> UInt64 { + guard datatypeValue.bytes.count >= MemoryLayout.size else { return 0 } + let bigEndianUInt64 = datatypeValue.bytes.withUnsafeBytes({ $0.load(as: UInt64.self )}) + return UInt64(bigEndian: bigEndianUInt64) + } + + public var datatypeValue: Blob { + var bytes: [UInt8] = [] + withUnsafeBytes(of: self) { pointer in + // little endian by default on iOS/macOS, so reverse to get bigEndian + bytes.append(contentsOf: pointer.reversed()) + } + return Blob(bytes: bytes) + } + +} + extension String: Binding, Value { public static let declaredDatatype = "TEXT" @@ -130,3 +151,17 @@ extension Int: Number, Value { } } + +extension UInt32: Number, Value { + + public static var declaredDatatype = Int64.declaredDatatype + + public static func fromDatatypeValue(_ datatypeValue: Int64) -> UInt32 { + UInt32(datatypeValue) + } + + public var datatypeValue: Int64 { + Int64(self) + } + +} diff --git a/Tests/SQLiteTests/SchemaTests.swift b/Tests/SQLiteTests/SchemaTests.swift index 495a5e51..49390421 100644 --- a/Tests/SQLiteTests/SchemaTests.swift +++ b/Tests/SQLiteTests/SchemaTests.swift @@ -27,6 +27,10 @@ class SchemaTests: XCTestCase { "\"doubleOptional\" REAL, " + "\"int64\" INTEGER NOT NULL, " + "\"int64Optional\" INTEGER, " + + "\"uint32\" INTEGER NOT NULL, " + + "\"uint32Optional\" INTEGER, " + + "\"uint64\" BLOB NOT NULL, " + + "\"uint64Optional\" BLOB, " + "\"string\" TEXT NOT NULL, " + "\"stringOptional\" TEXT" + ")", @@ -37,6 +41,10 @@ class SchemaTests: XCTestCase { t.column(doubleOptional) t.column(int64) t.column(int64Optional) + t.column(uint32) + t.column(uint32Optional) + t.column(uint64) + t.column(uint64Optional) t.column(string) t.column(stringOptional) } diff --git a/Tests/SQLiteTests/SelectTests.swift b/Tests/SQLiteTests/SelectTests.swift index d1126b66..53b8c13f 100644 --- a/Tests/SQLiteTests/SelectTests.swift +++ b/Tests/SQLiteTests/SelectTests.swift @@ -14,7 +14,9 @@ class SelectTests: SQLiteTestCase { CREATE TABLE users_name ( id INTEGER, user_id INTEGER REFERENCES users(id), - name TEXT + name TEXT, + step_count BLOB, + stair_count INTEGER ) """ ) @@ -27,18 +29,27 @@ class SelectTests: SQLiteTestCase { let name = Expression("name") let id = Expression("id") let userID = Expression("user_id") + let stepCount = Expression("step_count") + let stairCount = Expression("stair_count") let email = Expression("email") + // use UInt64.max - 1 to test Endianness - it should store/load as big endian + let reallyBigNumber = UInt64.max - 1 + let prettyBigNumber = UInt32.max - 1 try! insertUser("Joey") try! db.run(usersData.insert( id <- 1, userID <- 1, - name <- "Joey" + name <- "Joey", + stepCount <- reallyBigNumber, + stairCount <- prettyBigNumber )) - try! db.prepare(users.select(name, email).join(usersData, on: userID == users[id])).forEach { + try! db.prepare(users.select(name, email, stepCount, stairCount).join(usersData, on: userID == users[id])).forEach { XCTAssertEqual($0[name], "Joey") XCTAssertEqual($0[email], "Joey@example.com") + XCTAssertEqual($0[stepCount], reallyBigNumber) + XCTAssertEqual($0[stairCount], prettyBigNumber) } } } diff --git a/Tests/SQLiteTests/TestHelpers.swift b/Tests/SQLiteTests/TestHelpers.swift index 07cc6f06..4dc5697c 100644 --- a/Tests/SQLiteTests/TestHelpers.swift +++ b/Tests/SQLiteTests/TestHelpers.swift @@ -92,6 +92,12 @@ let intOptional = Expression("intOptional") let int64 = Expression("int64") let int64Optional = Expression("int64Optional") +let uint32 = Expression("uint32") +let uint32Optional = Expression("uint32Optional") + +let uint64 = Expression("uint64") +let uint64Optional = Expression("uint64Optional") + let string = Expression("string") let stringOptional = Expression("stringOptional") From 17d4b90f6a8f6226233f6754f4a86937332a9eed Mon Sep 17 00:00:00 2001 From: Adam Wulf Date: Wed, 20 Oct 2021 18:18:54 -0500 Subject: [PATCH 2/6] Fixed test helpers data type --- Tests/SQLiteTests/TestHelpers.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/SQLiteTests/TestHelpers.swift b/Tests/SQLiteTests/TestHelpers.swift index 4dc5697c..b1e5ab8b 100644 --- a/Tests/SQLiteTests/TestHelpers.swift +++ b/Tests/SQLiteTests/TestHelpers.swift @@ -92,8 +92,8 @@ let intOptional = Expression("intOptional") let int64 = Expression("int64") let int64Optional = Expression("int64Optional") -let uint32 = Expression("uint32") -let uint32Optional = Expression("uint32Optional") +let uint32 = Expression("uint32") +let uint32Optional = Expression("uint32Optional") let uint64 = Expression("uint64") let uint64Optional = Expression("uint64Optional") From 69bac352f745fc74394fa0784a1ed5103a678a9a Mon Sep 17 00:00:00 2001 From: Adam Wulf Date: Wed, 20 Oct 2021 18:33:01 -0500 Subject: [PATCH 3/6] Also allow comparing two blobs as if they were giant unsigned ints --- Sources/SQLite/Core/Blob.swift | 20 +++++++++++++++++++ .../SQLiteTests/AggregateFunctionsTests.swift | 2 ++ Tests/SQLiteTests/FoundationTests.swift | 13 ++++++++++++ 3 files changed, 35 insertions(+) diff --git a/Sources/SQLite/Core/Blob.swift b/Sources/SQLite/Core/Blob.swift index cd31483b..faa4afa4 100644 --- a/Sources/SQLite/Core/Blob.swift +++ b/Sources/SQLite/Core/Blob.swift @@ -58,3 +58,23 @@ extension Blob: Equatable { public func ==(lhs: Blob, rhs: Blob) -> Bool { lhs.bytes == rhs.bytes } + +extension Blob: Comparable { + public static func < (lhs: Blob, rhs: Blob) -> Bool { + // put most sig digit at the end + let lBytes: [UInt8] = lhs.bytes.reversed() + let rBytes: [UInt8] = rhs.bytes.reversed() + + for i in stride(from: max(lhs.bytes.count, rhs.bytes.count) - 1, to: 0, by: -1) { + let lVal = i < lBytes.count ? lBytes[i] : 0 + let rVal = i < rBytes.count ? rBytes[i] : 0 + if lVal < rVal { + return true + } else if lVal > rVal { + return false + } + } + return true // lhs.bytes == rhs.bytes + } + +} diff --git a/Tests/SQLiteTests/AggregateFunctionsTests.swift b/Tests/SQLiteTests/AggregateFunctionsTests.swift index 71ac79fb..952d5e74 100644 --- a/Tests/SQLiteTests/AggregateFunctionsTests.swift +++ b/Tests/SQLiteTests/AggregateFunctionsTests.swift @@ -21,6 +21,8 @@ class AggregateFunctionsTests: XCTestCase { func test_max_wrapsComparableExpressionsWithMaxFunction() { assertSQL("max(\"int\")", int.max) assertSQL("max(\"intOptional\")", intOptional.max) + assertSQL("max(\"uint64\")", uint64.max) + assertSQL("max(\"uint64Optional\")", uint64Optional.max) assertSQL("max(\"double\")", double.max) assertSQL("max(\"doubleOptional\")", doubleOptional.max) assertSQL("max(\"string\")", string.max) diff --git a/Tests/SQLiteTests/FoundationTests.swift b/Tests/SQLiteTests/FoundationTests.swift index cef485fc..0c1d0f89 100644 --- a/Tests/SQLiteTests/FoundationTests.swift +++ b/Tests/SQLiteTests/FoundationTests.swift @@ -25,4 +25,17 @@ class FoundationTests: XCTestCase { let uuid = UUID.fromDatatypeValue(string) XCTAssertEqual(UUID(uuidString: "4ABE10C9-FF12-4CD4-90C1-4B429001BAD3"), uuid) } + + func testCompareBlob() { + let data1 = Data([1, 2, 3]) + let data2 = Data([1, 3, 3]) + let data3 = Data([4, 3]) + let blob1 = data1.datatypeValue + let blob2 = data2.datatypeValue + let blob3 = data3.datatypeValue + XCTAssert(blob1 < blob2) + XCTAssert(blob2 > blob1) + XCTAssert(blob1 > blob3) + XCTAssert(blob2 > blob3) + } } From 14734e0541fea9319362241fbe5603ebc0b0596b Mon Sep 17 00:00:00 2001 From: Adam Wulf Date: Thu, 21 Oct 2021 12:16:54 -0500 Subject: [PATCH 4/6] variable name for linting --- Sources/SQLite/Core/Blob.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/SQLite/Core/Blob.swift b/Sources/SQLite/Core/Blob.swift index faa4afa4..a7fffdc0 100644 --- a/Sources/SQLite/Core/Blob.swift +++ b/Sources/SQLite/Core/Blob.swift @@ -65,9 +65,9 @@ extension Blob: Comparable { let lBytes: [UInt8] = lhs.bytes.reversed() let rBytes: [UInt8] = rhs.bytes.reversed() - for i in stride(from: max(lhs.bytes.count, rhs.bytes.count) - 1, to: 0, by: -1) { - let lVal = i < lBytes.count ? lBytes[i] : 0 - let rVal = i < rBytes.count ? rBytes[i] : 0 + for byteIndex in stride(from: max(lhs.bytes.count, rhs.bytes.count) - 1, to: 0, by: -1) { + let lVal = byteIndex < lBytes.count ? lBytes[byteIndex] : 0 + let rVal = byteIndex < rBytes.count ? rBytes[byteIndex] : 0 if lVal < rVal { return true } else if lVal > rVal { From 9a75697baf1c0a52bea81a029597252d360299f0 Mon Sep 17 00:00:00 2001 From: Adam Wulf Date: Thu, 21 Oct 2021 12:40:01 -0500 Subject: [PATCH 5/6] Explicit bind for UInt64 --- Sources/SQLite/Core/Statement.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/SQLite/Core/Statement.swift b/Sources/SQLite/Core/Statement.swift index 1e2489b5..e4d54d71 100644 --- a/Sources/SQLite/Core/Statement.swift +++ b/Sources/SQLite/Core/Statement.swift @@ -108,6 +108,8 @@ public final class Statement { sqlite3_bind_double(handle, Int32(idx), value) } else if let value = value as? Int64 { sqlite3_bind_int64(handle, Int32(idx), value) + } else if let value = value as? UInt64 { + sqlite3_bind_blob(handle, Int32(idx), value.datatypeValue.bytes, Int32(value.datatypeValue.bytes.count), SQLITE_TRANSIENT) } else if let value = value as? String { sqlite3_bind_text(handle, Int32(idx), value, -1, SQLITE_TRANSIENT) } else if let value = value as? Int { From 6537109cda4851d8bcac036c173622f6b3c84f88 Mon Sep 17 00:00:00 2001 From: Adam Wulf Date: Thu, 21 Oct 2021 14:05:07 -0500 Subject: [PATCH 6/6] Adding test for binding UInt64 and 32 --- Sources/SQLite/Core/Statement.swift | 2 ++ Tests/SQLiteTests/SelectTests.swift | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Sources/SQLite/Core/Statement.swift b/Sources/SQLite/Core/Statement.swift index e4d54d71..c9d9f944 100644 --- a/Sources/SQLite/Core/Statement.swift +++ b/Sources/SQLite/Core/Statement.swift @@ -110,6 +110,8 @@ public final class Statement { sqlite3_bind_int64(handle, Int32(idx), value) } else if let value = value as? UInt64 { sqlite3_bind_blob(handle, Int32(idx), value.datatypeValue.bytes, Int32(value.datatypeValue.bytes.count), SQLITE_TRANSIENT) + } else if let value = value as? UInt32 { + sqlite3_bind_int64(handle, Int32(idx), value.datatypeValue) } else if let value = value as? String { sqlite3_bind_text(handle, Int32(idx), value, -1, SQLITE_TRANSIENT) } else if let value = value as? Int { diff --git a/Tests/SQLiteTests/SelectTests.swift b/Tests/SQLiteTests/SelectTests.swift index 53b8c13f..1e8038b2 100644 --- a/Tests/SQLiteTests/SelectTests.swift +++ b/Tests/SQLiteTests/SelectTests.swift @@ -22,7 +22,7 @@ class SelectTests: SQLiteTestCase { ) } - func test_select_columns_from_multiple_tables() { + func test_select_columns_from_multiple_tables() throws { let usersData = Table("users_name") let users = Table("users") @@ -51,5 +51,9 @@ class SelectTests: SQLiteTestCase { XCTAssertEqual($0[stepCount], reallyBigNumber) XCTAssertEqual($0[stairCount], prettyBigNumber) } + + // ensure we can bind UInt64 and UInt32 + _ = try db.run("SELECT * FROM \"users_name\" WHERE step_count = ? AND stair_count = ?", + reallyBigNumber, prettyBigNumber) } }