From 7cad505b793a1af8ce5398843e66ccd7de4e070c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helge=20He=C3=9F?= Date: Sun, 17 Sep 2023 14:03:56 +0200 Subject: [PATCH 1/4] Split SQLError/LighterError SQLError.swift is useful in other contexts. --- Sources/Lighter/Utilities/LighterError.swift | 48 ++++++++++++++++++++ Sources/Lighter/Utilities/SQLError.swift | 46 +------------------ 2 files changed, 49 insertions(+), 45 deletions(-) create mode 100644 Sources/Lighter/Utilities/LighterError.swift diff --git a/Sources/Lighter/Utilities/LighterError.swift b/Sources/Lighter/Utilities/LighterError.swift new file mode 100644 index 0000000..57bc6f3 --- /dev/null +++ b/Sources/Lighter/Utilities/LighterError.swift @@ -0,0 +1,48 @@ +// +// Created by Helge Heß. +// Copyright © 2022-2023 ZeeZide GmbH. +// + +import struct Foundation.URL + +/** + * An error that is thrown by Lighter database operations. + * + * This carries a higher level error type as well as the SQLite3 + * error code and message. + */ +public struct LighterError: Swift.Error { + + /// The kind of the error that happened within Lighter. + public enum ErrorType: Hashable { + + case insertFailed(record: AnyHashable) + case updateFailed(record: AnyHashable) + case deleteFailed(record: AnyHashable) + + case couldNotOpenDatabase(URL) + + case couldNotBeginTransaction + case couldNotRollbackTransaction + case couldNotCommitTransaction + + case couldNotFindRelationshipTarget + } + + /// The higher level error type. + public let type : ErrorType + + /// The SQLite3 error code. + public let code : Int32 + /// The SQLite3 error message. + public let message : String? + + @inlinable + public init(_ type: ErrorType, + _ code: Int32, _ message: UnsafePointer? = nil) + { + self.type = type + self.code = code + self.message = message.flatMap(String.init(cString:)) + } +} diff --git a/Sources/Lighter/Utilities/SQLError.swift b/Sources/Lighter/Utilities/SQLError.swift index 9f453df..b550f0d 100644 --- a/Sources/Lighter/Utilities/SQLError.swift +++ b/Sources/Lighter/Utilities/SQLError.swift @@ -1,52 +1,8 @@ // // Created by Helge Heß. -// Copyright © 2022 ZeeZide GmbH. +// Copyright © 2022-2023 ZeeZide GmbH. // -import struct Foundation.URL - -/** - * An error that is thrown by Lighter database operations. - * - * This carries a higher level error type as well as the SQLite3 - * error code and message. - */ -public struct LighterError: Swift.Error { - - /// The kind of the error that happened within Lighter. - public enum ErrorType: Hashable { - - case insertFailed(record: AnyHashable) - case updateFailed(record: AnyHashable) - case deleteFailed(record: AnyHashable) - - case couldNotOpenDatabase(URL) - - case couldNotBeginTransaction - case couldNotRollbackTransaction - case couldNotCommitTransaction - - case couldNotFindRelationshipTarget - } - - /// The higher level error type. - public let type : ErrorType - - /// The SQLite3 error code. - public let code : Int32 - /// The SQLite3 error message. - public let message : String? - - @inlinable - public init(_ type: ErrorType, - _ code: Int32, _ message: UnsafePointer? = nil) - { - self.type = type - self.code = code - self.message = message.flatMap(String.init(cString:)) - } -} - import func SQLite3.sqlite3_errcode import func SQLite3.sqlite3_errmsg From d5984ec35beb41060d984c89f65f6023e7a35676 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helge=20He=C3=9F?= Date: Sun, 17 Sep 2023 14:17:07 +0200 Subject: [PATCH 2/4] Add support for all Int types and Float Useful, sometimes :-) --- Sources/Lighter/Schema/SQLiteValueType.swift | 97 +++++++++++++++++++- 1 file changed, 93 insertions(+), 4 deletions(-) diff --git a/Sources/Lighter/Schema/SQLiteValueType.swift b/Sources/Lighter/Schema/SQLiteValueType.swift index 1e9b128..8fffdaa 100644 --- a/Sources/Lighter/Schema/SQLiteValueType.swift +++ b/Sources/Lighter/Schema/SQLiteValueType.swift @@ -1,6 +1,6 @@ // // Created by Helge Heß. -// Copyright © 2022 ZeeZide GmbH. +// Copyright © 2022-2023 ZeeZide GmbH. // import SQLite3 @@ -155,17 +155,26 @@ extension Bool : SQLiteValueType { } } -extension Int : SQLiteValueType { +extension Int : SQLiteValueType {} +extension Int8 : SQLiteValueType {} +extension UInt8 : SQLiteValueType {} +extension Int16 : SQLiteValueType {} +extension UInt16 : SQLiteValueType {} +extension Int32 : SQLiteValueType {} +extension UInt32 : SQLiteValueType {} +extension Int64 : SQLiteValueType {} + +extension BinaryInteger { @inlinable public init(unsafeSQLite3StatementHandle stmt: OpaquePointer!, column: Int32) throws { - self = Int(sqlite3_column_int64(stmt, column)) + self = Self(sqlite3_column_int64(stmt, column)) } @inlinable public init(unsafeSQLite3ValueHandle value: OpaquePointer?) throws { - self = Int(sqlite3_value_int64(value)) + self = Self(sqlite3_value_int64(value)) } @inlinable public var sqlStringValue : String { String(self) } @@ -180,6 +189,86 @@ extension Int : SQLiteValueType { } } +extension UInt: SQLiteValueType { // This can overflow Int64 on 64bit archs + + @inlinable + public init(unsafeSQLite3StatementHandle stmt: OpaquePointer!, column: Int32) + throws + { + self = MemoryLayout.size < 8 + ? Self(sqlite3_column_int64(stmt, column)) + : Self(UInt64(bitPattern: sqlite3_column_int64(stmt, column))) + } + @inlinable + public init(unsafeSQLite3ValueHandle value: OpaquePointer?) throws { + self = MemoryLayout.size < 8 + ? Self(sqlite3_value_int64(value)) + : Self(UInt64(bitPattern: sqlite3_value_int64(value))) + } + + @inlinable public var sqlStringValue : String { String(self) } + @inlinable public var requiresSQLBinding : Bool { false } + + @inlinable + public func bind(unsafeSQLite3StatementHandle stmt: OpaquePointer!, + index: Int32, then execute: () -> Void) + { + sqlite3_bind_int64(stmt, index, MemoryLayout.size < 8 + ? Int64(self) : Int64(bitPattern: UInt64(self))) + execute() + } +} + +extension UInt64: SQLiteValueType { // This can overflow Int64 + + @inlinable + public init(unsafeSQLite3StatementHandle stmt: OpaquePointer!, column: Int32) + throws + { + self = UInt64(bitPattern: sqlite3_column_int64(stmt, column)) + } + @inlinable + public init(unsafeSQLite3ValueHandle value: OpaquePointer?) throws { + self = UInt64(bitPattern: sqlite3_value_int64(value)) + } + + @inlinable public var sqlStringValue : String { String(self) } + @inlinable public var requiresSQLBinding : Bool { false } + + @inlinable + public func bind(unsafeSQLite3StatementHandle stmt: OpaquePointer!, + index: Int32, then execute: () -> Void) + { + sqlite3_bind_int64(stmt, index, Int64(bitPattern: self)) + execute() + } +} + +extension Float : SQLiteValueType { + + @inlinable + public init(unsafeSQLite3StatementHandle stmt: OpaquePointer!, column: Int32) + throws + { + self = Float(sqlite3_column_double(stmt, column)) + } + @inlinable + public init(unsafeSQLite3ValueHandle value: OpaquePointer?) throws { + self = Float(sqlite3_value_double(value)) + } + + @inlinable public var sqlStringValue : String { String(self) } // TBD! + @inlinable public var requiresSQLBinding : Bool { false } + + @inlinable + public func bind(unsafeSQLite3StatementHandle stmt: OpaquePointer!, + index: Int32, then execute: () -> Void) + { + sqlite3_bind_double(stmt, index, Double(self)) + execute() + } +} + extension Double : SQLiteValueType { @inlinable From 80e4c6ef4b5ca3c3c0b88895e73d6e1d6e3ceb2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helge=20He=C3=9F?= Date: Sun, 17 Sep 2023 14:36:30 +0200 Subject: [PATCH 3/4] Bool's can now load from string columns Added a basic detection for legacy databases: - Y(es)/N(o) - T(rue)/F(alse) - empty => false - 0 => false - 1...9 => true Throws an error on other values. --- Sources/Lighter/Schema/SQLiteValueType.swift | 59 +++++++++++++++++--- 1 file changed, 50 insertions(+), 9 deletions(-) diff --git a/Sources/Lighter/Schema/SQLiteValueType.swift b/Sources/Lighter/Schema/SQLiteValueType.swift index 8fffdaa..34daa63 100644 --- a/Sources/Lighter/Schema/SQLiteValueType.swift +++ b/Sources/Lighter/Schema/SQLiteValueType.swift @@ -20,10 +20,11 @@ import struct Foundation.UUID * A value that can be used in SQLite columns. * * The base types supported by SQLite3 are: - * - `Int` (SQL `INTEGER`) - * - `Double` (SQL `REAL`) - * - `String` (SQL `TEXT`) - * - `[ UInt8 ]` (SQL `BLOB`) + * - `Int` (all variants) (SQL `INTEGER`) + * - `Float`, `Double` (SQL `REAL`) + * - `String`, `Substring` (SQL `TEXT`) + * - `[ UInt8 ]` (SQL `BLOB`) + * - `Bool` (SQL `INTEGER`) * * In addition Lighter has builtin support for a set of common Foundation types: * - `URL` (mapped to the String representation of the `URL`) @@ -131,16 +132,54 @@ extension RawRepresentable where Self.RawValue: SQLiteValueType { } extension Bool : SQLiteValueType { - + + public enum SQLiteBoolConversionError: Swift.Error { + case couldNotParseString(String) + } + + @inlinable + init(unsafeCString cstr: UnsafePointer?) throws { + guard let cstr = cstr else { + self = false + return + } + let firstCharAsASCII = cstr.pointee + switch firstCharAsASCII { + case 0 : self = false // empty string + case 48 : self = false // `0` + case 49...57 : self = true // 1...9 + case 89, 121 : self = true // `Y`/`y` (es) + case 78, 110 : self = true // `N`/`n` (o) + case 84, 116 : self = false // `T`/`t` (rue) + case 70, 102 : self = false // `F`/`f` (alse) + default: throw SQLiteBoolConversionError + .couldNotParseString(String(cString: cstr)) + } + } + @inlinable public init(unsafeSQLite3StatementHandle stmt: OpaquePointer!, column: Int32) throws { - self = sqlite3_column_int64(stmt, column) != 0 + switch sqlite3_column_type(stmt, column) { + case SQLITE_INTEGER, SQLITE_FLOAT: + self = sqlite3_column_int64(stmt, column) != 0 + case SQLITE_NULL: + self = false + default: + try self.init(unsafeCString: sqlite3_column_text(stmt, column)) + } } @inlinable public init(unsafeSQLite3ValueHandle value: OpaquePointer?) throws { - self = sqlite3_value_int64(value) != 0 + switch sqlite3_value_type(value) { + case SQLITE_INTEGER, SQLITE_FLOAT: + self = sqlite3_value_int64(value) != 0 + case SQLITE_NULL: + self = false + default: + try self.init(unsafeCString: sqlite3_value_text(value)) + } } @inlinable public var sqlStringValue : String { String(self) } @@ -165,6 +204,8 @@ extension UInt32 : SQLiteValueType {} extension Int64 : SQLiteValueType {} extension BinaryInteger { + // Note: They don't need to detect text or NULL, SQLite itself does a + // conversion. Its rules apply. @inlinable public init(unsafeSQLite3StatementHandle stmt: OpaquePointer!, column: Int32) @@ -409,7 +450,7 @@ extension Array: SQLiteValueType where Element == UInt8 { @inlinable public init(unsafeSQLite3StatementHandle stmt: OpaquePointer!, column: Int32) - throws + throws { if let blob = sqlite3_column_blob(stmt, column) { let count = Int(sqlite3_column_bytes(stmt, column)) @@ -552,7 +593,7 @@ extension Data: SQLiteValueType { @inlinable public init(unsafeSQLite3StatementHandle stmt: OpaquePointer!, column: Int32) - throws + throws { let s = try [ UInt8 ](unsafeSQLite3StatementHandle: stmt, column: column) self.init(s) From c01a12ad6971fbead4a0e40010f56151e5691264 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helge=20He=C3=9F?= Date: Sun, 17 Sep 2023 14:40:44 +0200 Subject: [PATCH 4/4] Invalid raw values now throw an error If a raw representable couldn't be created from the backing raw SQLite value, it would crash in a force unwrap. Now this is throwing a proper error. --- Sources/Lighter/Schema/SQLiteValueType.swift | 34 ++++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/Sources/Lighter/Schema/SQLiteValueType.swift b/Sources/Lighter/Schema/SQLiteValueType.swift index 34daa63..4e8093c 100644 --- a/Sources/Lighter/Schema/SQLiteValueType.swift +++ b/Sources/Lighter/Schema/SQLiteValueType.swift @@ -36,6 +36,14 @@ import struct Foundation.UUID * An `Optional` can be used for optional values (e.g. `String?` for * `TEXT NULL`). * + * Finally `RawRepresentable` types, like `enum`s, that have a + * `RawRepresentable` value can also be used, e.g.: + * ```swift + * enum Colors: String { + * case red, green, blue + * } + * ``` + * * Note: `SQLiteValueType`s are usually `Hashable`, making record types * Hashable too! */ @@ -88,6 +96,16 @@ public protocol SQLiteValueType { index: Int32, then execute: () -> Void) } +/** + * An error happened while converting between SQLite types and a + * `RawRepresentable`. + */ +public enum SQLiteRawConversionError: Swift.Error { + + /// The raw value initializers returned `nil` for the given raw value. + case couldNotConvertRawValue(RawValue) +} + /** * This extension allows one to use `RawRepresentable`s that have a * `SQLiteValueType` as their raw value, to be `SQLiteValueType`s themselves. @@ -105,16 +123,20 @@ extension RawRepresentable where Self.RawValue: SQLiteValueType { public init(unsafeSQLite3StatementHandle stmt: OpaquePointer!, column: Int32) throws { - self.init(rawValue: - try RawValue(unsafeSQLite3StatementHandle: stmt, column: column) - )! // Hm, not optimal + let value = try RawValue(unsafeSQLite3StatementHandle: stmt, column: column) + guard let me = Self(rawValue: value) else { + throw SQLiteRawConversionError.couldNotConvertRawValue(value) + } + self = me } @inlinable public init(unsafeSQLite3ValueHandle value: OpaquePointer?) throws { - self.init(rawValue: - try RawValue(unsafeSQLite3ValueHandle: value) - )! // Hm, not optimal + let value = try RawValue(unsafeSQLite3ValueHandle: value) + guard let me = Self(rawValue: value) else { + throw SQLiteRawConversionError.couldNotConvertRawValue(value) + } + self = me } @inlinable public var sqlStringValue : String { rawValue.sqlStringValue }