Files

387 lines
12 KiB
Swift

import Foundation
/// A value stored in a database table.
///
/// To get `DatabaseValue` instances, you can:
///
/// - Fetch `DatabaseValue` from a ``Database`` instace:
///
/// ```swift
/// try dbQueue.read { db in
/// let dbValue = try DatabaseValue.fetchOne(db, sql: """
/// SELECT name FROM player
/// """)
/// }
/// ```
///
/// - Extract `DatabaseValue` from a database ``Row``:
///
/// ```swift
/// try dbQueue.read { db in
/// if let row = try Row.fetchOne(db, sql: """
/// SELECT name FROM player
/// """)
/// {
/// let dbValue = row[0] as DatabaseValue
/// }
/// }
/// ```
///
/// - Use the ``DatabaseValueConvertible/databaseValue-1ob9k`` property on a
/// ``DatabaseValueConvertible`` value:
///
/// ```swift
/// let dbValue = DatabaseValue.null
/// let dbValue = 1.databaseValue
/// let dbValue = "Arthur".databaseValue
/// let dbValue = Date().databaseValue
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/datatype3.html>
///
/// ## Topics
///
/// ### Creating a DatabaseValue
///
/// - ``init(value:)``
/// - ``init(sqliteStatement:index:)``
/// - ``null``
///
/// ### Accessing the SQLite storage
///
/// - ``isNull``
/// - ``storage-swift.property``
/// - ``Storage-swift.enum``
public struct DatabaseValue: Hashable {
/// The SQLite storage.
public let storage: Storage
/// The NULL DatabaseValue.
public static let null = DatabaseValue(storage: .null)
/// A value stored in a database table, with its exact SQLite storage
/// (NULL, INTEGER, REAL, TEXT, BLOB).
///
/// Related SQLite documentation: <https://www.sqlite.org/datatype3.html#storage_classes_and_datatypes>
@frozen
public enum Storage {
/// The NULL storage class.
case null
/// The INTEGER storage class, wrapping an Int64.
case int64(Int64)
/// The REAL storage class, wrapping a Double.
case double(Double)
/// The TEXT storage class, wrapping a String.
case string(String)
/// The BLOB storage class, wrapping Data.
case blob(Data)
/// Returns `Int64`, `Double`, `String`, `Data` or nil.
public var value: (any DatabaseValueConvertible)? {
switch self {
case .null:
return nil
case .int64(let int64):
return int64
case .double(let double):
return double
case .string(let string):
return string
case .blob(let data):
return data
}
}
}
/// Creates a `DatabaseValue` from any value.
///
/// The result is nil unless `value` adopts ``DatabaseValueConvertible``.
public init?(value: Any) {
guard let convertible = value as? any DatabaseValueConvertible else {
return nil
}
self = convertible.databaseValue
}
// MARK: - Extracting Value
/// A boolean value indicating is the database value is `NULL`.
public var isNull: Bool {
switch storage {
case .null:
return true
default:
return false
}
}
// MARK: - Not Public
init(storage: Storage) {
self.storage = storage
}
// SQLite function argument
init(sqliteValue: SQLiteValue) {
switch sqlite3_value_type(sqliteValue) {
case SQLITE_NULL:
storage = .null
case SQLITE_INTEGER:
storage = .int64(sqlite3_value_int64(sqliteValue))
case SQLITE_FLOAT:
storage = .double(sqlite3_value_double(sqliteValue))
case SQLITE_TEXT:
storage = .string(String(cString: sqlite3_value_text(sqliteValue)!))
case SQLITE_BLOB:
if let bytes = sqlite3_value_blob(sqliteValue) {
let count = Int(sqlite3_value_bytes(sqliteValue))
storage = .blob(Data(bytes: bytes, count: count)) // copy bytes
} else {
storage = .blob(Data())
}
case let type:
// Assume a GRDB bug: there is no point throwing any error.
fatalError("Unexpected SQLite value type: \(type)")
}
}
/// Creates a `DatabaseValue` initialized from a raw SQLite statement pointer.
public init(sqliteStatement: SQLiteStatement, index: CInt) {
switch sqlite3_column_type(sqliteStatement, index) {
case SQLITE_NULL:
storage = .null
case SQLITE_INTEGER:
storage = .int64(sqlite3_column_int64(sqliteStatement, index))
case SQLITE_FLOAT:
storage = .double(sqlite3_column_double(sqliteStatement, index))
case SQLITE_TEXT:
storage = .string(String(cString: sqlite3_column_text(sqliteStatement, index)))
case SQLITE_BLOB:
if let bytes = sqlite3_column_blob(sqliteStatement, index) {
let count = Int(sqlite3_column_bytes(sqliteStatement, index))
storage = .blob(Data(bytes: bytes, count: count)) // copy bytes
} else {
storage = .blob(Data())
}
case let type:
// Assume a GRDB bug: there is no point throwing any error.
fatalError("Unexpected SQLite column type: \(type)")
}
}
}
extension DatabaseValue: StatementBinding {
public func bind(to sqliteStatement: SQLiteStatement, at index: CInt) -> CInt {
switch storage {
case .null:
return sqlite3_bind_null(sqliteStatement, index)
case .int64(let int64):
return int64.bind(to: sqliteStatement, at: index)
case .double(let double):
return double.bind(to: sqliteStatement, at: index)
case .string(let string):
return string.bind(to: sqliteStatement, at: index)
case .blob(let data):
return data.bind(to: sqliteStatement, at: index)
}
}
/// Calls the given closure after binding a statement argument.
///
/// The binding is valid only during the execution of this method.
///
/// - parameter sqliteStatement: An SQLite statement.
/// - parameter index: 1-based index to statement arguments.
/// - parameter body: The closure to execute when argument is bound.
func withBinding<T>(to sqliteStatement: SQLiteStatement, at index: CInt, do body: () throws -> T) throws -> T {
switch storage {
case .null:
let code = sqlite3_bind_null(sqliteStatement, index)
try checkBindingSuccess(code: code, sqliteStatement: sqliteStatement)
return try body()
case .int64(let int64):
let code = int64.bind(to: sqliteStatement, at: index)
try checkBindingSuccess(code: code, sqliteStatement: sqliteStatement)
return try body()
case .double(let double):
let code = double.bind(to: sqliteStatement, at: index)
try checkBindingSuccess(code: code, sqliteStatement: sqliteStatement)
return try body()
case .string(let string):
return try string.withBinding(to: sqliteStatement, at: index, do: body)
case .blob(let data):
return try data.withBinding(to: sqliteStatement, at: index, do: body)
}
}
}
extension DatabaseValue: Sendable { }
// @unchecked Sendable because Data is not Sendable in all target OS
extension DatabaseValue.Storage: @unchecked Sendable { }
// MARK: - Hashable & Equatable
extension DatabaseValue.Storage: Equatable {
/// Return true if the storages are identical.
///
/// Unlike ``DatabaseValue`` equality that considers the integer 1 as
/// equal to the 1.0 double (as SQLite does), int64 and double storages
/// are never equal.
public static func == (_ lhs: Self, _ rhs: Self) -> Bool {
switch (lhs, rhs) {
case (.null, .null): return true
case let (.int64(lhs), .int64(rhs)): return lhs == rhs
case let (.double(lhs), .double(rhs)): return lhs == rhs
case let (.string(lhs), .string(rhs)): return lhs == rhs
case let (.blob(lhs), .blob(rhs)): return lhs == rhs
default: return false
}
}
}
extension DatabaseValue: Equatable {
/// Returns whether two ``DatabaseValue`` are equal.
///
/// For example:
///
/// ```swift
/// 1.databaseValue == "foo".databaseValue // false
/// 1.databaseValue == 1.databaseValue // true
/// ```
///
/// When comparing integers and doubles, the result is true if and only
/// values are equal, and if converting one type to the other does
/// not lose information:
///
/// ```swift
/// 1.databaseValue == 1.0.databaseValue // true
/// ```
///
/// For a comparison that distinguishes integer and doubles, compare
/// storages instead:
///
/// ```swift
/// 1.databaseValue.storage == 1.0.databaseValue.storage // false
/// ```
public static func == (lhs: DatabaseValue, rhs: DatabaseValue) -> Bool {
switch (lhs.storage, rhs.storage) {
case (.null, .null):
return true
case let (.int64(lhs), .int64(rhs)):
return lhs == rhs
case let (.double(lhs), .double(rhs)):
return lhs == rhs
case let (.int64(lhs), .double(rhs)):
return Int64(exactly: rhs) == lhs
case let (.double(lhs), .int64(rhs)):
return rhs == Int64(exactly: lhs)
case let (.string(lhs), .string(rhs)):
return lhs == rhs
case let (.blob(lhs), .blob(rhs)):
return lhs == rhs
default:
return false
}
}
}
extension DatabaseValue {
public func hash(into hasher: inout Hasher) {
switch storage {
case .null:
hasher.combine(0)
case .int64(let int64):
// 1 == 1.0, hence 1 and 1.0 must have the same hash:
hasher.combine(Double(int64))
case .double(let double):
hasher.combine(double)
case .string(let string):
hasher.combine(string)
case .blob(let data):
hasher.combine(data)
}
}
}
extension DatabaseValue: DatabaseValueConvertible {
/// Returns self
public var databaseValue: DatabaseValue {
self
}
/// Returns the database value
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> DatabaseValue? {
dbValue
}
}
extension DatabaseValue: SQLSpecificExpressible {
public var sqlExpression: SQLExpression {
.databaseValue(self)
}
}
extension DatabaseValue: CustomStringConvertible {
public var description: String {
switch storage {
case .null:
return "NULL"
case .int64(let int64):
return String(int64)
case .double(let double):
return String(double)
case .string(let string):
return String(reflecting: string)
case .blob(let data):
return "Data(\(data.description))"
}
}
}
/// Compares DatabaseValue like SQLite.
///
/// See RxGRDB for tests.
///
/// This comparison is not public because it does not handle text collations,
/// and may be dangerous when put in user hands.
///
/// So far, the only goal of this sorting method so far is aesthetic, and
/// easier testing.
func < (lhs: DatabaseValue, rhs: DatabaseValue) -> Bool {
switch (lhs.storage, rhs.storage) {
case let (.int64(lhs), .int64(rhs)):
return lhs < rhs
case let (.double(lhs), .double(rhs)):
return lhs < rhs
case let (.int64(lhs), .double(rhs)):
return Double(lhs) < rhs
case let (.double(lhs), .int64(rhs)):
return lhs < Double(rhs)
case let (.string(lhs), .string(rhs)):
return lhs.utf8.lexicographicallyPrecedes(rhs.utf8)
case let (.blob(lhs), .blob(rhs)):
return lhs.lexicographicallyPrecedes(rhs, by: <)
case (.blob, _):
return false
case (_, .blob):
return true
case (.string, _):
return false
case (_, .string):
return true
case (.int64, _), (.double, _):
return false
case (_, .int64), (_, .double):
return true
case (.null, _):
return false
case (_, .null):
return true
}
}