diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a12f377..49590fbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,8 @@ * Fix `insertMany([Encodable])` ([#1130][], [#1138][]) * Fix incorrect spelling of `remove_diacritics` ([#1128][]) * Fix project build order ([#1131][]) -* Performance improvements ([#1109][], [#1115][], [#1132][]) +* Blob performance improvements ([#416][], [#1167][]) +* Various performance improvements ([#1109][], [#1115][], [#1132][]) * Removed FTS3/4 tokenizer integration (`registerTokenizer`, [#1104][], [#1144][]) 0.13.3 (27-03-2022), [diff][diff-0.13.3] @@ -130,6 +131,7 @@ [#30]: https://github.com/stephencelis/SQLite.swift/issues/30 [#142]: https://github.com/stephencelis/SQLite.swift/issues/142 [#315]: https://github.com/stephencelis/SQLite.swift/issues/315 +[#416]: https://github.com/stephencelis/SQLite.swift/pull/416 [#426]: https://github.com/stephencelis/SQLite.swift/pull/426 [#481]: https://github.com/stephencelis/SQLite.swift/pull/481 [#532]: https://github.com/stephencelis/SQLite.swift/issues/532 @@ -189,3 +191,4 @@ [#1144]: https://github.com/stephencelis/SQLite.swift/pull/1144 [#1146]: https://github.com/stephencelis/SQLite.swift/pull/1146 [#1148]: https://github.com/stephencelis/SQLite.swift/pull/1148 +[#1167]: https://github.com/stephencelis/SQLite.swift/pull/1167 diff --git a/Sources/SQLite/Core/Blob.swift b/Sources/SQLite/Core/Blob.swift index cd31483b..c8bd2c15 100644 --- a/Sources/SQLite/Core/Blob.swift +++ b/Sources/SQLite/Core/Blob.swift @@ -21,26 +21,50 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // +import Foundation -public struct Blob { +public final class Blob { - public let bytes: [UInt8] + public let data: NSData - public init(bytes: [UInt8]) { - self.bytes = bytes + public var bytes: UnsafePointer { + data.bytes.assumingMemoryBound(to: UInt8.self) } - public init(bytes: UnsafeRawPointer, length: Int) { - let i8bufptr = UnsafeBufferPointer(start: bytes.assumingMemoryBound(to: UInt8.self), count: length) - self.init(bytes: [UInt8](i8bufptr)) + public var length: Int { + data.count } - public func toHex() -> String { - bytes.map { - ($0 < 16 ? "0" : "") + String($0, radix: 16, uppercase: false) - }.joined(separator: "") + public convenience init(bytes: [UInt8]) { + guard bytes.count > 0 else { + self.init(data: NSData()) + return + } + self.init(data: NSData(bytes: bytes, length: bytes.count)) + } + + public convenience init(bytes: UnsafeRawPointer, length: Int) { + self.init(data: NSData(bytes: bytes, length: length)) + } + + public init(data: NSData) { + precondition(!(data is NSMutableData), "Blob cannot be initialized with mutable data") + self.data = data } + public func toHex() -> String { + guard length > 0 else { return "" } + + var hex = "" + for idx in 0.. Bool { - lhs.bytes == rhs.bytes + lhs.data == rhs.data } diff --git a/Sources/SQLite/Core/Connection.swift b/Sources/SQLite/Core/Connection.swift index 0e51e651..2a7ec487 100644 --- a/Sources/SQLite/Core/Connection.swift +++ b/Sources/SQLite/Core/Connection.swift @@ -741,7 +741,7 @@ extension Context { func set(result: Binding?) { switch result { case let blob as Blob: - sqlite3_result_blob(self, blob.bytes, Int32(blob.bytes.count), nil) + sqlite3_result_blob(self, blob.bytes, Int32(blob.length), nil) case let double as Double: sqlite3_result_double(self, double) case let int as Int64: diff --git a/Sources/SQLite/Core/Statement.swift b/Sources/SQLite/Core/Statement.swift index 7ba4026c..df4ceebf 100644 --- a/Sources/SQLite/Core/Statement.swift +++ b/Sources/SQLite/Core/Statement.swift @@ -100,21 +100,24 @@ public final class Statement { } fileprivate func bind(_ value: Binding?, atIndex idx: Int) { - if value == nil { + switch value { + case .none: sqlite3_bind_null(handle, Int32(idx)) - } else if let value = value as? Blob { - sqlite3_bind_blob(handle, Int32(idx), value.bytes, Int32(value.bytes.count), SQLITE_TRANSIENT) - } else if let value = value as? Double { + case let value as Blob where value.length == 0: + sqlite3_bind_zeroblob(handle, Int32(idx), 0) + case let value as Blob: + sqlite3_bind_blob(handle, Int32(idx), value.bytes, Int32(value.length), SQLITE_TRANSIENT) + case let value as Double: sqlite3_bind_double(handle, Int32(idx), value) - } else if let value = value as? Int64 { + case let value as Int64: sqlite3_bind_int64(handle, Int32(idx), value) - } else if let value = value as? String { + case let value as String: sqlite3_bind_text(handle, Int32(idx), value, -1, SQLITE_TRANSIENT) - } else if let value = value as? Int { + case let value as Int: self.bind(value.datatypeValue, atIndex: idx) - } else if let value = value as? Bool { + case let value as Bool: self.bind(value.datatypeValue, atIndex: idx) - } else if let value = value { + case .some(let value): fatalError("tried to bind unexpected value \(value)") } } diff --git a/Sources/SQLite/Extensions/Cipher.swift b/Sources/SQLite/Extensions/Cipher.swift index 8af04df9..71889ae5 100644 --- a/Sources/SQLite/Extensions/Cipher.swift +++ b/Sources/SQLite/Extensions/Cipher.swift @@ -32,7 +32,7 @@ extension Connection { } public func key(_ key: Blob, db: String = "main") throws { - try _key_v2(db: db, keyPointer: key.bytes, keySize: key.bytes.count) + try _key_v2(db: db, keyPointer: key.bytes, keySize: key.length) } /// Same as `key(_ key: String, db: String = "main")`, running "PRAGMA cipher_migrate;" @@ -53,7 +53,7 @@ extension Connection { /// Same as `[`keyAndMigrate(_ key: String, db: String = "main")` accepting byte array as key public func keyAndMigrate(_ key: Blob, db: String = "main") throws { - try _key_v2(db: db, keyPointer: key.bytes, keySize: key.bytes.count, migrate: true) + try _key_v2(db: db, keyPointer: key.bytes, keySize: key.length, migrate: true) } /// Change the key on an open database. NB: only works if the database is already encrypted. @@ -68,7 +68,7 @@ extension Connection { } public func rekey(_ key: Blob, db: String = "main") throws { - try _rekey_v2(db: db, keyPointer: key.bytes, keySize: key.bytes.count) + try _rekey_v2(db: db, keyPointer: key.bytes, keySize: key.length) } /// Converts a non-encrypted database to an encrypted one. diff --git a/Sources/SQLite/Foundation.swift b/Sources/SQLite/Foundation.swift index 44a31736..e81c4964 100644 --- a/Sources/SQLite/Foundation.swift +++ b/Sources/SQLite/Foundation.swift @@ -31,13 +31,11 @@ extension Data: Value { } public static func fromDatatypeValue(_ dataValue: Blob) -> Data { - Data(dataValue.bytes) + dataValue.data as Data } public var datatypeValue: Blob { - withUnsafeBytes { (pointer: UnsafeRawBufferPointer) -> Blob in - Blob(bytes: pointer.baseAddress!, length: count) - } + Blob(data: self as NSData) } } diff --git a/Tests/SQLiteTests/Core/BlobTests.swift b/Tests/SQLiteTests/Core/BlobTests.swift index 87eb5709..bef4bb83 100644 --- a/Tests/SQLiteTests/Core/BlobTests.swift +++ b/Tests/SQLiteTests/Core/BlobTests.swift @@ -5,19 +5,47 @@ class BlobTests: XCTestCase { func test_toHex() { let blob = Blob(bytes: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 150, 250, 255]) - XCTAssertEqual(blob.toHex(), "000a141e28323c46505a6496faff") } + func test_toHex_empty() { + let blob = Blob(bytes: []) + XCTAssertEqual(blob.toHex(), "") + } + + func test_description() { + let blob = Blob(bytes: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 150, 250, 255]) + XCTAssertEqual(blob.description, "x'000a141e28323c46505a6496faff'") + } + + func test_description_empty() { + let blob = Blob(bytes: []) + XCTAssertEqual(blob.description, "x''") + } + func test_init_array() { - let blob = Blob(bytes: [42, 42, 42]) - XCTAssertEqual(blob.bytes, [42, 42, 42]) + let blob = Blob(bytes: [42, 43, 44]) + XCTAssertEqual([UInt8](blob.data), [42, 43, 44]) } func test_init_unsafeRawPointer() { let pointer = UnsafeMutablePointer.allocate(capacity: 3) pointer.initialize(repeating: 42, count: 3) let blob = Blob(bytes: pointer, length: 3) - XCTAssertEqual(blob.bytes, [42, 42, 42]) + XCTAssertEqual([UInt8](blob.data), [42, 42, 42]) + } + + func test_equality() { + let blob1 = Blob(bytes: [42, 42, 42]) + let blob2 = Blob(bytes: [42, 42, 42]) + let blob3 = Blob(bytes: [42, 42, 43]) + + XCTAssertEqual(Blob(bytes: []), Blob(bytes: [])) + XCTAssertEqual(blob1, blob2) + XCTAssertNotEqual(blob1, blob3) + } + + func XXX_test_init_with_mutable_data_fails() { + _ = Blob(data: NSMutableData()) } } diff --git a/Tests/SQLiteTests/Core/StatementTests.swift b/Tests/SQLiteTests/Core/StatementTests.swift index eeb513fe..2ab84c09 100644 --- a/Tests/SQLiteTests/Core/StatementTests.swift +++ b/Tests/SQLiteTests/Core/StatementTests.swift @@ -22,7 +22,7 @@ class StatementTests: SQLiteTestCase { let statement = try db.prepare("SELECT email FROM users") XCTAssert(try statement.step()) let blob = statement.row[0] as Blob - XCTAssertEqual("alice@example.com", String(bytes: blob.bytes, encoding: .utf8)!) + XCTAssertEqual("alice@example.com", String(data: blob.data as Data, encoding: .utf8)!) } func test_zero_sized_blob_returns_null() throws { @@ -31,7 +31,7 @@ class StatementTests: SQLiteTestCase { try db.run(blobs.create { $0.column(blobColumn) }) try db.run(blobs.insert(blobColumn <- Blob(bytes: []))) let blobValue = try db.scalar(blobs.select(blobColumn).limit(1, offset: 0)) - XCTAssertEqual([], blobValue.bytes) + XCTAssertEqual([], [UInt8](blobValue.data)) } func test_prepareRowIterator() throws { @@ -71,5 +71,4 @@ class StatementTests: SQLiteTestCase { // truncate succeeds try db.run("DROP TABLE users") } - } diff --git a/Tests/SQLiteTests/Extensions/CipherTests.swift b/Tests/SQLiteTests/Extensions/CipherTests.swift index cc43272e..bd7f2042 100644 --- a/Tests/SQLiteTests/Extensions/CipherTests.swift +++ b/Tests/SQLiteTests/Extensions/CipherTests.swift @@ -19,7 +19,7 @@ class CipherTests: XCTestCase { // db2 let key2 = keyData() - try db2.key(Blob(bytes: key2.bytes, length: key2.length)) + try db2.key(Blob(data: key2)) try db2.run("CREATE TABLE foo (bar TEXT)") try db2.run("INSERT INTO foo (bar) VALUES ('world')") @@ -47,7 +47,7 @@ class CipherTests: XCTestCase { func test_data_rekey() throws { let newKey = keyData() - try db2.rekey(Blob(bytes: newKey.bytes, length: newKey.length)) + try db2.rekey(Blob(data: newKey)) XCTAssertEqual(1, try db2.scalar("SELECT count(*) FROM foo") as? Int64) } @@ -108,12 +108,12 @@ class CipherTests: XCTestCase { XCTAssertEqual(1, try conn.scalar("SELECT count(*) FROM foo") as? Int64) } - private func keyData(length: Int = 64) -> NSMutableData { + private func keyData(length: Int = 64) -> NSData { let keyData = NSMutableData(length: length)! let result = SecRandomCopyBytes(kSecRandomDefault, length, keyData.mutableBytes.assumingMemoryBound(to: UInt8.self)) XCTAssertEqual(0, result) - return keyData + return NSData(data: keyData) } } #endif diff --git a/Tests/SQLiteTests/FoundationTests.swift b/Tests/SQLiteTests/FoundationTests.swift index 453febcd..ffd3fae1 100644 --- a/Tests/SQLiteTests/FoundationTests.swift +++ b/Tests/SQLiteTests/FoundationTests.swift @@ -5,7 +5,7 @@ class FoundationTests: XCTestCase { func testDataFromBlob() { let data = Data([1, 2, 3]) let blob = data.datatypeValue - XCTAssertEqual([1, 2, 3], blob.bytes) + XCTAssertEqual([1, 2, 3], [UInt8](blob.data)) } func testBlobToData() {