add swiftUI code
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
import Foundation
|
||||
|
||||
extension EncodableRecord where Self: Encodable {
|
||||
/// Encodes the record into the provided persistence container, using the
|
||||
/// `Encodable` conformance.
|
||||
public func encode(to container: inout PersistenceContainer) throws {
|
||||
let encoder = RecordEncoder<Self>(persistenceContainer: container)
|
||||
try encode(to: encoder)
|
||||
container = encoder.persistenceContainer
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - RecordEncoder
|
||||
|
||||
/// The encoder that encodes a record into GRDB's PersistenceContainer
|
||||
private class RecordEncoder<Record: EncodableRecord>: Encoder {
|
||||
var codingPath: [any CodingKey] { [] }
|
||||
var userInfo: [CodingUserInfoKey: Any] { Record.databaseEncodingUserInfo }
|
||||
private var _persistenceContainer: PersistenceContainer
|
||||
var persistenceContainer: PersistenceContainer { _persistenceContainer }
|
||||
var keyEncodingStrategy: DatabaseColumnEncodingStrategy { Record.databaseColumnEncodingStrategy }
|
||||
|
||||
init(persistenceContainer: PersistenceContainer) {
|
||||
_persistenceContainer = persistenceContainer
|
||||
}
|
||||
|
||||
func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> {
|
||||
let container = KeyedContainer<Key>(recordEncoder: self)
|
||||
return KeyedEncodingContainer(container)
|
||||
}
|
||||
|
||||
func unkeyedContainer() -> UnkeyedEncodingContainer {
|
||||
fatalError("unkeyed encoding is not supported")
|
||||
}
|
||||
|
||||
func singleValueContainer() -> SingleValueEncodingContainer {
|
||||
self
|
||||
}
|
||||
|
||||
private struct KeyedContainer<Key: CodingKey>: KeyedEncodingContainerProtocol {
|
||||
var recordEncoder: RecordEncoder
|
||||
var userInfo: [CodingUserInfoKey: Any] { Record.databaseEncodingUserInfo }
|
||||
var codingPath: [any CodingKey] { [] }
|
||||
|
||||
// swiftlint:disable comma
|
||||
func encode(_ value: Bool, forKey key: Key) throws { recordEncoder.persist(value, forKey: key) }
|
||||
func encode(_ value: Int, forKey key: Key) throws { recordEncoder.persist(value, forKey: key) }
|
||||
func encode(_ value: Int8, forKey key: Key) throws { recordEncoder.persist(value, forKey: key) }
|
||||
func encode(_ value: Int16, forKey key: Key) throws { recordEncoder.persist(value, forKey: key) }
|
||||
func encode(_ value: Int32, forKey key: Key) throws { recordEncoder.persist(value, forKey: key) }
|
||||
func encode(_ value: Int64, forKey key: Key) throws { recordEncoder.persist(value, forKey: key) }
|
||||
func encode(_ value: UInt, forKey key: Key) throws { recordEncoder.persist(value, forKey: key) }
|
||||
func encode(_ value: UInt8, forKey key: Key) throws { recordEncoder.persist(value, forKey: key) }
|
||||
func encode(_ value: UInt16, forKey key: Key) throws { recordEncoder.persist(value, forKey: key) }
|
||||
func encode(_ value: UInt32, forKey key: Key) throws { recordEncoder.persist(value, forKey: key) }
|
||||
func encode(_ value: UInt64, forKey key: Key) throws { recordEncoder.persist(value, forKey: key) }
|
||||
func encode(_ value: Float, forKey key: Key) throws { recordEncoder.persist(value, forKey: key) }
|
||||
func encode(_ value: Double, forKey key: Key) throws { recordEncoder.persist(value, forKey: key) }
|
||||
func encode(_ value: String, forKey key: Key) throws { recordEncoder.persist(value, forKey: key) }
|
||||
// swiftlint:enable comma
|
||||
|
||||
func encode<T>(_ value: T, forKey key: Key) throws where T: Encodable {
|
||||
try recordEncoder.encode(value, forKey: key)
|
||||
}
|
||||
|
||||
func encodeNil(forKey key: Key) throws { recordEncoder.persist(nil, forKey: key) }
|
||||
|
||||
// swiftlint:disable comma
|
||||
func encodeIfPresent(_ value: Bool?, forKey key: Key) throws { recordEncoder.persist(value, forKey: key) }
|
||||
func encodeIfPresent(_ value: Int?, forKey key: Key) throws { recordEncoder.persist(value, forKey: key) }
|
||||
func encodeIfPresent(_ value: Int8?, forKey key: Key) throws { recordEncoder.persist(value, forKey: key) }
|
||||
func encodeIfPresent(_ value: Int16?, forKey key: Key) throws { recordEncoder.persist(value, forKey: key) }
|
||||
func encodeIfPresent(_ value: Int32?, forKey key: Key) throws { recordEncoder.persist(value, forKey: key) }
|
||||
func encodeIfPresent(_ value: Int64?, forKey key: Key) throws { recordEncoder.persist(value, forKey: key) }
|
||||
func encodeIfPresent(_ value: UInt?, forKey key: Key) throws { recordEncoder.persist(value, forKey: key) }
|
||||
func encodeIfPresent(_ value: UInt8?, forKey key: Key) throws { recordEncoder.persist(value, forKey: key) }
|
||||
func encodeIfPresent(_ value: UInt16?, forKey key: Key) throws { recordEncoder.persist(value, forKey: key) }
|
||||
func encodeIfPresent(_ value: UInt32?, forKey key: Key) throws { recordEncoder.persist(value, forKey: key) }
|
||||
func encodeIfPresent(_ value: UInt64?, forKey key: Key) throws { recordEncoder.persist(value, forKey: key) }
|
||||
func encodeIfPresent(_ value: Float?, forKey key: Key) throws { recordEncoder.persist(value, forKey: key) }
|
||||
func encodeIfPresent(_ value: Double?, forKey key: Key) throws { recordEncoder.persist(value, forKey: key) }
|
||||
func encodeIfPresent(_ value: String?, forKey key: Key) throws { recordEncoder.persist(value, forKey: key) }
|
||||
// swiftlint:enable comma
|
||||
|
||||
func encodeIfPresent<T>(_ value: T?, forKey key: Key) throws where T: Encodable {
|
||||
if let value {
|
||||
try recordEncoder.encode(value, forKey: key)
|
||||
} else {
|
||||
recordEncoder.persist(nil, forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
func nestedContainer<NestedKey>(
|
||||
keyedBy keyType: NestedKey.Type,
|
||||
forKey key: Key)
|
||||
-> KeyedEncodingContainer<NestedKey>
|
||||
{
|
||||
fatalError("Not implemented")
|
||||
}
|
||||
|
||||
func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
|
||||
fatalError("Not implemented")
|
||||
}
|
||||
|
||||
func superEncoder() -> Encoder {
|
||||
recordEncoder
|
||||
}
|
||||
|
||||
func superEncoder(forKey key: Key) -> Encoder {
|
||||
recordEncoder
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper methods
|
||||
fileprivate func persist(_ value: (any DatabaseValueConvertible)?, forKey key: any CodingKey) {
|
||||
_persistenceContainer[keyEncodingStrategy.column(forKey: key)] = value
|
||||
}
|
||||
|
||||
fileprivate func encode<T>(_ value: T, forKey key: any CodingKey) throws where T: Encodable {
|
||||
if let data = value as? Data {
|
||||
persist(Record.databaseDataEncodingStrategy.encode(data), forKey: key)
|
||||
} else if let date = value as? Date {
|
||||
persist(Record.databaseDateEncodingStrategy.encode(date), forKey: key)
|
||||
} else if let uuid = value as? UUID {
|
||||
persist(Record.databaseUUIDEncodingStrategy.encode(uuid), forKey: key)
|
||||
} else if let value = value as? any DatabaseValueConvertible {
|
||||
// Prefer DatabaseValueConvertible encoding over Decodable.
|
||||
persist(value.databaseValue, forKey: key)
|
||||
} else {
|
||||
do {
|
||||
// This encoding will fail for types that encode into keyed
|
||||
// or unkeyed containers, because we're encoding a single
|
||||
// value here (string, int, double, data, null). If such an
|
||||
// error happens, we'll switch to JSON encoding.
|
||||
let encoder = ColumnEncoder(recordEncoder: self, key: key)
|
||||
try value.encode(to: encoder)
|
||||
if encoder.requiresJSON {
|
||||
// Here we handle empty arrays and dictionaries.
|
||||
throw JSONRequiredError()
|
||||
}
|
||||
} catch is JSONRequiredError {
|
||||
// Encode to JSON
|
||||
try autoreleasepool {
|
||||
|
||||
let jsonData = try Record.databaseJSONEncoder(for: key.stringValue).encode(value)
|
||||
|
||||
// Store JSON String in the database for easier debugging and
|
||||
// database inspection. Thanks to SQLite weak typing, we won't
|
||||
// have any trouble decoding this string into data when we
|
||||
// eventually perform JSON decoding.
|
||||
// TODO: possible optimization: avoid this conversion to string,
|
||||
// and store raw data bytes as an SQLite string
|
||||
let jsonString = String(data: jsonData, encoding: .utf8)!
|
||||
persist(jsonString, forKey: key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension RecordEncoder: SingleValueEncodingContainer {
|
||||
private func unsupportedSingleValueEncoding() {
|
||||
fatalError("Can't encode a single value in a database row.")
|
||||
}
|
||||
|
||||
func encodeNil() throws {
|
||||
unsupportedSingleValueEncoding()
|
||||
}
|
||||
|
||||
func encode(_ value: Bool) throws {
|
||||
unsupportedSingleValueEncoding()
|
||||
}
|
||||
|
||||
func encode(_ value: String) throws {
|
||||
unsupportedSingleValueEncoding()
|
||||
}
|
||||
|
||||
func encode(_ value: Double) throws {
|
||||
unsupportedSingleValueEncoding()
|
||||
}
|
||||
|
||||
func encode(_ value: Float) throws {
|
||||
unsupportedSingleValueEncoding()
|
||||
}
|
||||
|
||||
func encode(_ value: Int) throws {
|
||||
unsupportedSingleValueEncoding()
|
||||
}
|
||||
|
||||
func encode(_ value: Int8) throws {
|
||||
unsupportedSingleValueEncoding()
|
||||
}
|
||||
|
||||
func encode(_ value: Int16) throws {
|
||||
unsupportedSingleValueEncoding()
|
||||
}
|
||||
|
||||
func encode(_ value: Int32) throws {
|
||||
unsupportedSingleValueEncoding()
|
||||
}
|
||||
|
||||
func encode(_ value: Int64) throws {
|
||||
unsupportedSingleValueEncoding()
|
||||
}
|
||||
|
||||
func encode(_ value: UInt) throws {
|
||||
unsupportedSingleValueEncoding()
|
||||
}
|
||||
|
||||
func encode(_ value: UInt8) throws {
|
||||
unsupportedSingleValueEncoding()
|
||||
}
|
||||
|
||||
func encode(_ value: UInt16) throws {
|
||||
unsupportedSingleValueEncoding()
|
||||
}
|
||||
|
||||
func encode(_ value: UInt32) throws {
|
||||
unsupportedSingleValueEncoding()
|
||||
}
|
||||
|
||||
func encode(_ value: UInt64) throws {
|
||||
unsupportedSingleValueEncoding()
|
||||
}
|
||||
|
||||
func encode<T>(_ value: T) throws where T: Encodable {
|
||||
if let record = value as? EncodableRecord {
|
||||
try record.encode(to: &_persistenceContainer)
|
||||
} else {
|
||||
try value.encode(to: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ColumnEncoder
|
||||
|
||||
/// The encoder that encodes into a database column
|
||||
private class ColumnEncoder<Record: EncodableRecord>: Encoder {
|
||||
var recordEncoder: RecordEncoder<Record>
|
||||
var key: any CodingKey
|
||||
var codingPath: [any CodingKey] { [key] }
|
||||
var userInfo: [CodingUserInfoKey: Any] { Record.databaseEncodingUserInfo }
|
||||
var requiresJSON = false
|
||||
|
||||
init(recordEncoder: RecordEncoder<Record>, key: some CodingKey) {
|
||||
self.recordEncoder = recordEncoder
|
||||
self.key = key
|
||||
}
|
||||
|
||||
func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key: CodingKey {
|
||||
// We need to perform JSON encoding. Unfortunately we can't access the
|
||||
// inner container of Foundation's JSONEncoder. At this point we must
|
||||
// throw an error so that the caller can retry encoding from scratch.
|
||||
// Unfortunately (bis), we can't throw right from here, so let's
|
||||
// return a JSONRequiredEncoder that will throw as soon as possible.
|
||||
requiresJSON = true
|
||||
let container = JSONRequiredEncoder.KeyedContainer<Key>(codingPath: codingPath)
|
||||
return KeyedEncodingContainer(container)
|
||||
}
|
||||
|
||||
func unkeyedContainer() -> UnkeyedEncodingContainer {
|
||||
// We need to perform JSON encoding. Unfortunately we can't access the
|
||||
// inner container of Foundation's JSONEncoder. At this point we must
|
||||
// throw an error so that the caller can retry encoding from scratch.
|
||||
// Unfortunately (bis), we can't throw right from here, so let's
|
||||
// return a JSONRequiredEncoder that will throw as soon as possible.
|
||||
requiresJSON = true
|
||||
return JSONRequiredEncoder(codingPath: codingPath)
|
||||
}
|
||||
|
||||
func singleValueContainer() -> SingleValueEncodingContainer { self }
|
||||
}
|
||||
|
||||
extension ColumnEncoder: SingleValueEncodingContainer {
|
||||
func encodeNil() throws { recordEncoder.persist(nil, forKey: key) }
|
||||
|
||||
func encode(_ value: Bool ) throws { recordEncoder.persist(value, forKey: key) }
|
||||
func encode(_ value: Int ) throws { recordEncoder.persist(value, forKey: key) }
|
||||
func encode(_ value: Int8 ) throws { recordEncoder.persist(value, forKey: key) }
|
||||
func encode(_ value: Int16 ) throws { recordEncoder.persist(value, forKey: key) }
|
||||
func encode(_ value: Int32 ) throws { recordEncoder.persist(value, forKey: key) }
|
||||
func encode(_ value: Int64 ) throws { recordEncoder.persist(value, forKey: key) }
|
||||
func encode(_ value: UInt ) throws { recordEncoder.persist(value, forKey: key) }
|
||||
func encode(_ value: UInt8 ) throws { recordEncoder.persist(value, forKey: key) }
|
||||
func encode(_ value: UInt16) throws { recordEncoder.persist(value, forKey: key) }
|
||||
func encode(_ value: UInt32) throws { recordEncoder.persist(value, forKey: key) }
|
||||
func encode(_ value: UInt64) throws { recordEncoder.persist(value, forKey: key) }
|
||||
func encode(_ value: Float ) throws { recordEncoder.persist(value, forKey: key) }
|
||||
func encode(_ value: Double) throws { recordEncoder.persist(value, forKey: key) }
|
||||
func encode(_ value: String) throws { recordEncoder.persist(value, forKey: key) }
|
||||
|
||||
func encode<T>(_ value: T) throws where T: Encodable {
|
||||
try recordEncoder.encode(value, forKey: key)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,725 @@
|
||||
import Foundation // For JSONEncoder
|
||||
|
||||
/// A type that can encode itself in a database row.
|
||||
///
|
||||
/// To conform to `EncodableRecord`, provide an implementation for the
|
||||
/// ``encode(to:)-k9pf`` method. This implementation is ready-made for
|
||||
/// `Encodable` types.
|
||||
///
|
||||
/// Most of the time, your record types will get `EncodableRecord` conformance
|
||||
/// through the ``MutablePersistableRecord`` or ``PersistableRecord`` protocols,
|
||||
/// which provide persistence methods.
|
||||
///
|
||||
/// ## Topics
|
||||
///
|
||||
/// ### Encoding a Database Row
|
||||
///
|
||||
/// - ``encode(to:)-k9pf``
|
||||
/// - ``PersistenceContainer``
|
||||
///
|
||||
/// ### Configuring Persistence for the Standard Encodable Protocol
|
||||
///
|
||||
/// - ``databaseColumnEncodingStrategy-5sx4v``
|
||||
/// - ``databaseDataEncodingStrategy-9y0c7``
|
||||
/// - ``databaseDateEncodingStrategy-2gtc1``
|
||||
/// - ``databaseEncodingUserInfo-8upii``
|
||||
/// - ``databaseJSONEncoder(for:)-6x62c``
|
||||
/// - ``databaseUUIDEncodingStrategy-2t96q``
|
||||
/// - ``DatabaseColumnEncodingStrategy``
|
||||
/// - ``DatabaseDataEncodingStrategy``
|
||||
/// - ``DatabaseDateEncodingStrategy``
|
||||
/// - ``DatabaseUUIDEncodingStrategy``
|
||||
///
|
||||
/// ### Converting a Record to a Dictionary
|
||||
///
|
||||
/// - ``databaseDictionary``
|
||||
///
|
||||
/// ### Comparing Records
|
||||
///
|
||||
/// - ``databaseChanges(from:)``
|
||||
/// - ``databaseChanges(modify:)``
|
||||
/// - ``databaseEquals(_:)``
|
||||
public protocol EncodableRecord {
|
||||
/// Encodes the record into the provided persistence container.
|
||||
///
|
||||
/// In your implementation of this method, store in the `container` argument
|
||||
/// all values that should be stored in database columns.
|
||||
///
|
||||
/// Primary key columns, if any, must be included.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: EncodableRecord {
|
||||
/// var id: Int64?
|
||||
/// var name: String?
|
||||
///
|
||||
/// func encode(to container: inout PersistenceContainer) {
|
||||
/// container["id"] = id
|
||||
/// container["name"] = name
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// It is undefined behavior to set different values for the same column.
|
||||
/// Column names are case insensitive, so defining both "name" and "NAME"
|
||||
/// is considered undefined behavior.
|
||||
///
|
||||
/// - throws: An error is thrown if the record can't be encoded to its
|
||||
/// database representation.
|
||||
func encode(to container: inout PersistenceContainer) throws
|
||||
|
||||
// MARK: - Customizing the Format of Database Columns
|
||||
|
||||
/// Contextual information made available to the
|
||||
/// `Encodable.encode(to:)` method.
|
||||
///
|
||||
/// This property is dedicated to ``EncodableRecord`` types that also
|
||||
/// conform to the standard `Encodable` protocol and use the default
|
||||
/// ``encode(to:)-1mrt`` implementation.
|
||||
///
|
||||
/// The returned dictionary is returned by `Encoder.userInfo` when the
|
||||
/// record is encoded.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// // A key that holds a encoder's name
|
||||
/// let encoderName = CodingUserInfoKey(rawValue: "encoderName")!
|
||||
///
|
||||
/// struct Player: PersistableRecord, Encodable {
|
||||
/// // Customize the encoder name when encoding a database row
|
||||
/// static let databaseEncodingUserInfo: [CodingUserInfoKey: Any] = [encoderName: "Database"]
|
||||
///
|
||||
/// func encode(to encoder: Encoder) throws {
|
||||
/// // Print the encoder name
|
||||
/// print(encoder.userInfo[encoderName])
|
||||
/// ...
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// let player = Player(...)
|
||||
///
|
||||
/// // prints "Database"
|
||||
/// try player.insert(db)
|
||||
///
|
||||
/// // prints "JSON"
|
||||
/// let encoder = JSONEncoder()
|
||||
/// encoder.userInfo = [encoderName: "JSON"]
|
||||
/// let data = try encoder.encode(player)
|
||||
/// ```
|
||||
static var databaseEncodingUserInfo: [CodingUserInfoKey: Any] { get }
|
||||
|
||||
/// Returns the `JSONEncoder` that encodes the value for a given column.
|
||||
///
|
||||
/// This method is dedicated to ``EncodableRecord`` types that also conform
|
||||
/// to the standard `Encodable` protocol and use the default
|
||||
/// ``encode(to:)-1mrt`` implementation.
|
||||
static func databaseJSONEncoder(for column: String) -> JSONEncoder
|
||||
|
||||
/// The strategy for encoding `Data` columns.
|
||||
///
|
||||
/// This property is dedicated to ``EncodableRecord`` types that also
|
||||
/// conform to the standard `Encodable` protocol and use the default
|
||||
/// ``encode(to:)-1mrt`` implementation.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: EncodableRecord, Encodable {
|
||||
/// static let databaseDataEncodingStrategy = DatabaseDataEncodingStrategy.text
|
||||
///
|
||||
/// // Encoded as SQL text. Data must contain valid UTF8 bytes.
|
||||
/// var jsonData: Data
|
||||
/// }
|
||||
/// ```
|
||||
static var databaseDataEncodingStrategy: DatabaseDataEncodingStrategy { get }
|
||||
|
||||
/// The strategy for encoding `Date` columns.
|
||||
///
|
||||
/// This property is dedicated to ``EncodableRecord`` types that also
|
||||
/// conform to the standard `Encodable` protocol and use the default
|
||||
/// ``encode(to:)-1mrt`` implementation.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: EncodableRecord, Encodable {
|
||||
/// static let databaseDateEncodingStrategy = DatabaseDateEncodingStrategy.timeIntervalSince1970
|
||||
///
|
||||
/// // Encoded as an epoch timestamp
|
||||
/// var creationDate: Date
|
||||
/// }
|
||||
/// ```
|
||||
static var databaseDateEncodingStrategy: DatabaseDateEncodingStrategy { get }
|
||||
|
||||
/// The strategy for encoding `UUID` columns.
|
||||
///
|
||||
/// This property is dedicated to ``EncodableRecord`` types that also
|
||||
/// conform to the standard `Encodable` protocol and use the default
|
||||
/// ``encode(to:)-1mrt`` implementation.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: EncodableRecord, Encodable {
|
||||
/// static let databaseUUIDEncodingStrategy = DatabaseUUIDEncodingStrategy.uppercaseString
|
||||
///
|
||||
/// // Encoded in a string like "E621E1F8-C36C-495A-93FC-0C247A3E6E5F"
|
||||
/// var uuid: UUID
|
||||
/// }
|
||||
/// ```
|
||||
static var databaseUUIDEncodingStrategy: DatabaseUUIDEncodingStrategy { get }
|
||||
|
||||
/// The strategy for converting coding keys to column names.
|
||||
///
|
||||
/// This property is dedicated to ``EncodableRecord`` types that also
|
||||
/// conform to the standard `Encodable` protocol and use the default
|
||||
/// ``encode(to:)-1mrt`` implementation.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: EncodableProtocol, Encodable {
|
||||
/// static let databaseColumnEncodingStrategy = DatabaseColumnEncodingStrategy.convertToSnakeCase
|
||||
///
|
||||
/// // Encoded in the 'player_id' column
|
||||
/// var playerID: String
|
||||
/// }
|
||||
/// ```
|
||||
static var databaseColumnEncodingStrategy: DatabaseColumnEncodingStrategy { get }
|
||||
}
|
||||
|
||||
extension EncodableRecord {
|
||||
/// Contextual information made available to the
|
||||
/// `Encodable.encode(to:)` method.
|
||||
///
|
||||
/// The default implementation returns an empty dictionary.
|
||||
public static var databaseEncodingUserInfo: [CodingUserInfoKey: Any] {
|
||||
[:]
|
||||
}
|
||||
|
||||
/// Returns the `JSONEncoder` that encodes the value for a given column.
|
||||
///
|
||||
/// The default implementation returns a `JSONEncoder` with the
|
||||
/// following properties:
|
||||
///
|
||||
/// - `dataEncodingStrategy`: `.base64`
|
||||
/// - `dateEncodingStrategy`: `.millisecondsSince1970`
|
||||
/// - `nonConformingFloatEncodingStrategy`: `.throw`
|
||||
/// - `outputFormatting`: `.sortedKeys`
|
||||
public static func databaseJSONEncoder(for column: String) -> JSONEncoder {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.dataEncodingStrategy = .base64
|
||||
encoder.dateEncodingStrategy = .millisecondsSince1970
|
||||
encoder.nonConformingFloatEncodingStrategy = .throw
|
||||
// guarantee some stability in order to ease record comparison
|
||||
encoder.outputFormatting = .sortedKeys
|
||||
encoder.userInfo = databaseEncodingUserInfo
|
||||
return encoder
|
||||
}
|
||||
|
||||
/// Returns the default strategy for encoding `Data` columns:
|
||||
/// ``DatabaseDataEncodingStrategy/deferredToData``.
|
||||
public static var databaseDataEncodingStrategy: DatabaseDataEncodingStrategy {
|
||||
.deferredToData
|
||||
}
|
||||
|
||||
/// Returns the default strategy for encoding `Date` columns:
|
||||
/// ``DatabaseDateEncodingStrategy/deferredToDate``.
|
||||
public static var databaseDateEncodingStrategy: DatabaseDateEncodingStrategy {
|
||||
.deferredToDate
|
||||
}
|
||||
|
||||
/// Returns the default strategy for encoding `UUID` columns:
|
||||
/// ``DatabaseUUIDEncodingStrategy/deferredToUUID``.
|
||||
public static var databaseUUIDEncodingStrategy: DatabaseUUIDEncodingStrategy {
|
||||
.deferredToUUID
|
||||
}
|
||||
|
||||
/// Returns the default strategy for converting coding keys to column names:
|
||||
/// ``DatabaseColumnEncodingStrategy/useDefaultKeys``.
|
||||
public static var databaseColumnEncodingStrategy: DatabaseColumnEncodingStrategy {
|
||||
.useDefaultKeys
|
||||
}
|
||||
}
|
||||
|
||||
extension EncodableRecord {
|
||||
/// A dictionary whose keys are the columns encoded in the
|
||||
/// <doc:/documentation/GRDB/EncodableRecord/encode(to:)-k9pf> method.
|
||||
///
|
||||
/// - throws: An error is thrown if the record can't be encoded to its
|
||||
/// database representation.
|
||||
public var databaseDictionary: [String: DatabaseValue] {
|
||||
get throws {
|
||||
try Dictionary(PersistenceContainer(self).storage).mapValues { $0?.databaseValue ?? .null }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension EncodableRecord {
|
||||
|
||||
// MARK: - Record Comparison
|
||||
|
||||
/// Returns a boolean indicating whether this record and the other record
|
||||
/// have the same database representation.
|
||||
public func databaseEquals(_ record: Self) -> Bool {
|
||||
do {
|
||||
return try PersistenceContainer(self).changesIterator(from: PersistenceContainer(record)).next() == nil
|
||||
} catch {
|
||||
// one record can't be encoded: they can't be identical in the database
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a dictionary of values changed from the other record.
|
||||
///
|
||||
/// The keys of the dictionary are the column names for which record do not
|
||||
/// share the same value. Values are the database values from the
|
||||
/// `other` record.
|
||||
///
|
||||
/// Note that the `other` record does not have to have the same type of the
|
||||
/// receiver record. When the two records don't define the same set of
|
||||
/// columns in their <doc:/documentation/GRDB/EncodableRecord/encode(to:)-k9pf>
|
||||
/// method, only the columns defined by the receiver are considered.
|
||||
///
|
||||
/// - throws: An error is thrown if one record can't be encoded to its
|
||||
/// database representation.
|
||||
public func databaseChanges(from record: some EncodableRecord)
|
||||
throws -> [String: DatabaseValue]
|
||||
{
|
||||
let changes = try PersistenceContainer(self).changesIterator(from: PersistenceContainer(record))
|
||||
return Dictionary(uniqueKeysWithValues: changes)
|
||||
}
|
||||
|
||||
/// Modifies the record according to the provided `modify` closure, and
|
||||
/// returns a dictionary of changed values.
|
||||
///
|
||||
/// The keys of the dictionary are the changed column names. Values are
|
||||
/// the database values from the initial version record.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// var player = Player(id: 1, score: 1000, hasAward: false)
|
||||
/// let changes = try player.databaseChanges {
|
||||
/// $0.score = 1000
|
||||
/// $0.hasAward = true
|
||||
/// }
|
||||
///
|
||||
/// player.hasAward // true (changed)
|
||||
///
|
||||
/// changes["score"] // nil (not changed)
|
||||
/// changes["hasAward"] // false (old value)
|
||||
/// ```
|
||||
///
|
||||
/// - parameter modify: A closure that modifies the record.
|
||||
public mutating func databaseChanges(modify: (inout Self) throws -> Void)
|
||||
throws -> [String: DatabaseValue]
|
||||
{
|
||||
let container = try PersistenceContainer(self)
|
||||
try modify(&self)
|
||||
let changes = try PersistenceContainer(self).changesIterator(from: container)
|
||||
return Dictionary(uniqueKeysWithValues: changes)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - PersistenceContainer
|
||||
|
||||
/// A container for database values to store in a database row.
|
||||
///
|
||||
/// `PersistenceContainer` is the argument of the
|
||||
/// ``EncodableRecord/encode(to:)-k9pf`` method.
|
||||
public struct PersistenceContainer {
|
||||
// fileprivate for Row(_:PersistenceContainer)
|
||||
// The ordering of the OrderedDictionary helps generating always the same
|
||||
// SQL queries, and hit the statement cache.
|
||||
fileprivate var storage: OrderedDictionary<String, (any DatabaseValueConvertible)?>
|
||||
|
||||
/// The value associated with the given column.
|
||||
public subscript(_ column: String) -> (any DatabaseValueConvertible)? {
|
||||
get { self[caseInsensitive: column] }
|
||||
set { storage.updateValue(newValue, forKey: column) }
|
||||
}
|
||||
|
||||
/// The value associated with the given column.
|
||||
public subscript(_ column: some ColumnExpression) -> (any DatabaseValueConvertible)? {
|
||||
get { self[column.name] }
|
||||
set { self[column.name] = newValue }
|
||||
}
|
||||
|
||||
init() {
|
||||
storage = OrderedDictionary()
|
||||
}
|
||||
|
||||
init(minimumCapacity: Int) {
|
||||
storage = OrderedDictionary(minimumCapacity: minimumCapacity)
|
||||
}
|
||||
|
||||
/// Convenience initializer from a record
|
||||
init<Record: EncodableRecord>(_ record: Record) throws {
|
||||
self.init()
|
||||
try record.encode(to: &self)
|
||||
}
|
||||
|
||||
/// Convenience initializer from a database connection and a record
|
||||
@usableFromInline
|
||||
init(_ db: Database, _ record: some EncodableRecord & TableRecord) throws {
|
||||
let databaseTableName = type(of: record).databaseTableName
|
||||
let columnCount = try db.columns(in: databaseTableName).count
|
||||
self.init(minimumCapacity: columnCount) // Optimization
|
||||
try record.encode(to: &self)
|
||||
}
|
||||
|
||||
/// Columns stored in the container, ordered like values.
|
||||
var columns: [String] { Array(storage.keys) }
|
||||
|
||||
/// Values stored in the container, ordered like columns.
|
||||
var values: [(any DatabaseValueConvertible)?] { Array(storage.values) }
|
||||
|
||||
/// Accesses the value associated with the given column, in a
|
||||
/// case-insensitive fashion.
|
||||
subscript(caseInsensitive column: String) -> (any DatabaseValueConvertible)? {
|
||||
get {
|
||||
if let value = storage[column] {
|
||||
return value
|
||||
}
|
||||
let lowercaseColumn = column.lowercased()
|
||||
for (key, value) in storage where key.lowercased() == lowercaseColumn {
|
||||
return value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
set {
|
||||
if storage[column] != nil {
|
||||
storage[column] = newValue
|
||||
return
|
||||
}
|
||||
let lowercaseColumn = column.lowercased()
|
||||
for key in storage.keys where key.lowercased() == lowercaseColumn {
|
||||
storage[key] = newValue
|
||||
return
|
||||
}
|
||||
|
||||
storage[column] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
// Returns nil if column is not defined
|
||||
func value(forCaseInsensitiveColumn column: String) -> DatabaseValue? {
|
||||
let lowercaseColumn = column.lowercased()
|
||||
for (key, value) in storage where key.lowercased() == lowercaseColumn {
|
||||
return value?.databaseValue ?? .null
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var isEmpty: Bool { storage.isEmpty }
|
||||
|
||||
/// An iterator over the (column, value) pairs
|
||||
func makeIterator() -> IndexingIterator<OrderedDictionary<String, (any DatabaseValueConvertible)?>> {
|
||||
storage.makeIterator()
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
func changesIterator(from container: PersistenceContainer) -> AnyIterator<(String, DatabaseValue)> {
|
||||
var newValueIterator = makeIterator()
|
||||
return AnyIterator {
|
||||
// Loop until we find a change, or exhaust columns:
|
||||
while let (column, newValue) = newValueIterator.next() {
|
||||
let oldValue = container[caseInsensitive: column]
|
||||
let oldDbValue = oldValue?.databaseValue ?? .null
|
||||
let newDbValue = newValue?.databaseValue ?? .null
|
||||
if newDbValue != oldDbValue {
|
||||
return (column, oldDbValue)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Row {
|
||||
convenience init<Record: EncodableRecord>(_ record: Record) throws {
|
||||
try self.init(PersistenceContainer(record))
|
||||
}
|
||||
|
||||
convenience init(_ container: PersistenceContainer) {
|
||||
self.init(Dictionary(container.storage))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - DatabaseDataEncodingStrategy
|
||||
|
||||
/// `DatabaseDataEncodingStrategy` specifies how `EncodableRecord` types that
|
||||
/// also adopt the standard `Encodable` protocol encode their `Data` properties
|
||||
/// in the default <doc:/documentation/GRDB/EncodableRecord/encode(to:)-1mrt>
|
||||
/// implementation.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: EncodableRecord, Encodable {
|
||||
/// static let databaseDataEncodingStrategy = DatabaseDataEncodingStrategy.text
|
||||
///
|
||||
/// // Encoded as SQL text. Data must contain valid UTF8 bytes.
|
||||
/// var jsonData: Data
|
||||
/// }
|
||||
/// ```
|
||||
public enum DatabaseDataEncodingStrategy {
|
||||
/// Encodes `Data` columns as SQL blob.
|
||||
case deferredToData
|
||||
|
||||
/// Encodes `Data` columns as SQL text. Data must contain valid UTF8 bytes.
|
||||
case text
|
||||
|
||||
/// Encodes `Data` column as the result of the user-provided function.
|
||||
case custom((Data) -> (any DatabaseValueConvertible)?)
|
||||
|
||||
func encode(_ data: Data) -> DatabaseValue {
|
||||
switch self {
|
||||
case .deferredToData:
|
||||
return data.databaseValue
|
||||
case .text:
|
||||
guard let string = String(data: data, encoding: .utf8) else {
|
||||
fatalError("Invalid UTF8 data")
|
||||
}
|
||||
return string.databaseValue
|
||||
case .custom(let format):
|
||||
return format(data)?.databaseValue ?? .null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - DatabaseDateEncodingStrategy
|
||||
|
||||
/// `DatabaseDateEncodingStrategy` specifies how `EncodableRecord` types that
|
||||
/// also adopt the standard `Encodable` protocol encode their `Date` properties
|
||||
/// in the default <doc:/documentation/GRDB/EncodableRecord/encode(to:)-1mrt>
|
||||
/// implementation.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: EncodableRecord, Encodable {
|
||||
/// static let databaseDateEncodingStrategy = DatabaseDateEncodingStrategy.timeIntervalSince1970
|
||||
///
|
||||
/// // Encoded as an epoch timestamp
|
||||
/// var creationDate: Date
|
||||
/// }
|
||||
/// ```
|
||||
public enum DatabaseDateEncodingStrategy {
|
||||
/// The strategy that uses formatting from the Date structure.
|
||||
///
|
||||
/// It encodes dates using the format "YYYY-MM-DD HH:MM:SS.SSS" in the
|
||||
/// UTC time zone.
|
||||
case deferredToDate
|
||||
|
||||
/// Encodes a Double: the number of seconds between the date and
|
||||
/// midnight UTC on 1 January 2001
|
||||
case timeIntervalSinceReferenceDate
|
||||
|
||||
/// Encodes a Double: the number of seconds between the date and
|
||||
/// midnight UTC on 1 January 1970
|
||||
case timeIntervalSince1970
|
||||
|
||||
/// Encodes an Int64: the number of seconds between the date and
|
||||
/// midnight UTC on 1 January 1970
|
||||
case secondsSince1970
|
||||
|
||||
/// Encodes an Int64: the number of milliseconds between the date and
|
||||
/// midnight UTC on 1 January 1970
|
||||
case millisecondsSince1970
|
||||
|
||||
/// Encodes dates according to the ISO 8601 and RFC 3339 standards
|
||||
case iso8601
|
||||
|
||||
/// Encodes a String, according to the provided formatter
|
||||
case formatted(DateFormatter)
|
||||
|
||||
/// Encodes the result of the user-provided function
|
||||
case custom((Date) -> (any DatabaseValueConvertible)?)
|
||||
|
||||
private static let iso8601Formatter: ISO8601DateFormatter = {
|
||||
let formatter = ISO8601DateFormatter()
|
||||
formatter.formatOptions = .withInternetDateTime
|
||||
return formatter
|
||||
}()
|
||||
|
||||
func encode(_ date: Date) -> DatabaseValue {
|
||||
switch self {
|
||||
case .deferredToDate:
|
||||
return date.databaseValue
|
||||
case .timeIntervalSinceReferenceDate:
|
||||
return date.timeIntervalSinceReferenceDate.databaseValue
|
||||
case .timeIntervalSince1970:
|
||||
return date.timeIntervalSince1970.databaseValue
|
||||
case .millisecondsSince1970:
|
||||
return Int64(floor(1000.0 * date.timeIntervalSince1970)).databaseValue
|
||||
case .secondsSince1970:
|
||||
return Int64(floor(date.timeIntervalSince1970)).databaseValue
|
||||
case .iso8601:
|
||||
return Self.iso8601Formatter.string(from: date).databaseValue
|
||||
case .formatted(let formatter):
|
||||
return formatter.string(from: date).databaseValue
|
||||
case .custom(let format):
|
||||
return format(date)?.databaseValue ?? .null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - DatabaseUUIDEncodingStrategy
|
||||
|
||||
/// `DatabaseUUIDEncodingStrategy` specifies how `EncodableRecord` types that
|
||||
/// also adopt the standard `Encodable` protocol encode their `UUID` properties
|
||||
/// in the default <doc:/documentation/GRDB/EncodableRecord/encode(to:)-1mrt>
|
||||
/// implementation.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: EncodableRecord, Encodable {
|
||||
/// static let databaseUUIDEncodingStrategy = DatabaseUUIDEncodingStrategy.uppercaseString
|
||||
///
|
||||
/// // Encoded in a string like "E621E1F8-C36C-495A-93FC-0C247A3E6E5F"
|
||||
/// var uuid: UUID
|
||||
/// }
|
||||
/// ```
|
||||
public enum DatabaseUUIDEncodingStrategy: Sendable {
|
||||
/// The strategy that uses formatting from the UUID type.
|
||||
///
|
||||
/// It encodes UUIDs as 16-bytes data blobs.
|
||||
case deferredToUUID
|
||||
|
||||
/// Encodes UUIDs as uppercased strings such as "E621E1F8-C36C-495A-93FC-0C247A3E6E5F"
|
||||
case uppercaseString
|
||||
|
||||
/// Encodes UUIDs as lowercased strings such as "e621e1f8-c36c-495a-93fc-0c247a3e6e5f"
|
||||
case lowercaseString
|
||||
|
||||
func encode(_ uuid: UUID) -> DatabaseValue {
|
||||
switch self {
|
||||
case .deferredToUUID:
|
||||
return uuid.databaseValue
|
||||
case .uppercaseString:
|
||||
return uuid.uuidString.databaseValue
|
||||
case .lowercaseString:
|
||||
return uuid.uuidString.lowercased().databaseValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - DatabaseColumnEncodingStrategy
|
||||
|
||||
/// `DatabaseColumnEncodingStrategy` specifies how `EncodableRecord` types that
|
||||
/// also adopt the standard `Encodable` protocol encode their coding keys into
|
||||
/// database columns in the default <doc:/documentation/GRDB/EncodableRecord/encode(to:)-1mrt>
|
||||
/// implementation.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: EncodableProtocol, Encodable {
|
||||
/// static let databaseColumnEncodingStrategy = DatabaseColumnEncodingStrategy.convertToSnakeCase
|
||||
///
|
||||
/// // Encoded in the 'player_id' column
|
||||
/// var playerID: String
|
||||
/// }
|
||||
/// ```
|
||||
public enum DatabaseColumnEncodingStrategy {
|
||||
/// A key encoding strategy that doesn’t change key names during encoding.
|
||||
case useDefaultKeys
|
||||
|
||||
/// A key encoding strategy that converts camel-case keys to snake-case keys.
|
||||
case convertToSnakeCase
|
||||
|
||||
/// A key encoding strategy defined by the closure you supply.
|
||||
case custom((any CodingKey) -> String)
|
||||
|
||||
func column(forKey key: some CodingKey) -> String {
|
||||
switch self {
|
||||
case .useDefaultKeys:
|
||||
return key.stringValue
|
||||
case .convertToSnakeCase:
|
||||
return Self._convertToSnakeCase(key.stringValue)
|
||||
case let .custom(column):
|
||||
return column(key)
|
||||
}
|
||||
}
|
||||
|
||||
// Copied straight from
|
||||
// https://github.com/apple/swift-corelibs-foundation/blob/8d6398d76eaf886a214e0bb2bd7549d968f7b40e/Sources/Foundation/JSONEncoder.swift#L127
|
||||
static func _convertToSnakeCase(_ stringKey: String) -> String {
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This function is part of the Swift.org open source project
|
||||
//
|
||||
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
|
||||
// Licensed under Apache License v2.0 with Runtime Library Exception
|
||||
//
|
||||
// See https://swift.org/LICENSE.txt for license information
|
||||
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
guard !stringKey.isEmpty else { return stringKey }
|
||||
|
||||
var words: [Range<String.Index>] = []
|
||||
// The general idea of this algorithm is to split words on transition
|
||||
// from lower to upper case, then on transition of >1 upper case
|
||||
// characters to lowercase
|
||||
//
|
||||
// myProperty -> my_property
|
||||
// myURLProperty -> my_url_property
|
||||
//
|
||||
// We assume, per Swift naming conventions, that the first character of
|
||||
// the key is lowercase.
|
||||
var wordStart = stringKey.startIndex
|
||||
var searchRange = stringKey.index(after: wordStart)..<stringKey.endIndex
|
||||
|
||||
// Find next uppercase character
|
||||
while let upperCaseRange = stringKey.rangeOfCharacter(
|
||||
from: CharacterSet.uppercaseLetters,
|
||||
options: [], range: searchRange)
|
||||
{
|
||||
let untilUpperCase = wordStart..<upperCaseRange.lowerBound
|
||||
words.append(untilUpperCase)
|
||||
|
||||
// Find next lowercase character
|
||||
searchRange = upperCaseRange.lowerBound..<searchRange.upperBound
|
||||
guard let lowerCaseRange = stringKey.rangeOfCharacter(
|
||||
from: CharacterSet.lowercaseLetters,
|
||||
options: [],
|
||||
range: searchRange)
|
||||
else {
|
||||
// There are no more lower case letters. Just end here.
|
||||
wordStart = searchRange.lowerBound
|
||||
break
|
||||
}
|
||||
|
||||
// Is the next lowercase letter more than 1 after the uppercase? If
|
||||
// so, we encountered a group of uppercase letters that we should
|
||||
// treat as its own word
|
||||
let nextCharacterAfterCapital = stringKey.index(after: upperCaseRange.lowerBound)
|
||||
if lowerCaseRange.lowerBound == nextCharacterAfterCapital {
|
||||
// The next character after capital is a lower case character
|
||||
// and therefore not a word boundary.
|
||||
// Continue searching for the next upper case for the boundary.
|
||||
wordStart = upperCaseRange.lowerBound
|
||||
} else {
|
||||
// There was a range of >1 capital letters. Turn those into a
|
||||
// word, stopping at the capital before the lower case character.
|
||||
let beforeLowerIndex = stringKey.index(before: lowerCaseRange.lowerBound)
|
||||
words.append(upperCaseRange.lowerBound..<beforeLowerIndex)
|
||||
|
||||
// Next word starts at the capital before the lowercase we just found
|
||||
wordStart = beforeLowerIndex
|
||||
}
|
||||
searchRange = lowerCaseRange.upperBound..<searchRange.upperBound
|
||||
}
|
||||
words.append(wordStart..<searchRange.upperBound)
|
||||
let result = words
|
||||
.map { (range) in stringKey[range].lowercased() }
|
||||
.joined(separator: "_")
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,936 @@
|
||||
import Foundation
|
||||
|
||||
extension FetchableRecord where Self: Decodable {
|
||||
/// Creates a record from `row`, using the `Decodable` conformance.
|
||||
public init(row: Row) throws {
|
||||
self = try FetchableRecordDecoder().decode(Self.self, from: row)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO GRDB7: make it a final class, and Sendable.
|
||||
/// An object that decodes fetchable records from database rows.
|
||||
///
|
||||
/// The example below shows how to decode an instance of a simple `Player`
|
||||
/// type, that conforms to both ``FetchableRecord`` and `Decodable`, from a
|
||||
/// database row.
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: FetchableRecord, Decodable {
|
||||
/// var id: Int64
|
||||
/// var name: String
|
||||
/// var score: Int
|
||||
/// }
|
||||
///
|
||||
/// try dbQueue.read { db in
|
||||
/// if let row = try Row.fetchOne(db, sql: "SELECT * FROM player WHERE id = 42") {
|
||||
/// let decoder = FetchableRecordDecoder()
|
||||
/// let player = try decoder.decode(Player.self, from: row)
|
||||
/// print(player.name)
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// You will generally not need to create an instance of
|
||||
/// `FetchableRecordDecoder`. The above sample code is correct, but you will
|
||||
/// generally write instead:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.read { db in
|
||||
/// // Prefer the init(row:) initializer:
|
||||
/// if let row = try Row.fetchOne(db, sql: "SELECT * FROM player WHERE id = 42") {
|
||||
/// let player = try Player(row: row)
|
||||
/// print(player.name)
|
||||
/// }
|
||||
///
|
||||
/// // OR just directly fetch a player:
|
||||
/// if let player = try Player.fetchOne(db, sql: "SELECT * FROM player WHERE id = 42") {
|
||||
/// print(player.name)
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The behavior of the decoder depends on the decoded type. See:
|
||||
///
|
||||
/// - ``FetchableRecord/databaseColumnDecodingStrategy-6uefz``
|
||||
/// - ``FetchableRecord/databaseDataDecodingStrategy-71bh1``
|
||||
/// - ``FetchableRecord/databaseDateDecodingStrategy-78y03``
|
||||
/// - ``FetchableRecord/databaseDecodingUserInfo-77jim``
|
||||
/// - ``FetchableRecord/databaseJSONDecoder(for:)-7lmxd``
|
||||
public class FetchableRecordDecoder {
|
||||
/// Creates a decoder for fetchable records.
|
||||
public init() { }
|
||||
|
||||
/// Returns a record of the type you specify, decoded from a
|
||||
/// database row.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - type: The type of the record to decode from the supplied
|
||||
/// database row.
|
||||
/// - row: The database row to decode.
|
||||
/// - Returns: An instance of the specified record type, if the decoder
|
||||
/// can parse the database row.
|
||||
public func decode<T: FetchableRecord & Decodable>(_ type: T.Type, from row: Row) throws -> T {
|
||||
let decoder = _RowDecoder<T>(row: row, codingPath: [], columnDecodingStrategy: T.databaseColumnDecodingStrategy)
|
||||
return try T(from: decoder)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - _RowDecoder
|
||||
|
||||
/// The decoder that decodes a record from a database row
|
||||
private struct _RowDecoder<R: FetchableRecord>: Decoder {
|
||||
var row: Row
|
||||
var codingPath: [CodingKey]
|
||||
var columnDecodingStrategy: DatabaseColumnDecodingStrategy
|
||||
var userInfo: [CodingUserInfoKey: Any] { R.databaseDecodingUserInfo }
|
||||
|
||||
func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> {
|
||||
KeyedDecodingContainer(KeyedContainer<Key>(decoder: self))
|
||||
}
|
||||
|
||||
func unkeyedContainer() throws -> UnkeyedDecodingContainer {
|
||||
guard let codingKey = codingPath.last else {
|
||||
fatalError("unkeyed decoding from database row is not supported")
|
||||
}
|
||||
let keys = row.prefetchedRows.keys
|
||||
let debugDescription: String
|
||||
if keys.isEmpty {
|
||||
debugDescription = "No available prefetched rows"
|
||||
} else {
|
||||
debugDescription = "Available keys for prefetched rows: \(keys.sorted())"
|
||||
}
|
||||
throw DecodingError.keyNotFound(
|
||||
codingKey,
|
||||
DecodingError.Context(
|
||||
codingPath: Array(codingPath.dropLast()),
|
||||
debugDescription: debugDescription))
|
||||
}
|
||||
|
||||
func singleValueContainer() throws -> SingleValueDecodingContainer {
|
||||
guard let key = codingPath.last else {
|
||||
// Not yet sure what we are decoding, this will be decided in the SingleValueDecodingContainer functions.
|
||||
// For decoding an array of scalars (in case of prefetched rows) we pick the first column.
|
||||
return SingleValueRowDecoder(
|
||||
columnDecoder: ColumnDecoder<R>(row: row, columnIndex: 0, codingPath: codingPath),
|
||||
columnDecodingStrategy: columnDecodingStrategy
|
||||
)
|
||||
}
|
||||
guard let index = row.index(forColumn: key.stringValue) else {
|
||||
// Don't use DecodingError.keyNotFound:
|
||||
// We need to specifically recognize missing columns in order to
|
||||
// provide correct feedback.
|
||||
throw RowDecodingError.columnNotFound(key.stringValue, context: RowDecodingContext(row: row))
|
||||
}
|
||||
// TODO: test
|
||||
// See DatabaseValueConversionErrorTests.testDecodableFetchableRecord2
|
||||
return ColumnDecoder<R>(row: row, columnIndex: index, codingPath: codingPath)
|
||||
}
|
||||
|
||||
class KeyedContainer<Key: CodingKey>: KeyedDecodingContainerProtocol {
|
||||
private let decoder: _RowDecoder
|
||||
var codingPath: [CodingKey] { decoder.codingPath }
|
||||
private var decodedRootKey: CodingKey?
|
||||
// Not nil iff decoder has a columnDecodingStrategy
|
||||
private let _columnForKey: [String: String]?
|
||||
|
||||
init(decoder: _RowDecoder) {
|
||||
self.decoder = decoder
|
||||
switch decoder.columnDecodingStrategy {
|
||||
case .useDefaultKeys:
|
||||
_columnForKey = nil
|
||||
default:
|
||||
var columnForKey: [String: String] = [:]
|
||||
for column in decoder.row.columnNames {
|
||||
if let key: Key = decoder.columnDecodingStrategy.key(forColumn: column) {
|
||||
columnForKey[key.stringValue] = column
|
||||
}
|
||||
}
|
||||
_columnForKey = columnForKey
|
||||
}
|
||||
}
|
||||
|
||||
lazy var allKeys: [Key] = {
|
||||
let row = decoder.row
|
||||
// TODO: test when _columnForKey is not nil
|
||||
var keys = _columnForKey.map { Set($0.keys) } ?? Set(row.columnNames)
|
||||
keys.formUnion(row.scopesTree.names)
|
||||
keys.formUnion(row.prefetchedRows.keys)
|
||||
return keys.compactMap(Key.init(stringValue:))
|
||||
}()
|
||||
|
||||
func contains(_ key: Key) -> Bool {
|
||||
let row = decoder.row
|
||||
if let _columnForKey {
|
||||
if let column = _columnForKey[key.stringValue] {
|
||||
assert(row.hasColumn(column))
|
||||
return true
|
||||
}
|
||||
} else if row.hasColumn(key.stringValue) {
|
||||
return true
|
||||
}
|
||||
if row.scopesTree[key.stringValue] != nil {
|
||||
return true
|
||||
}
|
||||
if row.prefetchedRows[key.stringValue] != nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func decodeNil(forKey key: Key) throws -> Bool {
|
||||
let row = decoder.row
|
||||
|
||||
// Column?
|
||||
if let column = try? decodeColumn(forKey: key),
|
||||
let index = row.index(forColumn: column)
|
||||
{
|
||||
return row.hasNull(atIndex: index)
|
||||
}
|
||||
|
||||
// Scope?
|
||||
if let scopedRow = row.scopesTree[key.stringValue] {
|
||||
return scopedRow.containsNonNullValue == false
|
||||
}
|
||||
|
||||
// Prefetched Rows?
|
||||
if row.prefetchedRows[key.stringValue] != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Unknown key
|
||||
return true
|
||||
}
|
||||
|
||||
// swiftlint:disable comma
|
||||
// swiftlint:disable line_length
|
||||
func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { try decoder.row.decode(forKey: decodeColumn(forKey: key)) }
|
||||
func decode(_ type: Int.Type, forKey key: Key) throws -> Int { try decoder.row.decode(forKey: decodeColumn(forKey: key)) }
|
||||
func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 { try decoder.row.decode(forKey: decodeColumn(forKey: key)) }
|
||||
func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 { try decoder.row.decode(forKey: decodeColumn(forKey: key)) }
|
||||
func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 { try decoder.row.decode(forKey: decodeColumn(forKey: key)) }
|
||||
func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 { try decoder.row.decode(forKey: decodeColumn(forKey: key)) }
|
||||
func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt { try decoder.row.decode(forKey: decodeColumn(forKey: key)) }
|
||||
func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 { try decoder.row.decode(forKey: decodeColumn(forKey: key)) }
|
||||
func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 { try decoder.row.decode(forKey: decodeColumn(forKey: key)) }
|
||||
func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 { try decoder.row.decode(forKey: decodeColumn(forKey: key)) }
|
||||
func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 { try decoder.row.decode(forKey: decodeColumn(forKey: key)) }
|
||||
func decode(_ type: Float.Type, forKey key: Key) throws -> Float { try decoder.row.decode(forKey: decodeColumn(forKey: key)) }
|
||||
func decode(_ type: Double.Type, forKey key: Key) throws -> Double { try decoder.row.decode(forKey: decodeColumn(forKey: key)) }
|
||||
func decode(_ type: String.Type, forKey key: Key) throws -> String { try decoder.row.decode(forKey: decodeColumn(forKey: key)) }
|
||||
// swiftlint:enable line_length
|
||||
// swiftlint:enable comma
|
||||
|
||||
private func decodeColumn(forKey key: Key) throws -> String {
|
||||
guard let _columnForKey else {
|
||||
return key.stringValue
|
||||
}
|
||||
|
||||
guard let column = _columnForKey[key.stringValue] else {
|
||||
let errorDescription: String
|
||||
switch decoder.columnDecodingStrategy {
|
||||
case .convertFromSnakeCase:
|
||||
// In this case we can attempt to recover the original value
|
||||
// by reversing the transform
|
||||
let original = key.stringValue
|
||||
let converted = DatabaseColumnEncodingStrategy._convertToSnakeCase(original)
|
||||
let roundtrip = DatabaseColumnDecodingStrategy._convertFromSnakeCase(converted)
|
||||
if converted == original {
|
||||
errorDescription = "\(key) (\"\(original)\")"
|
||||
} else if roundtrip == original {
|
||||
errorDescription = """
|
||||
\(key) (\"\(original)\"), \
|
||||
converted to \(converted)
|
||||
"""
|
||||
} else {
|
||||
errorDescription = """
|
||||
\(key) (\"\(original)\"), \
|
||||
with divergent representation \(roundtrip), \
|
||||
converted to \(converted)
|
||||
"""
|
||||
}
|
||||
default:
|
||||
// Otherwise, just report the converted string
|
||||
errorDescription = "\(key) (\"\(key.stringValue)\")"
|
||||
}
|
||||
|
||||
// TODO: this is not quite correct: key IS NOT a column name.
|
||||
// So we shouldn't use RowKey.columnName. Yet this only impacts
|
||||
// internal types, so the damage is limited.
|
||||
throw RowDecodingError.keyNotFound(
|
||||
.columnName(key.stringValue), // <- See above TODO
|
||||
RowDecodingError.Context(
|
||||
decodingContext: RowDecodingContext(row: decoder.row),
|
||||
debugDescription: "key not found: \(errorDescription)"))
|
||||
}
|
||||
|
||||
return column
|
||||
}
|
||||
|
||||
func decodeIfPresent<T>(_ type: T.Type, forKey key: Key) throws -> T? where T: Decodable {
|
||||
let row = decoder.row
|
||||
|
||||
// Column?
|
||||
if let column = try? decodeColumn(forKey: key),
|
||||
let index = row.index(forColumn: column)
|
||||
{
|
||||
// Prefer DatabaseValueConvertible decoding over Decodable.
|
||||
// This allows decoding Date from String, or DatabaseValue from NULL.
|
||||
if type == Data.self {
|
||||
return try R.databaseDataDecodingStrategy.decodeIfPresent(
|
||||
fromRow: row,
|
||||
atUncheckedIndex: index) as! T?
|
||||
} else if type == Date.self {
|
||||
return try R.databaseDateDecodingStrategy.decodeIfPresent(
|
||||
fromRow: row,
|
||||
atUncheckedIndex: index) as! T?
|
||||
} else if let type = T.self as? any (DatabaseValueConvertible & StatementColumnConvertible).Type {
|
||||
return try type.fastDecodeIfPresent(fromRow: row, atUncheckedIndex: index) as! T?
|
||||
} else if let type = T.self as? any DatabaseValueConvertible.Type {
|
||||
return try type.decodeIfPresent(fromRow: row, atUncheckedIndex: index) as! T?
|
||||
} else if row.impl.hasNull(atUncheckedIndex: index) {
|
||||
return nil
|
||||
} else {
|
||||
return try decode(type, fromRow: row, columnAtIndex: index, key: key)
|
||||
}
|
||||
}
|
||||
|
||||
// Scope?
|
||||
if let scopedRow = row.scopesTree[key.stringValue] {
|
||||
// Beware left joins: check if scoped row contains non-null
|
||||
// values before decoding
|
||||
if scopedRow.containsNonNullValue {
|
||||
return try decode(type, fromRow: scopedRow, codingPath: codingPath + [key])
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Prefetched Rows?
|
||||
if let prefetchedRows = row.prefetchedRows[key.stringValue] {
|
||||
let decoder = PrefetchedRowsDecoder<R>(rows: prefetchedRows, codingPath: codingPath)
|
||||
return try T(from: decoder)
|
||||
}
|
||||
|
||||
// Unknown key
|
||||
return nil
|
||||
}
|
||||
|
||||
func decode<T>(_ type: T.Type, forKey key: Key) throws -> T where T: Decodable {
|
||||
let row = decoder.row
|
||||
|
||||
// Column?
|
||||
if let column = try? decodeColumn(forKey: key),
|
||||
let index = row.index(forColumn: column)
|
||||
{
|
||||
// Prefer DatabaseValueConvertible decoding over Decodable.
|
||||
// This allows decoding Date from String, or DatabaseValue from NULL.
|
||||
if type == Data.self {
|
||||
return try R.databaseDataDecodingStrategy.decode(fromRow: row, atUncheckedIndex: index) as! T
|
||||
} else if type == Date.self {
|
||||
return try R.databaseDateDecodingStrategy.decode(fromRow: row, atUncheckedIndex: index) as! T
|
||||
} else if let type = T.self as? any (DatabaseValueConvertible & StatementColumnConvertible).Type {
|
||||
return try type.fastDecode(fromRow: row, atUncheckedIndex: index) as! T
|
||||
} else if let type = T.self as? any DatabaseValueConvertible.Type {
|
||||
return try type.decode(fromRow: row, atUncheckedIndex: index) as! T
|
||||
} else {
|
||||
return try decode(type, fromRow: row, columnAtIndex: index, key: key)
|
||||
}
|
||||
}
|
||||
|
||||
// Scope?
|
||||
if let scopedRow = row.scopesTree[key.stringValue] {
|
||||
return try decode(type, fromRow: scopedRow, codingPath: codingPath + [key])
|
||||
}
|
||||
|
||||
// Prefetched Rows?
|
||||
if let prefetchedRows = row.prefetchedRows[key.stringValue] {
|
||||
let decoder = PrefetchedRowsDecoder<R>(rows: prefetchedRows, codingPath: codingPath)
|
||||
return try T(from: decoder)
|
||||
}
|
||||
|
||||
// Unknown key
|
||||
//
|
||||
// Should we throw an error? Well... The use case is the following:
|
||||
//
|
||||
// // SELECT book.*, author.* FROM book
|
||||
// // JOIN author ON author.id = book.authorId
|
||||
// let request = Book.including(required: Book.author)
|
||||
//
|
||||
// Rows loaded from this request don't have any "book" key:
|
||||
//
|
||||
// let row = try Row.fetchOne(db, request)!
|
||||
// print(row.debugDescription)
|
||||
// // ▿ [id:1 title:"Moby-Dick" authorId:2]
|
||||
// // unadapted: [id:1 title:"Moby-Dick" authorId:2 id:2 name:"Melville"]
|
||||
// // author: [id:2 name:"Melville"]
|
||||
//
|
||||
// And yet we have to decode the "book" key when we decode the
|
||||
// BookInfo type below:
|
||||
//
|
||||
// struct BookInfo {
|
||||
// var book: Book // <- decodes from the "book" key
|
||||
// var author: Author
|
||||
// }
|
||||
// let infos = try BookInfos.fetchAll(db, request)
|
||||
//
|
||||
// Our current strategy is to assume that a missing key (such as
|
||||
// "book", which is not the name of a column, and not the name of a
|
||||
// scope) has to be decoded right from the base row. But this can
|
||||
// happen only once.
|
||||
if let decodedRootKey {
|
||||
let keys = [decodedRootKey.stringValue, key.stringValue].sorted()
|
||||
throw DecodingError.keyNotFound(key, DecodingError.Context(
|
||||
codingPath: codingPath,
|
||||
debugDescription: "No such key: \(keys.joined(separator: " or "))"))
|
||||
} else {
|
||||
decodedRootKey = key
|
||||
return try decode(type, fromRow: row, codingPath: codingPath + [key])
|
||||
}
|
||||
}
|
||||
|
||||
func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Key)
|
||||
throws -> KeyedDecodingContainer<NestedKey> where NestedKey: CodingKey
|
||||
{
|
||||
let row = decoder.row
|
||||
|
||||
// Column?
|
||||
if let column = try? decodeColumn(forKey: key),
|
||||
row.index(forColumn: column) != nil
|
||||
{
|
||||
// We need a JSON container, but how do we create one?
|
||||
throw DecodingError.typeMismatch(
|
||||
KeyedDecodingContainer<NestedKey>.self,
|
||||
DecodingError.Context(
|
||||
codingPath: codingPath,
|
||||
debugDescription: """
|
||||
not implemented: building a nested JSON container for the column '\(column)'
|
||||
"""))
|
||||
}
|
||||
|
||||
// Scope?
|
||||
if let scopedRow = row.scopesTree[key.stringValue] {
|
||||
return KeyedDecodingContainer(KeyedContainer<NestedKey>(decoder: _RowDecoder(
|
||||
row: scopedRow,
|
||||
codingPath: codingPath + [key],
|
||||
columnDecodingStrategy: decoder.columnDecodingStrategy)))
|
||||
}
|
||||
|
||||
// Don't look for prefetched rows: those need a unkeyed container.
|
||||
|
||||
throw DecodingError.typeMismatch(
|
||||
KeyedDecodingContainer<NestedKey>.self,
|
||||
DecodingError.Context(
|
||||
codingPath: codingPath,
|
||||
debugDescription: "No keyed container found for key '\(key)'"))
|
||||
}
|
||||
|
||||
func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer {
|
||||
throw DecodingError.typeMismatch(
|
||||
UnkeyedDecodingContainer.self,
|
||||
DecodingError.Context(codingPath: codingPath, debugDescription: "unkeyed decoding is not supported"))
|
||||
}
|
||||
|
||||
func superDecoder() throws -> Decoder {
|
||||
decoder
|
||||
}
|
||||
|
||||
func superDecoder(forKey key: Key) throws -> Decoder {
|
||||
decoder
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
|
||||
private func decode<T>(
|
||||
_ type: T.Type,
|
||||
fromRow row: Row,
|
||||
codingPath: [CodingKey])
|
||||
throws -> T
|
||||
where T: Decodable
|
||||
{
|
||||
if let type = T.self as? any FetchableRecord.Type {
|
||||
// Prefer FetchableRecord decoding over Decodable.
|
||||
return try type.init(row: row) as! T
|
||||
} else {
|
||||
let decoder = _RowDecoder(row: row, codingPath: codingPath, columnDecodingStrategy: .useDefaultKeys)
|
||||
return try T(from: decoder)
|
||||
}
|
||||
}
|
||||
|
||||
private func decode<T>(
|
||||
_ type: T.Type,
|
||||
fromRow row: Row,
|
||||
columnAtIndex index: Int,
|
||||
key: Key)
|
||||
throws -> T
|
||||
where T: Decodable
|
||||
{
|
||||
do {
|
||||
// This decoding will fail for types that decode from keyed
|
||||
// or unkeyed containers, because we're decoding a single
|
||||
// value here (string, int, double, data, null). If such an
|
||||
// error happens, we'll switch to JSON decoding.
|
||||
let columnDecoder = ColumnDecoder<R>(
|
||||
row: row,
|
||||
columnIndex: index,
|
||||
codingPath: codingPath + [key])
|
||||
return try T(from: columnDecoder)
|
||||
} catch is JSONRequiredError {
|
||||
// Decode from JSON
|
||||
return try row.withUnsafeData(atIndex: index) { data in
|
||||
guard let data else {
|
||||
throw DecodingError.valueNotFound(Data.self, DecodingError.Context(
|
||||
codingPath: codingPath + [key],
|
||||
debugDescription: "Missing Data"))
|
||||
}
|
||||
return try R
|
||||
.databaseJSONDecoder(for: key.stringValue)
|
||||
.decode(type.self, from: data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct SingleValueRowDecoder<R: FetchableRecord>: SingleValueDecodingContainer {
|
||||
var columnDecoder: ColumnDecoder<R>
|
||||
var columnDecodingStrategy: DatabaseColumnDecodingStrategy
|
||||
let codingPath: [any CodingKey] = []
|
||||
|
||||
func decodeNil() -> Bool { columnDecoder.decodeNil() }
|
||||
func decode(_ type: Bool.Type) throws -> Bool { try columnDecoder.decode(type) }
|
||||
func decode(_ type: String.Type) throws -> String { try columnDecoder.decode(type) }
|
||||
func decode(_ type: Double.Type) throws -> Double { try columnDecoder.decode(type) }
|
||||
func decode(_ type: Float.Type) throws -> Float { try columnDecoder.decode(type) }
|
||||
func decode(_ type: Int.Type) throws -> Int { try columnDecoder.decode(type) }
|
||||
func decode(_ type: Int8.Type) throws -> Int8 { try columnDecoder.decode(type) }
|
||||
func decode(_ type: Int16.Type) throws -> Int16 { try columnDecoder.decode(type) }
|
||||
func decode(_ type: Int32.Type) throws -> Int32 { try columnDecoder.decode(type) }
|
||||
func decode(_ type: Int64.Type) throws -> Int64 { try columnDecoder.decode(type) }
|
||||
#if compiler(>=6)
|
||||
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
|
||||
func decode(_ type: Int128.Type) throws -> Int128 { try columnDecoder.decode(type) }
|
||||
#endif
|
||||
func decode(_ type: UInt.Type) throws -> UInt { try columnDecoder.decode(type) }
|
||||
func decode(_ type: UInt8.Type) throws -> UInt8 { try columnDecoder.decode(type) }
|
||||
func decode(_ type: UInt16.Type) throws -> UInt16 { try columnDecoder.decode(type) }
|
||||
func decode(_ type: UInt32.Type) throws -> UInt32 { try columnDecoder.decode(type) }
|
||||
func decode(_ type: UInt64.Type) throws -> UInt64 { try columnDecoder.decode(type) }
|
||||
#if compiler(>=6)
|
||||
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
|
||||
func decode(_ type: UInt128.Type) throws -> UInt128 { try columnDecoder.decode(type) }
|
||||
#endif
|
||||
|
||||
func decode<T>(_ type: T.Type) throws -> T where T: Decodable {
|
||||
if let type = T.self as? any FetchableRecord.Type {
|
||||
// Prefer FetchableRecord decoding over Decodable.
|
||||
return try type.init(row: columnDecoder.row) as! T
|
||||
} else {
|
||||
let decoder = _RowDecoder<R>(
|
||||
row: columnDecoder.row,
|
||||
codingPath: [],
|
||||
columnDecodingStrategy: columnDecodingStrategy
|
||||
)
|
||||
return try T(from: decoder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - PrefetchedRowsDecoder
|
||||
|
||||
private struct PrefetchedRowsDecoder<R: FetchableRecord>: Decoder {
|
||||
var rows: [Row]
|
||||
var codingPath: [CodingKey]
|
||||
var currentIndex: Int
|
||||
var userInfo: [CodingUserInfoKey: Any] { R.databaseDecodingUserInfo }
|
||||
|
||||
init(rows: [Row], codingPath: [CodingKey]) {
|
||||
self.rows = rows
|
||||
self.codingPath = codingPath
|
||||
self.currentIndex = 0
|
||||
}
|
||||
|
||||
func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key: CodingKey {
|
||||
fatalError("keyed decoding from prefetched rows is not supported")
|
||||
}
|
||||
|
||||
func unkeyedContainer() throws -> UnkeyedDecodingContainer { self }
|
||||
|
||||
func singleValueContainer() throws -> SingleValueDecodingContainer {
|
||||
fatalError("single value decoding from prefetched rows is not supported")
|
||||
}
|
||||
}
|
||||
|
||||
extension PrefetchedRowsDecoder: UnkeyedDecodingContainer {
|
||||
var count: Int? { rows.count }
|
||||
|
||||
var isAtEnd: Bool { currentIndex >= rows.count }
|
||||
|
||||
mutating func decodeNil() throws -> Bool { false }
|
||||
|
||||
mutating func decode<T>(_ type: T.Type) throws -> T where T: Decodable {
|
||||
defer { currentIndex += 1 }
|
||||
|
||||
let columnDecodingStrategy: DatabaseColumnDecodingStrategy
|
||||
if let type = T.self as? any FetchableRecord.Type {
|
||||
columnDecodingStrategy = type.databaseColumnDecodingStrategy
|
||||
} else {
|
||||
columnDecodingStrategy = .useDefaultKeys
|
||||
}
|
||||
|
||||
let decoder = _RowDecoder<R>(
|
||||
row: rows[currentIndex],
|
||||
codingPath: codingPath,
|
||||
columnDecodingStrategy: columnDecodingStrategy)
|
||||
return try T(from: decoder)
|
||||
}
|
||||
|
||||
mutating func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type)
|
||||
throws -> KeyedDecodingContainer<NestedKey>
|
||||
where NestedKey: CodingKey
|
||||
{
|
||||
fatalError("not implemented")
|
||||
}
|
||||
|
||||
mutating func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer {
|
||||
fatalError("not implemented")
|
||||
}
|
||||
|
||||
mutating func superDecoder() throws -> Decoder {
|
||||
fatalError("not implemented")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ColumnDecoder
|
||||
|
||||
/// The decoder that decodes from a database column
|
||||
private struct ColumnDecoder<R: FetchableRecord>: Decoder {
|
||||
var row: Row
|
||||
var columnIndex: Int
|
||||
var codingPath: [CodingKey]
|
||||
var userInfo: [CodingUserInfoKey: Any] { R.databaseDecodingUserInfo }
|
||||
|
||||
func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> {
|
||||
// We need to switch to JSON decoding
|
||||
throw JSONRequiredError()
|
||||
}
|
||||
|
||||
func unkeyedContainer() throws -> UnkeyedDecodingContainer {
|
||||
// We need to switch to JSON decoding
|
||||
throw JSONRequiredError()
|
||||
}
|
||||
|
||||
func singleValueContainer() throws -> SingleValueDecodingContainer { self }
|
||||
}
|
||||
|
||||
extension ColumnDecoder: SingleValueDecodingContainer {
|
||||
func decodeNil() -> Bool {
|
||||
row.hasNull(atIndex: columnIndex)
|
||||
}
|
||||
|
||||
func decode(_ type: Bool.Type ) throws -> Bool { try row.decode(atIndex: columnIndex) }
|
||||
func decode(_ type: Int.Type ) throws -> Int { try row.decode(atIndex: columnIndex) }
|
||||
func decode(_ type: Int8.Type ) throws -> Int8 { try row.decode(atIndex: columnIndex) }
|
||||
func decode(_ type: Int16.Type ) throws -> Int16 { try row.decode(atIndex: columnIndex) }
|
||||
func decode(_ type: Int32.Type ) throws -> Int32 { try row.decode(atIndex: columnIndex) }
|
||||
func decode(_ type: Int64.Type ) throws -> Int64 { try row.decode(atIndex: columnIndex) }
|
||||
func decode(_ type: UInt.Type ) throws -> UInt { try row.decode(atIndex: columnIndex) }
|
||||
func decode(_ type: UInt8.Type ) throws -> UInt8 { try row.decode(atIndex: columnIndex) }
|
||||
func decode(_ type: UInt16.Type) throws -> UInt16 { try row.decode(atIndex: columnIndex) }
|
||||
func decode(_ type: UInt32.Type) throws -> UInt32 { try row.decode(atIndex: columnIndex) }
|
||||
func decode(_ type: UInt64.Type) throws -> UInt64 { try row.decode(atIndex: columnIndex) }
|
||||
func decode(_ type: Float.Type ) throws -> Float { try row.decode(atIndex: columnIndex) }
|
||||
func decode(_ type: Double.Type) throws -> Double { try row.decode(atIndex: columnIndex) }
|
||||
func decode(_ type: String.Type) throws -> String { try row.decode(atIndex: columnIndex) }
|
||||
|
||||
func decode<T>(_ type: T.Type) throws -> T where T: Decodable {
|
||||
// TODO: not tested
|
||||
if type == Data.self {
|
||||
return try R.databaseDataDecodingStrategy.decode(fromRow: row, atUncheckedIndex: columnIndex) as! T
|
||||
} else if type == Date.self {
|
||||
return try R.databaseDateDecodingStrategy.decode(fromRow: row, atUncheckedIndex: columnIndex) as! T
|
||||
} else if let type = T.self as? any (DatabaseValueConvertible & StatementColumnConvertible).Type {
|
||||
return try type.fastDecode(fromRow: row, atUncheckedIndex: columnIndex) as! T
|
||||
} else if let type = T.self as? any DatabaseValueConvertible.Type {
|
||||
return try type.decode(fromRow: row, atUncheckedIndex: columnIndex) as! T
|
||||
} else {
|
||||
return try T(from: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let iso8601Formatter: ISO8601DateFormatter = {
|
||||
let formatter = ISO8601DateFormatter()
|
||||
formatter.formatOptions = .withInternetDateTime
|
||||
return formatter
|
||||
}()
|
||||
|
||||
extension DatabaseDataDecodingStrategy {
|
||||
fileprivate func decodeIfPresent(fromRow row: Row, atUncheckedIndex index: Int) throws -> Data? {
|
||||
if let sqliteStatement = row.sqliteStatement {
|
||||
return try decodeIfPresent(
|
||||
fromStatement: sqliteStatement,
|
||||
atUncheckedIndex: CInt(index),
|
||||
context: RowDecodingContext(row: row, key: .columnIndex(index)))
|
||||
} else {
|
||||
return try decodeIfPresent(
|
||||
fromDatabaseValue: row[index],
|
||||
context: RowDecodingContext(row: row, key: .columnIndex(index)))
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func decode(fromRow row: Row, atUncheckedIndex index: Int) throws -> Data {
|
||||
if let sqliteStatement = row.sqliteStatement {
|
||||
let statementIndex = CInt(index)
|
||||
|
||||
if sqlite3_column_type(sqliteStatement, statementIndex) == SQLITE_NULL {
|
||||
throw RowDecodingError.valueMismatch(
|
||||
Data.self,
|
||||
sqliteStatement: sqliteStatement,
|
||||
index: statementIndex,
|
||||
context: RowDecodingContext(row: row, key: .columnIndex(index)))
|
||||
}
|
||||
|
||||
return try decode(
|
||||
fromStatement: sqliteStatement,
|
||||
atUncheckedIndex: statementIndex,
|
||||
context: RowDecodingContext(row: row, key: .columnIndex(index)))
|
||||
} else {
|
||||
return try decode(
|
||||
fromDatabaseValue: row[index],
|
||||
context: RowDecodingContext(row: row, key: .columnIndex(index)))
|
||||
}
|
||||
}
|
||||
|
||||
/// - precondition: value is not NULL
|
||||
fileprivate func decode(
|
||||
fromStatement sqliteStatement: SQLiteStatement,
|
||||
atUncheckedIndex index: CInt,
|
||||
context: @autoclosure () -> RowDecodingContext)
|
||||
throws -> Data
|
||||
{
|
||||
assert(sqlite3_column_type(sqliteStatement, index) != SQLITE_NULL, "unexpected NULL value")
|
||||
switch self {
|
||||
case .deferredToData:
|
||||
return Data(sqliteStatement: sqliteStatement, index: index)
|
||||
case .custom(let format):
|
||||
let dbValue = DatabaseValue(sqliteStatement: sqliteStatement, index: index)
|
||||
guard let data = format(dbValue) else {
|
||||
throw RowDecodingError.valueMismatch(
|
||||
Data.self,
|
||||
context: context(),
|
||||
databaseValue: DatabaseValue(sqliteStatement: sqliteStatement, index: index))
|
||||
}
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func decodeIfPresent(
|
||||
fromStatement sqliteStatement: SQLiteStatement,
|
||||
atUncheckedIndex index: CInt,
|
||||
context: @autoclosure () -> RowDecodingContext)
|
||||
throws -> Data?
|
||||
{
|
||||
if sqlite3_column_type(sqliteStatement, index) == SQLITE_NULL {
|
||||
return nil
|
||||
}
|
||||
return try decode(fromStatement: sqliteStatement, atUncheckedIndex: index, context: context())
|
||||
}
|
||||
|
||||
fileprivate func decode(
|
||||
fromDatabaseValue dbValue: DatabaseValue,
|
||||
context: @autoclosure () -> RowDecodingContext)
|
||||
throws -> Data
|
||||
{
|
||||
if let data = dataFromDatabaseValue(dbValue) {
|
||||
return data
|
||||
} else {
|
||||
throw RowDecodingError.valueMismatch(Data.self, context: context(), databaseValue: dbValue)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func decodeIfPresent(
|
||||
fromDatabaseValue dbValue: DatabaseValue,
|
||||
context: @autoclosure () -> RowDecodingContext)
|
||||
throws -> Data?
|
||||
{
|
||||
if dbValue.isNull {
|
||||
return nil
|
||||
} else if let data = dataFromDatabaseValue(dbValue) {
|
||||
return data
|
||||
} else {
|
||||
throw RowDecodingError.valueMismatch(Data.self, context: context(), databaseValue: dbValue)
|
||||
}
|
||||
}
|
||||
|
||||
// Returns nil if decoding fails
|
||||
private func dataFromDatabaseValue(_ dbValue: DatabaseValue) -> Data? {
|
||||
switch self {
|
||||
case .deferredToData:
|
||||
return Data.fromDatabaseValue(dbValue)
|
||||
case .custom(let format):
|
||||
return format(dbValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DatabaseDateDecodingStrategy {
|
||||
fileprivate func decodeIfPresent(fromRow row: Row, atUncheckedIndex index: Int) throws -> Date? {
|
||||
if let sqliteStatement = row.sqliteStatement {
|
||||
return try decodeIfPresent(
|
||||
fromStatement: sqliteStatement,
|
||||
atUncheckedIndex: CInt(index),
|
||||
context: RowDecodingContext(row: row, key: .columnIndex(index)))
|
||||
} else {
|
||||
return try decodeIfPresent(
|
||||
fromDatabaseValue: row[index],
|
||||
context: RowDecodingContext(row: row, key: .columnIndex(index)))
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func decode(fromRow row: Row, atUncheckedIndex index: Int) throws -> Date {
|
||||
if let sqliteStatement = row.sqliteStatement {
|
||||
let statementIndex = CInt(index)
|
||||
|
||||
if sqlite3_column_type(sqliteStatement, statementIndex) == SQLITE_NULL {
|
||||
throw RowDecodingError.valueMismatch(
|
||||
Date.self,
|
||||
sqliteStatement: sqliteStatement,
|
||||
index: statementIndex,
|
||||
context: RowDecodingContext(row: row, key: .columnIndex(index)))
|
||||
}
|
||||
|
||||
return try decode(
|
||||
fromStatement: sqliteStatement,
|
||||
atUncheckedIndex: CInt(index),
|
||||
context: RowDecodingContext(row: row, key: .columnIndex(index)))
|
||||
} else {
|
||||
return try decode(
|
||||
fromDatabaseValue: row[index],
|
||||
context: RowDecodingContext(row: row, key: .columnIndex(index)))
|
||||
}
|
||||
}
|
||||
|
||||
/// - precondition: value is not NULL
|
||||
fileprivate func decode(
|
||||
fromStatement sqliteStatement: SQLiteStatement,
|
||||
atUncheckedIndex index: CInt,
|
||||
context: @autoclosure () -> RowDecodingContext)
|
||||
throws -> Date
|
||||
{
|
||||
assert(sqlite3_column_type(sqliteStatement, index) != SQLITE_NULL, "unexpected NULL value")
|
||||
switch self {
|
||||
case .deferredToDate:
|
||||
guard let date = Date(sqliteStatement: sqliteStatement, index: index) else {
|
||||
throw RowDecodingError.valueMismatch(
|
||||
Date.self,
|
||||
context: context(),
|
||||
databaseValue: DatabaseValue(sqliteStatement: sqliteStatement, index: index))
|
||||
}
|
||||
return date
|
||||
case .timeIntervalSinceReferenceDate:
|
||||
let timeInterval = TimeInterval(sqliteStatement: sqliteStatement, index: index)
|
||||
return Date(timeIntervalSinceReferenceDate: timeInterval)
|
||||
case .timeIntervalSince1970:
|
||||
let timeInterval = TimeInterval(sqliteStatement: sqliteStatement, index: index)
|
||||
return Date(timeIntervalSince1970: timeInterval)
|
||||
case .millisecondsSince1970:
|
||||
let timeInterval = TimeInterval(sqliteStatement: sqliteStatement, index: index)
|
||||
return Date(timeIntervalSince1970: timeInterval / 1000.0)
|
||||
case .iso8601:
|
||||
let string = String(sqliteStatement: sqliteStatement, index: index)
|
||||
guard let date = iso8601Formatter.date(from: string) else {
|
||||
throw RowDecodingError.valueMismatch(
|
||||
Date.self,
|
||||
context: context(),
|
||||
databaseValue: DatabaseValue(sqliteStatement: sqliteStatement, index: index))
|
||||
}
|
||||
return date
|
||||
case .formatted(let formatter):
|
||||
let string = String(sqliteStatement: sqliteStatement, index: index)
|
||||
guard let date = formatter.date(from: string) else {
|
||||
throw RowDecodingError.valueMismatch(
|
||||
Date.self,
|
||||
context: context(),
|
||||
databaseValue: DatabaseValue(sqliteStatement: sqliteStatement, index: index))
|
||||
}
|
||||
return date
|
||||
case .custom(let format):
|
||||
let dbValue = DatabaseValue(sqliteStatement: sqliteStatement, index: index)
|
||||
guard let date = format(dbValue) else {
|
||||
throw RowDecodingError.valueMismatch(
|
||||
Date.self,
|
||||
context: context(),
|
||||
databaseValue: DatabaseValue(sqliteStatement: sqliteStatement, index: index))
|
||||
}
|
||||
return date
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func decodeIfPresent(
|
||||
fromStatement sqliteStatement: SQLiteStatement,
|
||||
atUncheckedIndex index: CInt,
|
||||
context: @autoclosure () -> RowDecodingContext)
|
||||
throws -> Date?
|
||||
{
|
||||
if sqlite3_column_type(sqliteStatement, index) == SQLITE_NULL {
|
||||
return nil
|
||||
}
|
||||
return try decode(fromStatement: sqliteStatement, atUncheckedIndex: index, context: context())
|
||||
}
|
||||
|
||||
fileprivate func decode(
|
||||
fromDatabaseValue dbValue: DatabaseValue,
|
||||
context: @autoclosure () -> RowDecodingContext)
|
||||
throws -> Date
|
||||
{
|
||||
if let date = dateFromDatabaseValue(dbValue) {
|
||||
return date
|
||||
} else {
|
||||
throw RowDecodingError.valueMismatch(Date.self, context: context(), databaseValue: dbValue)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func decodeIfPresent(
|
||||
fromDatabaseValue dbValue: DatabaseValue,
|
||||
context: @autoclosure () -> RowDecodingContext)
|
||||
throws -> Date?
|
||||
{
|
||||
if dbValue.isNull {
|
||||
return nil
|
||||
} else if let date = dateFromDatabaseValue(dbValue) {
|
||||
return date
|
||||
} else {
|
||||
throw RowDecodingError.valueMismatch(Date.self, context: context(), databaseValue: dbValue)
|
||||
}
|
||||
}
|
||||
|
||||
// Returns nil if decoding fails
|
||||
private func dateFromDatabaseValue(_ dbValue: DatabaseValue) -> Date? {
|
||||
switch self {
|
||||
case .deferredToDate:
|
||||
return Date.fromDatabaseValue(dbValue)
|
||||
case .timeIntervalSinceReferenceDate:
|
||||
return TimeInterval
|
||||
.fromDatabaseValue(dbValue)
|
||||
.map { Date(timeIntervalSinceReferenceDate: $0) }
|
||||
case .timeIntervalSince1970:
|
||||
return TimeInterval
|
||||
.fromDatabaseValue(dbValue)
|
||||
.map { Date(timeIntervalSince1970: $0) }
|
||||
case .millisecondsSince1970:
|
||||
return TimeInterval
|
||||
.fromDatabaseValue(dbValue)
|
||||
.map { Date(timeIntervalSince1970: $0 / 1000.0) }
|
||||
case .iso8601:
|
||||
return String
|
||||
.fromDatabaseValue(dbValue)
|
||||
.flatMap { iso8601Formatter.date(from: $0) }
|
||||
case .formatted(let formatter):
|
||||
return String
|
||||
.fromDatabaseValue(dbValue)
|
||||
.flatMap { formatter.date(from: $0) }
|
||||
case .custom(let format):
|
||||
return format(dbValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,537 @@
|
||||
extension FetchableRecord where Self: TableRecord {
|
||||
|
||||
// MARK: Fetching All
|
||||
|
||||
/// Returns a cursor over all records fetched from the database.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: FetchableRecord, TableRecord { }
|
||||
///
|
||||
/// try dbQueue.read { db in
|
||||
/// // SELECT * FROM player
|
||||
/// let players = try Player.fetchCursor(db)
|
||||
/// while let player = try players.next() {
|
||||
/// print(player.name)
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The order in which the records are returned is undefined
|
||||
/// ([ref](https://www.sqlite.org/lang_select.html#the_order_by_clause)).
|
||||
///
|
||||
/// The returned cursor is valid only during the remaining execution of the
|
||||
/// database access. Do not store or return the cursor for later use.
|
||||
///
|
||||
/// If the database is modified during the cursor iteration, the remaining
|
||||
/// elements are undefined.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - returns: A ``RecordCursor`` over fetched records.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public static func fetchCursor(_ db: Database) throws -> RecordCursor<Self> {
|
||||
try all().fetchCursor(db)
|
||||
}
|
||||
|
||||
/// Returns an array of all records fetched from the database.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: FetchableRecord, TableRecord { }
|
||||
///
|
||||
/// try dbQueue.read { db in
|
||||
/// // SELECT * FROM player
|
||||
/// let players = try Player.fetchAll(db)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The order in which the records are returned is undefined
|
||||
/// ([ref](https://www.sqlite.org/lang_select.html#the_order_by_clause)).
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public static func fetchAll(_ db: Database) throws -> [Self] {
|
||||
try all().fetchAll(db)
|
||||
}
|
||||
|
||||
/// Returns a single record fetched from the database.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: FetchableRecord, TableRecord { }
|
||||
///
|
||||
/// try dbQueue.read { db in
|
||||
/// // SELECT * FROM player LIMIT 1
|
||||
/// let player = try Player.fetchOne(db)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public static func fetchOne(_ db: Database) throws -> Self? {
|
||||
try all().fetchOne(db)
|
||||
}
|
||||
}
|
||||
|
||||
extension FetchableRecord where Self: TableRecord & Hashable {
|
||||
/// Returns a set of all records fetched from the database.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: FetchableRecord, TableRecord, Hashable { }
|
||||
///
|
||||
/// try dbQueue.read { db in
|
||||
/// // SELECT * FROM player
|
||||
/// let players = try Player.fetchSet(db)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public static func fetchSet(_ db: Database) throws -> Set<Self> {
|
||||
try all().fetchSet(db)
|
||||
}
|
||||
}
|
||||
|
||||
extension FetchableRecord where Self: TableRecord {
|
||||
|
||||
// MARK: Fetching by Single-Column Primary Key
|
||||
|
||||
/// Returns a cursor over records identified by their primary keys.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.read { db in
|
||||
/// let players = try Player.fetchCursor(db, keys: [1, 2, 3])
|
||||
/// while let player = try players.next() {
|
||||
/// print(player.name)
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The order in which the records are returned is undefined
|
||||
/// ([ref](https://www.sqlite.org/lang_select.html#the_order_by_clause)).
|
||||
///
|
||||
/// The returned cursor is valid only during the remaining execution of the
|
||||
/// database access. Do not store or return the cursor for later use.
|
||||
///
|
||||
/// If the database is modified during the cursor iteration, the remaining
|
||||
/// elements are undefined.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - db: A database connection.
|
||||
/// - keys: A sequence of primary keys.
|
||||
/// - returns: A ``RecordCursor`` over fetched records.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public static func fetchCursor<Keys>(_ db: Database, keys: Keys)
|
||||
throws -> RecordCursor<Self>
|
||||
where Keys: Sequence, Keys.Element: DatabaseValueConvertible
|
||||
{
|
||||
try filter(keys: keys).fetchCursor(db)
|
||||
}
|
||||
|
||||
/// Returns an array of records identified by their primary keys.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.read { db in
|
||||
/// let players = try Player.fetchAll(db, keys: [1, 2, 3])
|
||||
/// let countries = try Country.fetchAll(db, keys: ["FR", "US"])
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The order in which the records are returned is undefined
|
||||
/// ([ref](https://www.sqlite.org/lang_select.html#the_order_by_clause)).
|
||||
///
|
||||
/// - parameters:
|
||||
/// - db: A database connection.
|
||||
/// - keys: A sequence of primary keys.
|
||||
/// - returns: An array of records.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public static func fetchAll<Keys>(_ db: Database, keys: Keys)
|
||||
throws -> [Self]
|
||||
where Keys: Sequence, Keys.Element: DatabaseValueConvertible
|
||||
{
|
||||
let keys = Array(keys)
|
||||
if keys.isEmpty {
|
||||
// Avoid hitting the database
|
||||
return []
|
||||
}
|
||||
return try filter(keys: keys).fetchAll(db)
|
||||
}
|
||||
|
||||
/// Returns the record identified by a primary key.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.read { db in
|
||||
/// let player = try Player.fetchOne(db, key: 123)
|
||||
/// let country = try Country.fetchOne(db, key: "FR")
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameters:
|
||||
/// - db: A database connection.
|
||||
/// - key: A primary key value.
|
||||
/// - returns: An optional record.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public static func fetchOne(_ db: Database, key: some DatabaseValueConvertible) throws -> Self? {
|
||||
if key.databaseValue.isNull {
|
||||
// Don't hit the database
|
||||
return nil
|
||||
}
|
||||
return try filter(key: key).fetchOne(db)
|
||||
}
|
||||
|
||||
/// Returns the record identified by a primary key, or throws an error if
|
||||
/// the record does not exist.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.read { db in
|
||||
/// let player = try Player.find(db, key: 123)
|
||||
/// let country = try Country.find(db, key: "FR")
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameters:
|
||||
/// - db: A database connection.
|
||||
/// - key: A primary key value.
|
||||
/// - returns: A record.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or a
|
||||
/// ``RecordError/recordNotFound(databaseTableName:key:)`` if the record
|
||||
/// does not exist in the database.
|
||||
public static func find(_ db: Database, key: some DatabaseValueConvertible) throws -> Self {
|
||||
guard let record = try fetchOne(db, key: key) else {
|
||||
throw recordNotFound(db, key: key)
|
||||
}
|
||||
return record
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
extension FetchableRecord where Self: TableRecord & Identifiable, ID: DatabaseValueConvertible {
|
||||
|
||||
// MARK: Fetching by Single-Column Primary Key
|
||||
|
||||
/// Returns a cursor over records identified by their primary keys.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.read { db in
|
||||
/// let players = try Player.fetchCursor(db, ids: [1, 2, 3])
|
||||
/// while let player = try players.next() {
|
||||
/// print(player.name)
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The order in which the records are returned is undefined
|
||||
/// ([ref](https://www.sqlite.org/lang_select.html#the_order_by_clause)).
|
||||
///
|
||||
/// The returned cursor is valid only during the remaining execution of the
|
||||
/// database access. Do not store or return the cursor for later use.
|
||||
///
|
||||
/// If the database is modified during the cursor iteration, the remaining
|
||||
/// elements are undefined.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - db: A database connection.
|
||||
/// - ids: A collection of primary keys.
|
||||
/// - returns: A ``RecordCursor`` over fetched records.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public static func fetchCursor<IDS>(_ db: Database, ids: IDS)
|
||||
throws -> RecordCursor<Self>
|
||||
where IDS: Collection, IDS.Element == ID
|
||||
{
|
||||
try filter(ids: ids).fetchCursor(db)
|
||||
}
|
||||
|
||||
/// Returns an array of records identified by their primary keys.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.read { db in
|
||||
/// let players = try Player.fetchAll(db, ids: [1, 2, 3])
|
||||
/// let players = try Country.fetchAll(db, ids: ["FR", "US"])
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The order in which the records are returned is undefined
|
||||
/// ([ref](https://www.sqlite.org/lang_select.html#the_order_by_clause)).
|
||||
///
|
||||
/// - parameters:
|
||||
/// - db: A database connection.
|
||||
/// - ids: A collection of primary keys.
|
||||
/// - returns: An array of records.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public static func fetchAll<IDS>(_ db: Database, ids: IDS) throws -> [Self]
|
||||
where IDS: Collection, IDS.Element == ID
|
||||
{
|
||||
if ids.isEmpty {
|
||||
// Avoid hitting the database
|
||||
return []
|
||||
}
|
||||
return try filter(ids: ids).fetchAll(db)
|
||||
}
|
||||
|
||||
/// Returns the record identified by a primary key.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.read { db in
|
||||
/// let player = try Player.fetchOne(db, id: 123)
|
||||
/// let country = try Country.fetchOne(db, id: "FR")
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameters:
|
||||
/// - db: A database connection.
|
||||
/// - id: A primary key value.
|
||||
/// - returns: An optional record.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public static func fetchOne(_ db: Database, id: ID) throws -> Self? {
|
||||
try filter(id: id).fetchOne(db)
|
||||
}
|
||||
|
||||
/// Returns the record identified by a primary key, or throws an error if
|
||||
/// the record does not exist.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.read { db in
|
||||
/// let player = try Player.find(db, id: 123)
|
||||
/// let country = try Country.find(db, id: "FR")
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameters:
|
||||
/// - db: A database connection.
|
||||
/// - id: A primary key value.
|
||||
/// - returns: A record.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or a
|
||||
/// ``RecordError/recordNotFound(databaseTableName:key:)`` if the record
|
||||
/// does not exist in the database.
|
||||
public static func find(_ db: Database, id: ID) throws -> Self {
|
||||
try find(db, key: id)
|
||||
}
|
||||
}
|
||||
|
||||
extension FetchableRecord where Self: TableRecord & Hashable {
|
||||
/// Returns a set of records identified by their primary keys.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.read { db in
|
||||
/// let players = try Player.fetchSet(db, keys: [1, 2, 3])
|
||||
/// let countries = try Country.fetchSet(db, keys: ["FR", "US"])
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameters:
|
||||
/// - db: A database connection.
|
||||
/// - keys: A sequence of primary keys.
|
||||
/// - returns: A set of records.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public static func fetchSet<Keys>(_ db: Database, keys: Keys)
|
||||
throws -> Set<Self>
|
||||
where Keys: Sequence, Keys.Element: DatabaseValueConvertible
|
||||
{
|
||||
let keys = Array(keys)
|
||||
if keys.isEmpty {
|
||||
// Avoid hitting the database
|
||||
return []
|
||||
}
|
||||
return try filter(keys: keys).fetchSet(db)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
extension FetchableRecord where Self: TableRecord & Hashable & Identifiable, ID: DatabaseValueConvertible {
|
||||
/// Returns a set of records identified by their primary keys.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.read { db in
|
||||
/// let players = try Player.fetchSet(db, ids: [1, 2, 3])
|
||||
/// let countries = try Country.fetchSet(db, ids: ["FR", "US"])
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameters:
|
||||
/// - db: A database connection.
|
||||
/// - ids: A collection of primary keys.
|
||||
/// - returns: A set of records.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public static func fetchSet<IDS>(_ db: Database, ids: IDS) throws -> Set<Self>
|
||||
where IDS: Collection, IDS.Element == ID
|
||||
{
|
||||
if ids.isEmpty {
|
||||
// Avoid hitting the database
|
||||
return []
|
||||
}
|
||||
return try filter(ids: ids).fetchSet(db)
|
||||
}
|
||||
}
|
||||
|
||||
extension FetchableRecord where Self: TableRecord {
|
||||
|
||||
// MARK: Fetching by Key
|
||||
|
||||
/// Returns a cursor over records identified by the provided unique keys
|
||||
/// (primary key or any key with a unique index on it).
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.read { db in
|
||||
/// let players = try Player.fetchCursor(db, keys: [
|
||||
/// ["email": "a@example.com"],
|
||||
/// ["email": "b@example.com"]])
|
||||
/// while let player = try players.next() {
|
||||
/// print(player.name)
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The order in which the records are returned is undefined
|
||||
/// ([ref](https://www.sqlite.org/lang_select.html#the_order_by_clause)).
|
||||
///
|
||||
/// The returned cursor is valid only during the remaining execution of the
|
||||
/// database access. Do not store or return the cursor for later use.
|
||||
///
|
||||
/// If the database is modified during the cursor iteration, the remaining
|
||||
/// elements are undefined.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - db: A database connection.
|
||||
/// - keys: An array of key dictionaries.
|
||||
/// - returns: A ``RecordCursor`` over fetched records.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public static func fetchCursor(_ db: Database, keys: [[String: (any DatabaseValueConvertible)?]])
|
||||
throws -> RecordCursor<Self>
|
||||
{
|
||||
try filter(keys: keys).fetchCursor(db)
|
||||
}
|
||||
|
||||
/// Returns an array of records identified by the provided unique keys
|
||||
/// (primary key or any key with a unique index on it).
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.read { db in
|
||||
/// let players = try Player.fetchAll(db, keys: [
|
||||
/// ["email": "a@example.com"],
|
||||
/// ["email": "b@example.com"]])
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The order in which the records are returned is undefined
|
||||
/// ([ref](https://www.sqlite.org/lang_select.html#the_order_by_clause)).
|
||||
///
|
||||
/// - parameters:
|
||||
/// - db: A database connection.
|
||||
/// - keys: An array of key dictionaries.
|
||||
/// - returns: An array of records.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public static func fetchAll(_ db: Database, keys: [[String: (any DatabaseValueConvertible)?]]) throws -> [Self] {
|
||||
if keys.isEmpty {
|
||||
// Avoid hitting the database
|
||||
return []
|
||||
}
|
||||
return try filter(keys: keys).fetchAll(db)
|
||||
}
|
||||
|
||||
/// Returns the record identified by a unique key (the primary key or
|
||||
/// any key with a unique index on it).
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.read { db in
|
||||
/// let player = try Player.fetchOne(db, key: ["name": "Arthur"])
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameters:
|
||||
/// - db: A database connection.
|
||||
/// - key: A key dictionary.
|
||||
/// - returns: An optional record.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public static func fetchOne(_ db: Database, key: [String: (any DatabaseValueConvertible)?]?) throws -> Self? {
|
||||
guard let key else {
|
||||
// Avoid hitting the database
|
||||
return nil
|
||||
}
|
||||
return try filter(key: key).fetchOne(db)
|
||||
}
|
||||
|
||||
/// Returns the record identified by a unique key (the primary key or
|
||||
/// any key with a unique index on it), or throws an error if the record
|
||||
/// does not exist.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.read { db in
|
||||
/// let player = try Player.find(db, key: ["name": "Arthur"])
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameters:
|
||||
/// - db: A database connection.
|
||||
/// - key: A key dictionary.
|
||||
/// - returns: A record.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or a
|
||||
/// ``RecordError/recordNotFound(databaseTableName:key:)`` if the record
|
||||
/// does not exist in the database.
|
||||
public static func find(_ db: Database, key: [String: (any DatabaseValueConvertible)?]) throws -> Self {
|
||||
guard let record = try filter(key: key).fetchOne(db) else {
|
||||
throw recordNotFound(key: key)
|
||||
}
|
||||
return record
|
||||
}
|
||||
}
|
||||
|
||||
extension FetchableRecord where Self: TableRecord & Hashable {
|
||||
/// Returns a set of records identified by the provided unique keys
|
||||
/// (primary key or any key with a unique index on it).
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.read { db in
|
||||
/// let players = try Player.fetchSet(db, keys: [
|
||||
/// ["email": "a@example.com"],
|
||||
/// ["email": "b@example.com"]])
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameters:
|
||||
/// - db: A database connection.
|
||||
/// - keys: An array of key dictionaries.
|
||||
/// - returns: A set of records.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public static func fetchSet(_ db: Database, keys: [[String: (any DatabaseValueConvertible)?]]) throws -> Set<Self> {
|
||||
if keys.isEmpty {
|
||||
// Avoid hitting the database
|
||||
return []
|
||||
}
|
||||
return try filter(keys: keys).fetchSet(db)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,365 @@
|
||||
/// DAO takes care of MutablePersistableRecord CRUD
|
||||
final class DAO<Record: MutablePersistableRecord> {
|
||||
/// The database
|
||||
let db: Database
|
||||
|
||||
/// DAO keeps a copy the record's persistenceContainer, so that this
|
||||
/// dictionary is built once whatever the database operation. It is
|
||||
/// guaranteed to have at least one (key, value) pair.
|
||||
let persistenceContainer: PersistenceContainer
|
||||
|
||||
/// The table name
|
||||
let databaseTableName: String
|
||||
|
||||
/// The table primary key info
|
||||
let primaryKey: PrimaryKeyInfo
|
||||
|
||||
init(_ db: Database, _ record: Record) throws {
|
||||
self.db = db
|
||||
databaseTableName = type(of: record).databaseTableName
|
||||
primaryKey = try db.primaryKey(databaseTableName)
|
||||
persistenceContainer = try PersistenceContainer(db, record)
|
||||
GRDBPrecondition(!persistenceContainer.isEmpty, "\(type(of: record)): invalid empty persistence container")
|
||||
}
|
||||
|
||||
func insertStatement(
|
||||
_ db: Database,
|
||||
onConflict: Database.ConflictResolution,
|
||||
returning selection: [any SQLSelectable])
|
||||
throws -> Statement
|
||||
{
|
||||
let query = InsertQuery(
|
||||
onConflict: onConflict,
|
||||
tableName: databaseTableName,
|
||||
insertedColumns: persistenceContainer.columns)
|
||||
|
||||
return try makeStatement(
|
||||
sql: query.sql,
|
||||
checkedArguments: StatementArguments(persistenceContainer.values),
|
||||
returning: selection)
|
||||
}
|
||||
|
||||
func upsertStatement(
|
||||
_ db: Database,
|
||||
onConflict conflictTargetColumns: [String],
|
||||
doUpdate assignments: ((_ excluded: TableAlias) -> [ColumnAssignment])?,
|
||||
updateCondition: ((_ existing: TableAlias, _ excluded: TableAlias) -> any SQLExpressible)? = nil,
|
||||
returning selection: [any SQLSelectable])
|
||||
throws -> Statement
|
||||
{
|
||||
// INSERT
|
||||
let insertedColumns = persistenceContainer.columns
|
||||
let columnsSQL = insertedColumns.map(\.quotedDatabaseIdentifier).joined(separator: ", ")
|
||||
let valuesSQL = databaseQuestionMarks(count: insertedColumns.count)
|
||||
var sql = """
|
||||
INSERT INTO \(databaseTableName.quotedDatabaseIdentifier) (\(columnsSQL)) \
|
||||
VALUES (\(valuesSQL))
|
||||
"""
|
||||
var arguments = StatementArguments(persistenceContainer.values)
|
||||
|
||||
// ON CONFLICT
|
||||
if conflictTargetColumns.isEmpty {
|
||||
sql += " ON CONFLICT"
|
||||
} else {
|
||||
let targetSQL = conflictTargetColumns
|
||||
.map { $0.quotedDatabaseIdentifier }
|
||||
.joined(separator: ", ")
|
||||
sql += " ON CONFLICT(\(targetSQL))"
|
||||
}
|
||||
|
||||
// DO UPDATE SET
|
||||
// We update explicit assignments from the `assignments` parameter.
|
||||
// Other columns are overwritten by inserted values. This makes sure
|
||||
// that no information stored in the record is lost, unless explicitly
|
||||
// requested by the user.
|
||||
sql += " DO UPDATE SET "
|
||||
let excluded = TableAlias(name: "excluded")
|
||||
var assignments = assignments?(excluded) ?? []
|
||||
let lowercaseExcludedColumns = Set(primaryKey.columns.map { $0.lowercased() })
|
||||
.union(conflictTargetColumns.map { $0.lowercased() })
|
||||
for column in persistenceContainer.columns {
|
||||
let lowercasedColumn = column.lowercased()
|
||||
if lowercaseExcludedColumns.contains(lowercasedColumn) {
|
||||
// excluded (primary key or conflict target)
|
||||
continue
|
||||
}
|
||||
if assignments.contains(where: { $0.columnName.lowercased() == lowercasedColumn }) {
|
||||
// already updated from the `assignments` argument
|
||||
continue
|
||||
}
|
||||
// overwrite
|
||||
assignments.append(Column(column).set(to: excluded[column]))
|
||||
}
|
||||
let context = SQLGenerationContext(db)
|
||||
let updateSQL = try assignments
|
||||
.compactMap { try $0.sql(context) }
|
||||
.joined(separator: ", ")
|
||||
if updateSQL.isEmpty {
|
||||
if !selection.isEmpty {
|
||||
// User has asked that no column was overwritten or updated.
|
||||
// In case of conflict, the upsert would do nothing, and return
|
||||
// nothing: <https://sqlite.org/forum/forumpost/1ead75e2c45de9a5>.
|
||||
//
|
||||
// But we have a RETURNING clause, so we WANT values to be
|
||||
// returned, and we MUST prevent the upsert statement from
|
||||
// return nothing. The RETURNING clause is how, for example, we
|
||||
// fetch the rowid of the upserted record, and feed record
|
||||
// callbacks such as `didInsert`. Not returning any value would
|
||||
// be a GRDB bug.
|
||||
//
|
||||
// So let's make SURE something is returned, and to do so, let's
|
||||
// update one column. The first column of the primary key should
|
||||
// be ok.
|
||||
let column = primaryKey.columns[0].quotedDatabaseIdentifier
|
||||
sql += "\(column) = \(column)"
|
||||
}
|
||||
} else {
|
||||
sql += updateSQL
|
||||
arguments += context.arguments
|
||||
}
|
||||
|
||||
// WHERE
|
||||
let existing = TableAlias(name: databaseTableName)
|
||||
if let condition = updateCondition?(existing, excluded) {
|
||||
let context = SQLGenerationContext(db)
|
||||
sql += try " WHERE " + condition.sqlExpression.sql(context)
|
||||
arguments += context.arguments
|
||||
}
|
||||
|
||||
return try makeStatement(
|
||||
sql: sql,
|
||||
checkedArguments: arguments,
|
||||
returning: selection)
|
||||
}
|
||||
|
||||
/// Returns nil if and only if primary key is nil
|
||||
func updateStatement(
|
||||
columns: Set<String>,
|
||||
onConflict: Database.ConflictResolution,
|
||||
returning selection: [any SQLSelectable])
|
||||
throws -> Statement?
|
||||
{
|
||||
// Fail early if primary key does not resolve to a database row.
|
||||
let primaryKeyColumns = primaryKey.columns
|
||||
let primaryKeyValues = primaryKeyColumns.map {
|
||||
persistenceContainer[caseInsensitive: $0]?.databaseValue ?? .null
|
||||
}
|
||||
if primaryKeyValues.allSatisfy({ $0.isNull }) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Don't update columns not present in columns
|
||||
// Don't update columns not present in the persistenceContainer
|
||||
// Don't update primary key columns
|
||||
let lowercaseUpdatedColumns = Set(columns.map { $0.lowercased() })
|
||||
.intersection(persistenceContainer.columns.map { $0.lowercased() })
|
||||
.subtracting(primaryKeyColumns.map { $0.lowercased() })
|
||||
|
||||
var updatedColumns: [String] = try db
|
||||
.columns(in: databaseTableName)
|
||||
.map(\.name)
|
||||
.filter { lowercaseUpdatedColumns.contains($0.lowercased()) }
|
||||
|
||||
if updatedColumns.isEmpty {
|
||||
// IMPLEMENTATION NOTE
|
||||
//
|
||||
// It is important to update something, so that
|
||||
// TransactionObserver can observe a change even though this
|
||||
// change is useless.
|
||||
//
|
||||
// The goal is to be able to write tests with minimal tables,
|
||||
// including tables made of a single primary key column.
|
||||
updatedColumns = primaryKeyColumns
|
||||
}
|
||||
|
||||
let updatedValues = updatedColumns.map {
|
||||
persistenceContainer[caseInsensitive: $0]?.databaseValue ?? .null
|
||||
}
|
||||
|
||||
let query = UpdateQuery(
|
||||
onConflict: onConflict,
|
||||
tableName: databaseTableName,
|
||||
updatedColumns: updatedColumns,
|
||||
conditionColumns: primaryKeyColumns)
|
||||
|
||||
return try makeStatement(
|
||||
sql: query.sql,
|
||||
checkedArguments: StatementArguments(updatedValues + primaryKeyValues),
|
||||
returning: selection)
|
||||
}
|
||||
|
||||
/// Returns nil if and only if primary key is nil
|
||||
func deleteStatement() throws -> Statement? {
|
||||
// Fail early if primary key does not resolve to a database row.
|
||||
let primaryKeyColumns = primaryKey.columns
|
||||
let primaryKeyValues = primaryKeyColumns.map {
|
||||
persistenceContainer[caseInsensitive: $0]?.databaseValue ?? .null
|
||||
}
|
||||
if primaryKeyValues.allSatisfy({ $0.isNull }) {
|
||||
return nil
|
||||
}
|
||||
|
||||
let query = DeleteQuery(
|
||||
tableName: databaseTableName,
|
||||
conditionColumns: primaryKeyColumns)
|
||||
let statement = try db.internalCachedStatement(sql: query.sql)
|
||||
statement.setUncheckedArguments(StatementArguments(primaryKeyValues))
|
||||
return statement
|
||||
}
|
||||
|
||||
/// Returns nil if and only if primary key is nil
|
||||
func existsStatement() throws -> Statement? {
|
||||
// Fail early if primary key does not resolve to a database row.
|
||||
let primaryKeyColumns = primaryKey.columns
|
||||
let primaryKeyValues = primaryKeyColumns.map {
|
||||
persistenceContainer[caseInsensitive: $0]?.databaseValue ?? .null
|
||||
}
|
||||
if primaryKeyValues.allSatisfy({ $0.isNull }) {
|
||||
return nil
|
||||
}
|
||||
|
||||
let query = ExistsQuery(
|
||||
tableName: databaseTableName,
|
||||
conditionColumns: primaryKeyColumns)
|
||||
let statement = try db.internalCachedStatement(sql: query.sql)
|
||||
statement.setUncheckedArguments(StatementArguments(primaryKeyValues))
|
||||
return statement
|
||||
}
|
||||
|
||||
/// Throws a RecordError.recordNotFound error
|
||||
func recordNotFound() throws -> Never {
|
||||
let key = Dictionary(uniqueKeysWithValues: primaryKey.columns.map {
|
||||
($0, persistenceContainer[caseInsensitive: $0]?.databaseValue ?? .null)
|
||||
})
|
||||
throw RecordError.recordNotFound(
|
||||
databaseTableName: databaseTableName,
|
||||
key: key)
|
||||
}
|
||||
|
||||
// Support for the RETURNING clause
|
||||
private func makeStatement(
|
||||
sql: String,
|
||||
checkedArguments arguments: StatementArguments,
|
||||
returning selection: [any SQLSelectable])
|
||||
throws -> Statement
|
||||
{
|
||||
if selection.isEmpty {
|
||||
let statement = try db.internalCachedStatement(sql: sql)
|
||||
// We have built valid arguments: don't check
|
||||
statement.setUncheckedArguments(arguments)
|
||||
return statement
|
||||
} else {
|
||||
let context = SQLGenerationContext(db)
|
||||
var sql = sql
|
||||
var arguments = arguments
|
||||
sql += " RETURNING "
|
||||
sql += try selection
|
||||
.map { try $0.sqlSelection.sql(context) }
|
||||
.joined(separator: ", ")
|
||||
arguments += context.arguments
|
||||
let statement = try db.internalCachedStatement(sql: sql)
|
||||
statement.arguments = arguments
|
||||
return statement
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - InsertQuery
|
||||
|
||||
private struct InsertQuery: Hashable {
|
||||
let onConflict: Database.ConflictResolution
|
||||
let tableName: String
|
||||
let insertedColumns: [String]
|
||||
}
|
||||
|
||||
extension InsertQuery {
|
||||
@ReadWriteBox private static var sqlCache: [InsertQuery: String] = [:]
|
||||
var sql: String {
|
||||
if let sql = Self.sqlCache[self] {
|
||||
return sql
|
||||
}
|
||||
let columnsSQL = insertedColumns.map(\.quotedDatabaseIdentifier).joined(separator: ", ")
|
||||
let valuesSQL = databaseQuestionMarks(count: insertedColumns.count)
|
||||
let sql: String
|
||||
switch onConflict {
|
||||
case .abort:
|
||||
sql = """
|
||||
INSERT INTO \(tableName.quotedDatabaseIdentifier) (\(columnsSQL)) \
|
||||
VALUES (\(valuesSQL))
|
||||
"""
|
||||
default:
|
||||
sql = """
|
||||
INSERT OR \(onConflict.rawValue) \
|
||||
INTO \(tableName.quotedDatabaseIdentifier) (\(columnsSQL)) \
|
||||
VALUES (\(valuesSQL))
|
||||
"""
|
||||
}
|
||||
Self.sqlCache[self] = sql
|
||||
return sql
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UpdateQuery
|
||||
|
||||
private struct UpdateQuery: Hashable {
|
||||
let onConflict: Database.ConflictResolution
|
||||
let tableName: String
|
||||
let updatedColumns: [String]
|
||||
let conditionColumns: [String]
|
||||
}
|
||||
|
||||
extension UpdateQuery {
|
||||
@ReadWriteBox private static var sqlCache: [UpdateQuery: String] = [:]
|
||||
var sql: String {
|
||||
if let sql = Self.sqlCache[self] {
|
||||
return sql
|
||||
}
|
||||
let updateSQL = updatedColumns.map { "\($0.quotedDatabaseIdentifier)=?" }.joined(separator: ", ")
|
||||
let whereSQL = conditionColumns.map { "\($0.quotedDatabaseIdentifier)=?" }.joined(separator: " AND ")
|
||||
let sql: String
|
||||
switch onConflict {
|
||||
case .abort:
|
||||
sql = """
|
||||
UPDATE \(tableName.quotedDatabaseIdentifier) \
|
||||
SET \(updateSQL) \
|
||||
WHERE \(whereSQL)
|
||||
"""
|
||||
default:
|
||||
sql = """
|
||||
UPDATE OR \(onConflict.rawValue) \(tableName.quotedDatabaseIdentifier) \
|
||||
SET \(updateSQL) \
|
||||
WHERE \(whereSQL)
|
||||
"""
|
||||
}
|
||||
Self.sqlCache[self] = sql
|
||||
return sql
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - DeleteQuery
|
||||
|
||||
private struct DeleteQuery {
|
||||
let tableName: String
|
||||
let conditionColumns: [String]
|
||||
}
|
||||
|
||||
extension DeleteQuery {
|
||||
var sql: String {
|
||||
let whereSQL = conditionColumns.map { "\($0.quotedDatabaseIdentifier)=?" }.joined(separator: " AND ")
|
||||
return "DELETE FROM \(tableName.quotedDatabaseIdentifier) WHERE \(whereSQL)"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ExistsQuery
|
||||
|
||||
private struct ExistsQuery {
|
||||
let tableName: String
|
||||
let conditionColumns: [String]
|
||||
}
|
||||
|
||||
extension ExistsQuery {
|
||||
var sql: String {
|
||||
let whereSQL = conditionColumns.map { "\($0.quotedDatabaseIdentifier)=?" }.joined(separator: " AND ")
|
||||
return "SELECT EXISTS (SELECT 1 FROM \(tableName.quotedDatabaseIdentifier) WHERE \(whereSQL))"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// MARK: - Delete Callbacks
|
||||
|
||||
extension MutablePersistableRecord {
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public func willDelete(_ db: Database) throws { }
|
||||
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public func aroundDelete(_ db: Database, delete: () throws -> Bool) throws {
|
||||
_ = try delete()
|
||||
}
|
||||
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public func didDelete(deleted: Bool) { }
|
||||
}
|
||||
|
||||
// MARK: - Delete
|
||||
|
||||
extension MutablePersistableRecord {
|
||||
/// Executes a DELETE statement.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - returns: Whether a database row was deleted.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
@discardableResult
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
public func delete(_ db: Database) throws -> Bool {
|
||||
try willDelete(db)
|
||||
|
||||
var deleted: Bool?
|
||||
try aroundDelete(db) {
|
||||
deleted = try deleteWithoutCallbacks(db)
|
||||
return deleted!
|
||||
}
|
||||
|
||||
guard let deleted else {
|
||||
try persistenceCallbackMisuse("aroundDelete")
|
||||
}
|
||||
didDelete(deleted: deleted)
|
||||
return deleted
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Internals
|
||||
|
||||
extension MutablePersistableRecord {
|
||||
/// Executes an `DELETE` statement, and DOES NOT run deletion callbacks.
|
||||
@usableFromInline
|
||||
func deleteWithoutCallbacks(_ db: Database) throws -> Bool {
|
||||
guard let statement = try DAO(db, self).deleteStatement() else {
|
||||
// Nil primary key
|
||||
return false
|
||||
}
|
||||
try statement.execute()
|
||||
return db.changesCount > 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,531 @@
|
||||
// MARK: - Insert Callbacks
|
||||
|
||||
extension MutablePersistableRecord {
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public mutating func willInsert(_ db: Database) throws { }
|
||||
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public func aroundInsert(_ db: Database, insert: () throws -> InsertionSuccess) throws {
|
||||
_ = try insert()
|
||||
}
|
||||
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public mutating func didInsert(_ inserted: InsertionSuccess) { }
|
||||
}
|
||||
|
||||
// MARK: - Insert
|
||||
|
||||
extension MutablePersistableRecord {
|
||||
/// Executes an `INSERT` statement.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.write { db in
|
||||
/// var player = Player(name: "Arthur")
|
||||
/// try player.insert(db)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
public mutating func insert(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil)
|
||||
throws
|
||||
{
|
||||
try willSave(db)
|
||||
|
||||
var saved: PersistenceSuccess?
|
||||
try aroundSave(db) {
|
||||
let inserted = try insertWithCallbacks(db, onConflict: conflictResolution)
|
||||
saved = PersistenceSuccess(inserted)
|
||||
return saved!
|
||||
}
|
||||
|
||||
guard let saved else {
|
||||
try persistenceCallbackMisuse("aroundSave")
|
||||
}
|
||||
didSave(saved)
|
||||
}
|
||||
|
||||
/// Executes an `INSERT` statement, and returns the inserted record.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let player = Player(name: "Arthur")
|
||||
/// let insertedPlayer = try await dbQueue.write { [player] db in
|
||||
/// try player.inserted(db)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - returns: The inserted record.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
public func inserted(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil)
|
||||
throws -> Self
|
||||
{
|
||||
var result = self
|
||||
try result.insert(db, onConflict: conflictResolution)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Insert and Fetch
|
||||
|
||||
extension MutablePersistableRecord {
|
||||
#if GRDBCUSTOMSQLITE || GRDBCIPHER
|
||||
// TODO: GRDB7 make it unable to return an optional
|
||||
/// Executes an `INSERT RETURNING` statement, and returns a new record built
|
||||
/// from the inserted row.
|
||||
///
|
||||
/// This method is equivalent to ``insertAndFetch(_:onConflict:as:)``,
|
||||
/// with `Self` as the `returnedType` argument:
|
||||
///
|
||||
/// ```swift
|
||||
/// // Equivalent
|
||||
/// let insertedPlayer = try player.insertAndFetch(db)
|
||||
/// let insertedPlayer = try player.insertAndFetch(db, as: Player.self)
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - returns: The inserted record, if any. The result can be nil when the
|
||||
/// conflict policy is `IGNORE`.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
public func insertAndFetch(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil)
|
||||
throws -> Self?
|
||||
where Self: FetchableRecord
|
||||
{
|
||||
var result = self
|
||||
return try result.insertAndFetch(db, onConflict: conflictResolution, as: Self.self)
|
||||
}
|
||||
|
||||
// TODO: GRDB7 make it unable to return an optional
|
||||
/// Executes an `INSERT RETURNING` statement, and returns a new record built
|
||||
/// from the inserted row.
|
||||
///
|
||||
/// This method helps dealing with default column values and
|
||||
/// generated columns.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// // A table with an auto-incremented primary key and a default value
|
||||
/// try dbQueue.write { db in
|
||||
/// try db.execute(sql: """
|
||||
/// CREATE TABLE player(
|
||||
/// id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
/// name TEXT,
|
||||
/// score INTEGER DEFAULT 1000)
|
||||
/// """)
|
||||
/// }
|
||||
///
|
||||
/// // A player with partial database information
|
||||
/// struct PartialPlayer: MutablePersistableRecord {
|
||||
/// static let databaseTableName = "player"
|
||||
/// var name: String
|
||||
/// }
|
||||
///
|
||||
/// // A full player, with all database information
|
||||
/// struct Player: TableRecord, FetchableRecord {
|
||||
/// var id: Int64
|
||||
/// var name: String
|
||||
/// var score: Int
|
||||
/// }
|
||||
///
|
||||
/// // Insert a partial player, get a full one
|
||||
/// try dbQueue.write { db in
|
||||
/// var partialPlayer = PartialPlayer(name: "Alice")
|
||||
///
|
||||
/// // INSERT INTO player (name) VALUES ('Alice') RETURNING *
|
||||
/// if let player = try partialPlayer.insertAndFetch(db, as: FullPlayer.self) {
|
||||
/// print(player.id) // The inserted id
|
||||
/// print(player.name) // The inserted name
|
||||
/// print(player.score) // The default score
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - parameter returnedType: The type of the returned record.
|
||||
/// - returns: A record of type `returnedType`, if any. The result can be
|
||||
/// nil when the conflict policy is `IGNORE`.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
public mutating func insertAndFetch<T: FetchableRecord & TableRecord>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
||||
as returnedType: T.Type)
|
||||
throws -> T?
|
||||
{
|
||||
try insertAndFetch(db, onConflict: conflictResolution, selection: T.databaseSelection) {
|
||||
try T.fetchOne($0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes an `INSERT RETURNING` statement, and returns the selected
|
||||
/// columns from the inserted row.
|
||||
///
|
||||
/// This method helps dealing with default column values and
|
||||
/// generated columns.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// // A table with an auto-incremented primary key and a default value
|
||||
/// try dbQueue.write { db in
|
||||
/// try db.execute(sql: """
|
||||
/// CREATE TABLE player(
|
||||
/// id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
/// name TEXT,
|
||||
/// score INTEGER DEFAULT 1000)
|
||||
/// """)
|
||||
/// }
|
||||
///
|
||||
/// // A player with partial database information
|
||||
/// struct PartialPlayer: MutablePersistableRecord {
|
||||
/// static let databaseTableName = "player"
|
||||
/// var name: String
|
||||
/// }
|
||||
///
|
||||
/// // Insert a partial player, get the inserted score
|
||||
/// try dbQueue.write { db in
|
||||
/// var partialPlayer = PartialPlayer(name: "Alice")
|
||||
///
|
||||
/// // INSERT INTO player (name) VALUES ('Alice') RETURNING score
|
||||
/// let score = try partialPlayer.insertAndFetch(db, selection: [Column("score")]) { statement in
|
||||
/// try Int.fetchOne(statement)
|
||||
/// }
|
||||
/// print(score) // The inserted score
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - parameter selection: The returned columns (must not be empty).
|
||||
/// - parameter fetch: A closure that executes its ``Statement`` argument.
|
||||
/// If the conflict policy is `IGNORE`, the statement may return no row.
|
||||
/// - returns: The result of the `fetch` function.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type.
|
||||
/// - precondition: `selection` is not empty.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
public mutating func insertAndFetch<T>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
||||
selection: [any SQLSelectable],
|
||||
fetch: (Statement) throws -> T)
|
||||
throws -> T
|
||||
{
|
||||
GRDBPrecondition(!selection.isEmpty, "Invalid empty selection")
|
||||
|
||||
try willSave(db)
|
||||
|
||||
var success: (inserted: InsertionSuccess, returned: T)?
|
||||
try aroundSave(db) {
|
||||
success = try insertAndFetchWithCallbacks(
|
||||
db, onConflict: conflictResolution,
|
||||
selection: selection,
|
||||
fetch: fetch)
|
||||
return PersistenceSuccess(success!.inserted)
|
||||
}
|
||||
|
||||
guard let success else {
|
||||
try persistenceCallbackMisuse("aroundSave")
|
||||
}
|
||||
didSave(PersistenceSuccess(success.inserted))
|
||||
return success.returned
|
||||
}
|
||||
#else
|
||||
// TODO: GRDB7 make it unable to return an optional
|
||||
/// Executes an `INSERT RETURNING` statement, and returns a new record built
|
||||
/// from the inserted row.
|
||||
///
|
||||
/// This method is equivalent to ``insertAndFetch(_:onConflict:as:)``,
|
||||
/// with `Self` as the `returnedType` argument:
|
||||
///
|
||||
/// ```swift
|
||||
/// // Equivalent
|
||||
/// let insertedPlayer = try player.insertAndFetch(db)
|
||||
/// let insertedPlayer = try player.insertAndFetch(db, as: Player.self)
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - returns: The inserted record, if any. The result can be nil when the
|
||||
/// conflict policy is `IGNORE`.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+
|
||||
public func insertAndFetch(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil)
|
||||
throws -> Self?
|
||||
where Self: FetchableRecord
|
||||
{
|
||||
var result = self
|
||||
return try result.insertAndFetch(db, onConflict: conflictResolution, as: Self.self)
|
||||
}
|
||||
|
||||
// TODO: GRDB7 make it unable to return an optional
|
||||
/// Executes an `INSERT RETURNING` statement, and returns a new record built
|
||||
/// from the inserted row.
|
||||
///
|
||||
/// This method helps dealing with default column values and
|
||||
/// generated columns.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// // A table with an auto-incremented primary key and a default value
|
||||
/// try dbQueue.write { db in
|
||||
/// try db.execute(sql: """
|
||||
/// CREATE TABLE player(
|
||||
/// id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
/// name TEXT,
|
||||
/// score INTEGER DEFAULT 1000)
|
||||
/// """)
|
||||
/// }
|
||||
///
|
||||
/// // A player with partial database information
|
||||
/// struct PartialPlayer: MutablePersistableRecord {
|
||||
/// static let databaseTableName = "player"
|
||||
/// var name: String
|
||||
/// }
|
||||
///
|
||||
/// // A full player, with all database information
|
||||
/// struct Player: TableRecord, FetchableRecord {
|
||||
/// var id: Int64
|
||||
/// var name: String
|
||||
/// var score: Int
|
||||
/// }
|
||||
///
|
||||
/// // Insert a partial player, get a full one
|
||||
/// try dbQueue.write { db in
|
||||
/// var partialPlayer = PartialPlayer(name: "Alice")
|
||||
///
|
||||
/// // INSERT INTO player (name) VALUES ('Alice') RETURNING *
|
||||
/// if let player = try partialPlayer.insertAndFetch(db, as: FullPlayer.self) {
|
||||
/// print(player.id) // The inserted id
|
||||
/// print(player.name) // The inserted name
|
||||
/// print(player.score) // The default score
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - parameter returnedType: The type of the returned record.
|
||||
/// - returns: A record of type `returnedType`, if any. The result can be
|
||||
/// nil when the conflict policy is `IGNORE`.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+
|
||||
public mutating func insertAndFetch<T: FetchableRecord & TableRecord>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
||||
as returnedType: T.Type)
|
||||
throws -> T?
|
||||
{
|
||||
try insertAndFetch(db, onConflict: conflictResolution, selection: T.databaseSelection) {
|
||||
try T.fetchOne($0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes an `INSERT RETURNING` statement, and returns the selected
|
||||
/// columns from the inserted row.
|
||||
///
|
||||
/// This method helps dealing with default column values and
|
||||
/// generated columns.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// // A table with an auto-incremented primary key and a default value
|
||||
/// try dbQueue.write { db in
|
||||
/// try db.execute(sql: """
|
||||
/// CREATE TABLE player(
|
||||
/// id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
/// name TEXT,
|
||||
/// score INTEGER DEFAULT 1000)
|
||||
/// """)
|
||||
/// }
|
||||
///
|
||||
/// // A player with partial database information
|
||||
/// struct PartialPlayer: MutablePersistableRecord {
|
||||
/// static let databaseTableName = "player"
|
||||
/// var name: String
|
||||
/// }
|
||||
///
|
||||
/// // Insert a partial player, get the inserted score
|
||||
/// try dbQueue.write { db in
|
||||
/// var partialPlayer = PartialPlayer(name: "Alice")
|
||||
///
|
||||
/// // INSERT INTO player (name) VALUES ('Alice') RETURNING score
|
||||
/// let score = try partialPlayer.insertAndFetch(db, selection: [Column("score")]) { statement in
|
||||
/// try Int.fetchOne(statement)
|
||||
/// }
|
||||
/// print(score) // The inserted score
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - parameter selection: The returned columns (must not be empty).
|
||||
/// - parameter fetch: A closure that executes its ``Statement`` argument.
|
||||
/// If the conflict policy is `IGNORE`, the statement may return no row.
|
||||
/// - returns: The result of the `fetch` function.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type.
|
||||
/// - precondition: `selection` is not empty.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+
|
||||
public mutating func insertAndFetch<T>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
||||
selection: [any SQLSelectable],
|
||||
fetch: (Statement) throws -> T)
|
||||
throws -> T
|
||||
{
|
||||
GRDBPrecondition(!selection.isEmpty, "Invalid empty selection")
|
||||
|
||||
try willSave(db)
|
||||
|
||||
var success: (inserted: InsertionSuccess, returned: T)?
|
||||
try aroundSave(db) {
|
||||
success = try insertAndFetchWithCallbacks(
|
||||
db, onConflict: conflictResolution,
|
||||
selection: selection,
|
||||
fetch: fetch)
|
||||
return PersistenceSuccess(success!.inserted)
|
||||
}
|
||||
|
||||
guard let success else {
|
||||
try persistenceCallbackMisuse("aroundSave")
|
||||
}
|
||||
didSave(PersistenceSuccess(success.inserted))
|
||||
return success.returned
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: - Internals
|
||||
|
||||
extension MutablePersistableRecord {
|
||||
/// Executes an `INSERT` statement, and runs insertion callbacks.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
mutating func insertWithCallbacks(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution?)
|
||||
throws -> InsertionSuccess
|
||||
{
|
||||
let (inserted, _) = try insertAndFetchWithCallbacks(db, onConflict: conflictResolution, selection: []) {
|
||||
// Nothing to fetch
|
||||
try $0.execute()
|
||||
}
|
||||
return inserted
|
||||
}
|
||||
|
||||
/// Executes an `INSERT` statement, with `RETURNING` clause if `selection`
|
||||
/// is not empty, and runs insertion callbacks.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
mutating func insertAndFetchWithCallbacks<T>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution?,
|
||||
selection: [any SQLSelectable],
|
||||
fetch: (Statement) throws -> T)
|
||||
throws -> (InsertionSuccess, T)
|
||||
{
|
||||
try willInsert(db)
|
||||
|
||||
var success: (inserted: InsertionSuccess, returned: T)?
|
||||
try aroundInsert(db) {
|
||||
success = try insertAndFetchWithoutCallbacks(
|
||||
db, onConflict: conflictResolution,
|
||||
selection: selection,
|
||||
fetch: fetch)
|
||||
return success!.inserted
|
||||
}
|
||||
|
||||
guard let success else {
|
||||
try persistenceCallbackMisuse("aroundInsert")
|
||||
}
|
||||
didInsert(success.inserted)
|
||||
return success
|
||||
}
|
||||
|
||||
/// Executes an `INSERT` statement, with `RETURNING` clause if `selection`
|
||||
/// is not empty, and DOES NOT run insertion callbacks.
|
||||
@usableFromInline
|
||||
func insertAndFetchWithoutCallbacks<T>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution?,
|
||||
selection: [any SQLSelectable],
|
||||
fetch: (Statement) throws -> T)
|
||||
throws -> (InsertionSuccess, T)
|
||||
{
|
||||
let conflictResolution = conflictResolution ?? type(of: self)
|
||||
.persistenceConflictPolicy
|
||||
.conflictResolutionForInsert
|
||||
let dao = try DAO(db, self)
|
||||
let statement = try dao.insertStatement(
|
||||
db,
|
||||
onConflict: conflictResolution,
|
||||
returning: selection)
|
||||
let returned = try fetch(statement)
|
||||
|
||||
let rowIDColumn = dao.primaryKey.rowIDColumn
|
||||
let rowid = db.lastInsertedRowID
|
||||
|
||||
// Update the persistenceContainer with the inserted rowid.
|
||||
// This allows the Record class to set its `hasDatabaseChanges` property
|
||||
// to false in its `aroundInsert` callback.
|
||||
var persistenceContainer = dao.persistenceContainer
|
||||
if let rowIDColumn {
|
||||
persistenceContainer[caseInsensitive: rowIDColumn] = rowid
|
||||
}
|
||||
|
||||
let inserted = InsertionSuccess(
|
||||
rowID: rowid,
|
||||
rowIDColumn: rowIDColumn,
|
||||
persistenceContainer: persistenceContainer)
|
||||
return (inserted, returned)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,411 @@
|
||||
// MARK: - Save Callbacks
|
||||
|
||||
extension MutablePersistableRecord {
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public func willSave(_ db: Database) throws { }
|
||||
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public func aroundSave(_ db: Database, save: () throws -> PersistenceSuccess) throws {
|
||||
_ = try save()
|
||||
}
|
||||
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public func didSave(_ saved: PersistenceSuccess) { }
|
||||
}
|
||||
|
||||
// MARK: - Save
|
||||
|
||||
extension MutablePersistableRecord {
|
||||
/// Executes an `INSERT` or `UPDATE` statement.
|
||||
///
|
||||
/// If the receiver has a non-nil primary key and a matching row in the
|
||||
/// database, this method performs an update.
|
||||
///
|
||||
/// Otherwise, performs an insert.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
public mutating func save(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil)
|
||||
throws
|
||||
{
|
||||
try willSave(db)
|
||||
|
||||
var saved: PersistenceSuccess?
|
||||
try aroundSave(db) {
|
||||
saved = try updateOrInsertWithCallbacks(db, onConflict: conflictResolution)
|
||||
return saved!
|
||||
}
|
||||
|
||||
guard let saved else {
|
||||
try persistenceCallbackMisuse("aroundSave")
|
||||
}
|
||||
didSave(saved)
|
||||
}
|
||||
|
||||
/// Executes an `INSERT` or `UPDATE` statement, and returns the
|
||||
/// saved record.
|
||||
///
|
||||
/// Usage:
|
||||
///
|
||||
/// let player = Player(id: nil, name: "Arthur")
|
||||
/// let savedPlayer = try dbQueue.write { db in
|
||||
/// try player.saved(db)
|
||||
/// }
|
||||
/// print(player.id) // nil
|
||||
/// print(savedPlayer.id) // some id
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - returns: The saved record.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
public func saved(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil)
|
||||
throws -> Self
|
||||
{
|
||||
var result = self
|
||||
try result.save(db, onConflict: conflictResolution)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Save and Fetch
|
||||
|
||||
extension MutablePersistableRecord {
|
||||
#if GRDBCUSTOMSQLITE || GRDBCIPHER
|
||||
// TODO: GRDB7 make it unable to return an optional
|
||||
/// Executes an `INSERT RETURNING` or `UPDATE RETURNING` statement, and
|
||||
/// returns a new record built from the saved row.
|
||||
///
|
||||
/// If the receiver has a non-nil primary key and a matching row in the
|
||||
/// database, this method performs an update. Otherwise, it performs
|
||||
/// an insert.
|
||||
///
|
||||
/// This method helps dealing with default column values and
|
||||
/// generated columns.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// let player: Player = ...
|
||||
/// let savedPlayer = player.saveAndFetch(db)
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - returns: The saved record. The result can be nil when the
|
||||
/// conflict policy is `IGNORE`.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
public func saveAndFetch(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil)
|
||||
throws -> Self?
|
||||
where Self: FetchableRecord
|
||||
{
|
||||
var result = self
|
||||
return try result.saveAndFetch(db, onConflict: conflictResolution, as: Self.self)
|
||||
}
|
||||
|
||||
// TODO: GRDB7 make it unable to return an optional
|
||||
/// Executes an `INSERT RETURNING` or `UPDATE RETURNING` statement, and
|
||||
/// returns a new record built from the saved row.
|
||||
///
|
||||
/// If the receiver has a non-nil primary key and a matching row in the
|
||||
/// database, this method performs an update. Otherwise, it performs
|
||||
/// an insert.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - parameter returnedType: The type of the returned record.
|
||||
/// - returns: A record of type `returnedType`. The result can be nil when
|
||||
/// the conflict policy is `IGNORE`.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
public mutating func saveAndFetch<T: FetchableRecord & TableRecord>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
||||
as returnedType: T.Type)
|
||||
throws -> T?
|
||||
{
|
||||
try willSave(db)
|
||||
|
||||
var success: (saved: PersistenceSuccess, returned: T?)?
|
||||
try aroundSave(db) {
|
||||
success = try updateOrInsertAndFetchWithCallbacks(
|
||||
db, onConflict: conflictResolution,
|
||||
selection: T.databaseSelection,
|
||||
fetch: {
|
||||
try T.fetchOne($0)
|
||||
})
|
||||
return success!.saved
|
||||
}
|
||||
|
||||
guard let success else {
|
||||
try persistenceCallbackMisuse("aroundSave")
|
||||
}
|
||||
didSave(success.saved)
|
||||
return success.returned
|
||||
}
|
||||
|
||||
/// Executes an `INSERT RETURNING` or `UPDATE RETURNING` statement, and
|
||||
/// returns the selected columns from the saved row.
|
||||
///
|
||||
/// If the receiver has a non-nil primary key and a matching row in the
|
||||
/// database, this method performs an update. Otherwise, it performs
|
||||
/// an insert.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - parameter selection: The returned columns (must not be empty).
|
||||
/// - parameter fetch: A function that executes it ``Statement`` argument.
|
||||
/// - returns: The result of the `fetch` function.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type.
|
||||
/// - precondition: `selection` is not empty.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
public mutating func saveAndFetch<T>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
||||
selection: [any SQLSelectable],
|
||||
fetch: (Statement) throws -> T)
|
||||
throws -> T
|
||||
{
|
||||
GRDBPrecondition(!selection.isEmpty, "Invalid empty selection")
|
||||
|
||||
try willSave(db)
|
||||
|
||||
var success: (saved: PersistenceSuccess, returned: T)?
|
||||
try aroundSave(db) {
|
||||
success = try updateOrInsertAndFetchWithCallbacks(
|
||||
db, onConflict: conflictResolution,
|
||||
selection: selection,
|
||||
fetch: fetch)
|
||||
return success!.saved
|
||||
}
|
||||
|
||||
guard let success else {
|
||||
try persistenceCallbackMisuse("aroundSave")
|
||||
}
|
||||
didSave(success.saved)
|
||||
return success.returned
|
||||
}
|
||||
#else
|
||||
// TODO: GRDB7 make it unable to return an optional
|
||||
/// Executes an `INSERT RETURNING` or `UPDATE RETURNING` statement, and
|
||||
/// returns a new record built from the saved row.
|
||||
///
|
||||
/// If the receiver has a non-nil primary key and a matching row in the
|
||||
/// database, this method performs an update. Otherwise, it performs
|
||||
/// an insert.
|
||||
///
|
||||
/// This method helps dealing with default column values and
|
||||
/// generated columns.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// let player: Player = ...
|
||||
/// let savedPlayer = player.saveAndFetch(db)
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - returns: The saved record. The result can be nil when the
|
||||
/// conflict policy is `IGNORE`.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+
|
||||
public func saveAndFetch(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil)
|
||||
throws -> Self?
|
||||
where Self: FetchableRecord
|
||||
{
|
||||
var result = self
|
||||
return try result.saveAndFetch(db, onConflict: conflictResolution, as: Self.self)
|
||||
}
|
||||
|
||||
// TODO: GRDB7 make it unable to return an optional
|
||||
/// Executes an `INSERT RETURNING` or `UPDATE RETURNING` statement, and
|
||||
/// returns a new record built from the saved row.
|
||||
///
|
||||
/// If the receiver has a non-nil primary key and a matching row in the
|
||||
/// database, this method performs an update. Otherwise, it performs
|
||||
/// an insert.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - parameter returnedType: The type of the returned record.
|
||||
/// - returns: A record of type `returnedType`. The result can be nil when
|
||||
/// the conflict policy is `IGNORE`.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+
|
||||
public mutating func saveAndFetch<T: FetchableRecord & TableRecord>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
||||
as returnedType: T.Type)
|
||||
throws -> T?
|
||||
{
|
||||
try willSave(db)
|
||||
|
||||
var success: (saved: PersistenceSuccess, returned: T?)?
|
||||
try aroundSave(db) {
|
||||
success = try updateOrInsertAndFetchWithCallbacks(
|
||||
db, onConflict: conflictResolution,
|
||||
selection: T.databaseSelection,
|
||||
fetch: {
|
||||
try T.fetchOne($0)
|
||||
})
|
||||
return success!.saved
|
||||
}
|
||||
|
||||
guard let success else {
|
||||
try persistenceCallbackMisuse("aroundSave")
|
||||
}
|
||||
didSave(success.saved)
|
||||
return success.returned
|
||||
}
|
||||
|
||||
/// Executes an `INSERT RETURNING` or `UPDATE RETURNING` statement, and
|
||||
/// returns the selected columns from the saved row.
|
||||
///
|
||||
/// If the receiver has a non-nil primary key and a matching row in the
|
||||
/// database, this method performs an update. Otherwise, it performs
|
||||
/// an insert.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - parameter selection: The returned columns (must not be empty).
|
||||
/// - parameter fetch: A function that executes it ``Statement`` argument.
|
||||
/// - returns: The result of the `fetch` function.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type.
|
||||
/// - precondition: `selection` is not empty.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+
|
||||
public mutating func saveAndFetch<T>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
||||
selection: [any SQLSelectable],
|
||||
fetch: (Statement) throws -> T)
|
||||
throws -> T
|
||||
{
|
||||
GRDBPrecondition(!selection.isEmpty, "Invalid empty selection")
|
||||
|
||||
try willSave(db)
|
||||
|
||||
var success: (saved: PersistenceSuccess, returned: T)?
|
||||
try aroundSave(db) {
|
||||
success = try updateOrInsertAndFetchWithCallbacks(
|
||||
db, onConflict: conflictResolution,
|
||||
selection: selection,
|
||||
fetch: fetch)
|
||||
return success!.saved
|
||||
}
|
||||
|
||||
guard let success else {
|
||||
try persistenceCallbackMisuse("aroundSave")
|
||||
}
|
||||
didSave(success.saved)
|
||||
return success.returned
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: - Internal
|
||||
|
||||
extension MutablePersistableRecord {
|
||||
/// Executes an `UPDATE` or `INSERT` statement, and runs insertion or
|
||||
/// update callbacks.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
mutating func updateOrInsertWithCallbacks(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution?)
|
||||
throws -> PersistenceSuccess
|
||||
{
|
||||
let (saved, _) = try updateOrInsertAndFetchWithCallbacks(
|
||||
db, onConflict: conflictResolution,
|
||||
selection: [],
|
||||
fetch: {
|
||||
// Nothing to fetch
|
||||
try $0.execute()
|
||||
})
|
||||
return saved
|
||||
}
|
||||
|
||||
/// Executes an `UPDATE` or `INSERT` statement, with `RETURNING` clause
|
||||
/// if `selection` is not empty, and runs insertion or update callbacks.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
mutating func updateOrInsertAndFetchWithCallbacks<T>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution?,
|
||||
selection: [any SQLSelectable],
|
||||
fetch: (Statement) throws -> T)
|
||||
throws -> (PersistenceSuccess, T)
|
||||
{
|
||||
// Attempt at updating if the record has a primary key
|
||||
if let key = try primaryKey(db) {
|
||||
let databaseTableName = type(of: self).databaseTableName
|
||||
do {
|
||||
let columns = try Set(db.columns(in: databaseTableName).map(\.name))
|
||||
return try updateAndFetchWithCallbacks(
|
||||
db, onConflict: conflictResolution,
|
||||
columns: columns,
|
||||
selection: selection,
|
||||
fetch: fetch)
|
||||
} catch RecordError.recordNotFound(databaseTableName: databaseTableName, key: key) {
|
||||
// No row was updated: fallback on insert.
|
||||
}
|
||||
}
|
||||
|
||||
// Insert
|
||||
let (inserted, returned) = try insertAndFetchWithCallbacks(
|
||||
db, onConflict: conflictResolution,
|
||||
selection: selection,
|
||||
fetch: fetch)
|
||||
return (PersistenceSuccess(inserted), returned)
|
||||
}
|
||||
|
||||
/// Return a non-nil dictionary if record has a non-null primary key
|
||||
@usableFromInline
|
||||
func primaryKey(_ db: Database) throws -> [String: DatabaseValue]? {
|
||||
let databaseTableName = type(of: self).databaseTableName
|
||||
let primaryKeyInfo = try db.primaryKey(databaseTableName)
|
||||
let container = try PersistenceContainer(db, self)
|
||||
let primaryKey = Dictionary(uniqueKeysWithValues: primaryKeyInfo.columns.map {
|
||||
($0, container[caseInsensitive: $0]?.databaseValue ?? .null)
|
||||
})
|
||||
if primaryKey.allSatisfy({ $0.value.isNull }) {
|
||||
return nil
|
||||
}
|
||||
return primaryKey
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,976 @@
|
||||
// MARK: - Update Callbacks
|
||||
|
||||
extension MutablePersistableRecord {
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public func willUpdate(_ db: Database, columns: Set<String>) throws { }
|
||||
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public func aroundUpdate(_ db: Database, columns: Set<String>, update: () throws -> PersistenceSuccess) throws {
|
||||
_ = try update()
|
||||
}
|
||||
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public func didUpdate(_ updated: PersistenceSuccess) { }
|
||||
}
|
||||
|
||||
// MARK: - Update
|
||||
|
||||
extension MutablePersistableRecord {
|
||||
/// Executes an `UPDATE` statement on the provided columns.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.write { db in
|
||||
/// var player = Player.find(db, id: 1)
|
||||
/// player.score += 10
|
||||
/// try player.update(db, columns: ["score"])
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - parameter columns: The columns to update.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type,
|
||||
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
|
||||
/// primary key does not match any row in the database.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
public func update<Columns>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
||||
columns: Columns)
|
||||
throws
|
||||
where Columns: Sequence, Columns.Element == String
|
||||
{
|
||||
try willSave(db)
|
||||
|
||||
var updated: PersistenceSuccess?
|
||||
try aroundSave(db) {
|
||||
updated = try updateWithCallbacks(db, onConflict: conflictResolution, columns: Set(columns))
|
||||
return updated!
|
||||
}
|
||||
|
||||
guard let updated else {
|
||||
try persistenceCallbackMisuse("aroundSave")
|
||||
}
|
||||
didSave(updated)
|
||||
}
|
||||
|
||||
/// Executes an `UPDATE` statement on the provided columns.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.write { db in
|
||||
/// var player = Player.find(db, id: 1)
|
||||
/// player.score += 10
|
||||
/// try player.update(db, columns: [Column("score")])
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - parameter columns: The columns to update.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type,
|
||||
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
|
||||
/// primary key does not match any row in the database.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
public func update<Columns>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
||||
columns: Columns)
|
||||
throws
|
||||
where Columns: Sequence, Columns.Element: ColumnExpression
|
||||
{
|
||||
try update(db, onConflict: conflictResolution, columns: columns.map(\.name))
|
||||
}
|
||||
|
||||
/// Executes an `UPDATE` statement on all columns.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.write { db in
|
||||
/// var player = Player.find(db, id: 1)
|
||||
/// player.score += 10
|
||||
/// try player.update(db)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type,
|
||||
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
|
||||
/// primary key does not match any row in the database.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
public func update(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil)
|
||||
throws
|
||||
{
|
||||
let databaseTableName = type(of: self).databaseTableName
|
||||
let columns = try db.columns(in: databaseTableName).map(\.name)
|
||||
try update(db, onConflict: conflictResolution, columns: columns)
|
||||
}
|
||||
|
||||
/// If the record has any difference from the other record, executes an
|
||||
/// `UPDATE` statement so that those differences and only those differences
|
||||
/// are updated in the database.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.write { db in
|
||||
/// if let oldPlayer = Player.fetchOne(db, id: 1) {
|
||||
/// var newPlayer = oldPlayer
|
||||
/// newPlayer.score = 1000
|
||||
/// newPlayer.hasAward = true
|
||||
/// let modified = try newPlayer.updateChanges(db, from: oldPlayer)
|
||||
/// if modified {
|
||||
/// print("player was modified")
|
||||
/// } else {
|
||||
/// print("player was not modified")
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - parameter record: The comparison record.
|
||||
/// - returns: Whether the record had changes and was updated.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type,
|
||||
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
|
||||
/// primary key does not match any row in the database.
|
||||
/// - SeeAlso: updateChanges(_:with:)
|
||||
@discardableResult
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
public func updateChanges<Record: MutablePersistableRecord>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
||||
from record: Record)
|
||||
throws -> Bool
|
||||
{
|
||||
try updateChanges(db, onConflict: conflictResolution, from: PersistenceContainer(db, record))
|
||||
}
|
||||
|
||||
/// Modifies the record according to the provided `modify` closure, and
|
||||
/// executes an `UPDATE` statement that updates the modified columns, if and
|
||||
/// only if the record was modified.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.write { db in
|
||||
/// var player = Player.find(db, id: 1)
|
||||
/// let modified = try player.updateChanges(db) {
|
||||
/// $0.score = 1000
|
||||
/// $0.hasAward = true
|
||||
/// }
|
||||
/// if modified {
|
||||
/// print("player was modified")
|
||||
/// } else {
|
||||
/// print("player was not modified")
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - parameter modify: A closure that modifies the record.
|
||||
/// - returns: Whether the record was changed and updated.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type,
|
||||
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
|
||||
/// primary key does not match any row in the database.
|
||||
@discardableResult
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
public mutating func updateChanges(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
||||
modify: (inout Self) throws -> Void)
|
||||
throws -> Bool
|
||||
{
|
||||
let container = try PersistenceContainer(db, self)
|
||||
try modify(&self)
|
||||
return try updateChanges(db, onConflict: conflictResolution, from: container)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Update and Fetch
|
||||
|
||||
extension MutablePersistableRecord {
|
||||
#if GRDBCUSTOMSQLITE || GRDBCIPHER
|
||||
// TODO: GRDB7 make it unable to return an optional
|
||||
/// Executes an `UPDATE RETURNING` statement on all columns, and returns a
|
||||
/// new record built from the updated row.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - returns: The updated record. The result can be nil when the
|
||||
/// conflict policy is `IGNORE`.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type,
|
||||
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
|
||||
/// primary key does not match any row in the database.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
public func updateAndFetch(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil)
|
||||
throws -> Self?
|
||||
where Self: FetchableRecord
|
||||
{
|
||||
try updateAndFetch(db, onConflict: conflictResolution, as: Self.self)
|
||||
}
|
||||
|
||||
// TODO: GRDB7 make it unable to return an optional
|
||||
/// Executes an `UPDATE RETURNING` statement on all columns, and returns a
|
||||
/// new record built from the updated row.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - parameter returnedType: The type of the returned record.
|
||||
/// - returns: A record of type `returnedType`. The result can be nil when
|
||||
/// the conflict policy is `IGNORE`.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type,
|
||||
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
|
||||
/// primary key does not match any row in the database.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
public func updateAndFetch<T: FetchableRecord & TableRecord>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
||||
as returnedType: T.Type)
|
||||
throws -> T?
|
||||
{
|
||||
try updateAndFetch(db, onConflict: conflictResolution, selection: T.databaseSelection) {
|
||||
try T.fetchOne($0)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: GRDB7 make it unable to return an optional
|
||||
/// Modifies the record according to the provided `modify` closure, and
|
||||
/// executes an `UPDATE RETURNING` statement that updates the modified
|
||||
/// columns, if and only if the record was modified. The method returns a
|
||||
/// new record built from the updated row.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - parameter modify: A closure that modifies the record.
|
||||
/// - returns: An updated record, or nil if the record has no change, or
|
||||
/// in case of a failed update due to the `IGNORE` conflict policy.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type,
|
||||
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
|
||||
/// primary key does not match any row in the database.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
public mutating func updateChangesAndFetch(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
||||
modify: (inout Self) throws -> Void)
|
||||
throws -> Self?
|
||||
where Self: FetchableRecord
|
||||
{
|
||||
try updateChangesAndFetch(db, onConflict: conflictResolution, as: Self.self, modify: modify)
|
||||
}
|
||||
|
||||
// TODO: GRDB7 make it unable to return an optional
|
||||
/// Modifies the record according to the provided `modify` closure, and
|
||||
/// executes an `UPDATE RETURNING` statement that updates the modified
|
||||
/// columns, if and only if the record was modified. The method returns a
|
||||
/// new record built from the updated row.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - parameter returnedType: The type of the returned record.
|
||||
/// - parameter modify: A closure that modifies the record.
|
||||
/// - returns: A record of type `returnedType`, or nil if the record has
|
||||
/// no change, or in case of a failed update due to the `IGNORE`
|
||||
/// conflict policy.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type,
|
||||
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
|
||||
/// primary key does not match any row in the database.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
public mutating func updateChangesAndFetch<T: FetchableRecord & TableRecord>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
||||
as returnedType: T.Type,
|
||||
modify: (inout Self) throws -> Void)
|
||||
throws -> T?
|
||||
{
|
||||
try updateChangesAndFetch(
|
||||
db, onConflict: conflictResolution,
|
||||
selection: T.databaseSelection,
|
||||
fetch: { try T.fetchOne($0) },
|
||||
modify: modify)
|
||||
}
|
||||
|
||||
/// Executes an `UPDATE RETURNING` statement on the provided columns, and
|
||||
/// returns the selected columns from the updated row.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.write { db in
|
||||
/// // UPDATE player SET score = ... RETURNING totalScore
|
||||
/// let totalScore = try player.updateAndFetch(
|
||||
/// db, columns: ["Score"],
|
||||
/// selection: [Column("totalScore")],
|
||||
/// fetch: { statement in
|
||||
/// try Int.fetchOne(statement)
|
||||
/// })
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - parameter columns: The columns to update.
|
||||
/// - parameter selection: The returned columns (must not be empty).
|
||||
/// - parameter fetch: A function that executes it ``Statement`` argument.
|
||||
/// - returns: The result of the `fetch` function.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type,
|
||||
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
|
||||
/// primary key does not match any row in the database.
|
||||
/// - precondition: `selection` is not empty.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
public func updateAndFetch<T, Columns>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
||||
columns: Columns,
|
||||
selection: [any SQLSelectable],
|
||||
fetch: (Statement) throws -> T)
|
||||
throws -> T
|
||||
where Columns: Sequence, Columns.Element == String
|
||||
{
|
||||
GRDBPrecondition(!selection.isEmpty, "Invalid empty selection")
|
||||
|
||||
try willSave(db)
|
||||
|
||||
var success: (updated: PersistenceSuccess, returned: T)?
|
||||
try aroundSave(db) {
|
||||
success = try updateAndFetchWithCallbacks(
|
||||
db, onConflict: conflictResolution,
|
||||
columns: Set(columns),
|
||||
selection: selection,
|
||||
fetch: fetch)
|
||||
return success!.updated
|
||||
}
|
||||
|
||||
guard let success else {
|
||||
try persistenceCallbackMisuse("aroundSave")
|
||||
}
|
||||
didSave(success.updated)
|
||||
return success.returned
|
||||
}
|
||||
|
||||
/// Executes an `UPDATE RETURNING` statement on the provided columns, and
|
||||
/// returns the selected columns from the updated row.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.write { db in
|
||||
/// // UPDATE player SET score = ... RETURNING totalScore
|
||||
/// let totalScore = try player.updateAndFetch(
|
||||
/// db, columns: [Column("Score")],
|
||||
/// selection: [Column("totalScore")],
|
||||
/// fetch: { statement in
|
||||
/// try Int.fetchOne(statement)
|
||||
/// })
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - parameter columns: The columns to update.
|
||||
/// - parameter selection: The returned columns (must not be empty).
|
||||
/// - parameter fetch: A function that executes it ``Statement`` argument.
|
||||
/// - returns: The result of the `fetch` function.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type,
|
||||
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
|
||||
/// primary key does not match any row in the database.
|
||||
/// - precondition: `selection` is not empty.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
public func updateAndFetch<T, Columns>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
||||
columns: Columns,
|
||||
selection: [any SQLSelectable],
|
||||
fetch: (Statement) throws -> T)
|
||||
throws -> T
|
||||
where Columns: Sequence, Columns.Element: ColumnExpression
|
||||
{
|
||||
try updateAndFetch(
|
||||
db, onConflict: conflictResolution,
|
||||
columns: columns.map(\.name),
|
||||
selection: selection,
|
||||
fetch: fetch)
|
||||
}
|
||||
|
||||
/// Executes an `UPDATE RETURNING` statement on all columns, and returns the
|
||||
/// selected columns from the updated row.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.write { db in
|
||||
/// // UPDATE player SET ... RETURNING totalScore
|
||||
/// let totalScore = try player.updateAndFetch(db, selection: [Column("totalScore")]) { statement in
|
||||
/// try Int.fetchOne(statement)
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - parameter selection: The returned columns (must not be empty).
|
||||
/// - parameter fetch: A function that executes it ``Statement`` argument.
|
||||
/// - returns: The result of the `fetch` function.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type,
|
||||
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
|
||||
/// primary key does not match any row in the database.
|
||||
/// - precondition: `selection` is not empty.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
public func updateAndFetch<T>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
||||
selection: [any SQLSelectable],
|
||||
fetch: (Statement) throws -> T)
|
||||
throws -> T
|
||||
{
|
||||
let databaseTableName = type(of: self).databaseTableName
|
||||
let columns = try db.columns(in: databaseTableName).map(\.name)
|
||||
return try updateAndFetch(
|
||||
db, onConflict: conflictResolution,
|
||||
columns: columns,
|
||||
selection: selection,
|
||||
fetch: fetch)
|
||||
}
|
||||
|
||||
// TODO: GRDB7 make it unable to return an optional
|
||||
/// Modifies the record according to the provided `modify` closure, and
|
||||
/// executes an `UPDATE RETURNING` statement that updates the modified
|
||||
/// columns, if and only if the record was modified. The method returns a
|
||||
/// new record built from the updated row.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - parameter selection: The returned columns (must not be empty).
|
||||
/// - parameter fetch: A function that executes it ``Statement`` argument.
|
||||
/// - parameter modify: A closure that modifies the record.
|
||||
/// - returns: The result of the `fetch` function.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type,
|
||||
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
|
||||
/// primary key does not match any row in the database.
|
||||
/// - precondition: `selection` is not empty.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
public mutating func updateChangesAndFetch<T>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
||||
selection: [any SQLSelectable],
|
||||
fetch: (Statement) throws -> T?,
|
||||
modify: (inout Self) throws -> Void)
|
||||
throws -> T?
|
||||
{
|
||||
let container = try PersistenceContainer(db, self)
|
||||
try modify(&self)
|
||||
return try updateChangesAndFetch(
|
||||
db, onConflict: conflictResolution,
|
||||
from: container,
|
||||
selection: selection,
|
||||
fetch: fetch)
|
||||
}
|
||||
#else
|
||||
// TODO: GRDB7 make it unable to return an optional
|
||||
/// Executes an `UPDATE RETURNING` statement on all columns, and returns a
|
||||
/// new record built from the updated row.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - returns: The updated record. The result can be nil when the
|
||||
/// conflict policy is `IGNORE`.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type,
|
||||
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
|
||||
/// primary key does not match any row in the database.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+
|
||||
public func updateAndFetch(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil)
|
||||
throws -> Self?
|
||||
where Self: FetchableRecord
|
||||
{
|
||||
try updateAndFetch(db, onConflict: conflictResolution, as: Self.self)
|
||||
}
|
||||
|
||||
/// Executes an `UPDATE RETURNING` statement on all columns, and returns a
|
||||
/// new record built from the updated row.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - parameter returnedType: The type of the returned record.
|
||||
/// - returns: A record of type `returnedType`. The result can be nil when
|
||||
/// the conflict policy is `IGNORE`.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type,
|
||||
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
|
||||
/// primary key does not match any row in the database.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+
|
||||
public func updateAndFetch<T: FetchableRecord & TableRecord>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
||||
as returnedType: T.Type)
|
||||
throws -> T?
|
||||
{
|
||||
try updateAndFetch(db, onConflict: conflictResolution, selection: T.databaseSelection) {
|
||||
try T.fetchOne($0)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: GRDB7 make it unable to return an optional
|
||||
/// Modifies the record according to the provided `modify` closure, and
|
||||
/// executes an `UPDATE RETURNING` statement that updates the modified
|
||||
/// columns, if and only if the record was modified. The method returns a
|
||||
/// new record built from the updated row.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - parameter modify: A closure that modifies the record.
|
||||
/// - returns: An updated record, or nil if the record has no change, or
|
||||
/// in case of a failed update due to the `IGNORE` conflict policy.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type,
|
||||
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
|
||||
/// primary key does not match any row in the database.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+
|
||||
public mutating func updateChangesAndFetch(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
||||
modify: (inout Self) throws -> Void)
|
||||
throws -> Self?
|
||||
where Self: FetchableRecord
|
||||
{
|
||||
try updateChangesAndFetch(db, onConflict: conflictResolution, as: Self.self, modify: modify)
|
||||
}
|
||||
|
||||
// TODO: GRDB7 make it unable to return an optional
|
||||
/// Modifies the record according to the provided `modify` closure, and
|
||||
/// executes an `UPDATE RETURNING` statement that updates the modified
|
||||
/// columns, if and only if the record was modified. The method returns a
|
||||
/// new record built from the updated row.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - parameter returnedType: The type of the returned record.
|
||||
/// - parameter modify: A closure that modifies the record.
|
||||
/// - returns: A record of type `returnedType`, or nil if the record has
|
||||
/// no change, or in case of a failed update due to the `IGNORE`
|
||||
/// conflict policy.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type,
|
||||
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
|
||||
/// primary key does not match any row in the database.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+
|
||||
public mutating func updateChangesAndFetch<T: FetchableRecord & TableRecord>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
||||
as returnedType: T.Type,
|
||||
modify: (inout Self) throws -> Void)
|
||||
throws -> T?
|
||||
{
|
||||
try updateChangesAndFetch(
|
||||
db, onConflict: conflictResolution,
|
||||
selection: T.databaseSelection,
|
||||
fetch: { try T.fetchOne($0) },
|
||||
modify: modify)
|
||||
}
|
||||
|
||||
/// Executes an `UPDATE RETURNING` statement on the provided columns, and
|
||||
/// returns the selected columns from the updated row.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.write { db in
|
||||
/// // UPDATE player SET score = ... RETURNING totalScore
|
||||
/// let totalScore = try player.updateAndFetch(
|
||||
/// db, columns: ["Score"],
|
||||
/// selection: [Column("totalScore")],
|
||||
/// fetch: { statement in
|
||||
/// try Int.fetchOne(statement)
|
||||
/// })
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - parameter columns: The columns to update.
|
||||
/// - parameter selection: The returned columns (must not be empty).
|
||||
/// - parameter fetch: A function that executes it ``Statement`` argument.
|
||||
/// - returns: The result of the `fetch` function.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type,
|
||||
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
|
||||
/// primary key does not match any row in the database.
|
||||
/// - precondition: `selection` is not empty.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+
|
||||
public func updateAndFetch<T, Columns>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
||||
columns: Columns,
|
||||
selection: [any SQLSelectable],
|
||||
fetch: (Statement) throws -> T)
|
||||
throws -> T
|
||||
where Columns: Sequence, Columns.Element == String
|
||||
{
|
||||
GRDBPrecondition(!selection.isEmpty, "Invalid empty selection")
|
||||
|
||||
try willSave(db)
|
||||
|
||||
var success: (updated: PersistenceSuccess, returned: T)?
|
||||
try aroundSave(db) {
|
||||
success = try updateAndFetchWithCallbacks(
|
||||
db, onConflict: conflictResolution,
|
||||
columns: Set(columns),
|
||||
selection: selection,
|
||||
fetch: fetch)
|
||||
return success!.updated
|
||||
}
|
||||
|
||||
guard let success else {
|
||||
try persistenceCallbackMisuse("aroundSave")
|
||||
}
|
||||
didSave(success.updated)
|
||||
return success.returned
|
||||
}
|
||||
|
||||
/// Executes an `UPDATE RETURNING` statement on the provided columns, and
|
||||
/// returns the selected columns from the updated row.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.write { db in
|
||||
/// // UPDATE player SET score = ... RETURNING totalScore
|
||||
/// let totalScore = try player.updateAndFetch(
|
||||
/// db, columns: [Column("Score")],
|
||||
/// selection: [Column("totalScore")],
|
||||
/// fetch: { statement in
|
||||
/// try Int.fetchOne(statement)
|
||||
/// })
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - parameter columns: The columns to update.
|
||||
/// - parameter selection: The returned columns (must not be empty).
|
||||
/// - parameter fetch: A function that executes it ``Statement`` argument.
|
||||
/// - returns: The result of the `fetch` function.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type,
|
||||
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
|
||||
/// primary key does not match any row in the database.
|
||||
/// - precondition: `selection` is not empty.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+
|
||||
public func updateAndFetch<T, Columns>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
||||
columns: Columns,
|
||||
selection: [any SQLSelectable],
|
||||
fetch: (Statement) throws -> T)
|
||||
throws -> T
|
||||
where Columns: Sequence, Columns.Element: ColumnExpression
|
||||
{
|
||||
try updateAndFetch(
|
||||
db, onConflict: conflictResolution,
|
||||
columns: columns.map(\.name),
|
||||
selection: selection,
|
||||
fetch: fetch)
|
||||
}
|
||||
|
||||
/// Executes an `UPDATE RETURNING` statement on all columns, and returns the
|
||||
/// selected columns from the updated row.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.write { db in
|
||||
/// // UPDATE player SET ... RETURNING totalScore
|
||||
/// let totalScore = try player.updateAndFetch(db, selection: [Column("totalScore")]) { statement in
|
||||
/// try Int.fetchOne(statement)
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - parameter selection: The returned columns (must not be empty).
|
||||
/// - parameter fetch: A function that executes it ``Statement`` argument.
|
||||
/// - returns: The result of the `fetch` function.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type,
|
||||
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
|
||||
/// primary key does not match any row in the database.
|
||||
/// - precondition: `selection` is not empty.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+
|
||||
public func updateAndFetch<T>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
||||
selection: [any SQLSelectable],
|
||||
fetch: (Statement) throws -> T)
|
||||
throws -> T
|
||||
{
|
||||
let databaseTableName = type(of: self).databaseTableName
|
||||
let columns = try db.columns(in: databaseTableName).map(\.name)
|
||||
return try updateAndFetch(
|
||||
db, onConflict: conflictResolution,
|
||||
columns: columns,
|
||||
selection: selection,
|
||||
fetch: fetch)
|
||||
}
|
||||
|
||||
// TODO: GRDB7 make it unable to return an optional
|
||||
/// Modifies the record according to the provided `modify` closure, and
|
||||
/// executes an `UPDATE RETURNING` statement that updates the modified
|
||||
/// columns, if and only if the record was modified. The method returns a
|
||||
/// new record built from the updated row.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - parameter selection: The returned columns (must not be empty).
|
||||
/// - parameter fetch: A function that executes it ``Statement`` argument.
|
||||
/// - parameter modify: A closure that modifies the record.
|
||||
/// - returns: The result of the `fetch` function.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type,
|
||||
/// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the
|
||||
/// primary key does not match any row in the database.
|
||||
/// - precondition: `selection` is not empty.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+
|
||||
public mutating func updateChangesAndFetch<T>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
||||
selection: [any SQLSelectable],
|
||||
fetch: (Statement) throws -> T?,
|
||||
modify: (inout Self) throws -> Void)
|
||||
throws -> T?
|
||||
{
|
||||
let container = try PersistenceContainer(db, self)
|
||||
try modify(&self)
|
||||
return try updateChangesAndFetch(
|
||||
db, onConflict: conflictResolution,
|
||||
from: container,
|
||||
selection: selection,
|
||||
fetch: fetch)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: - Internals
|
||||
|
||||
extension MutablePersistableRecord {
|
||||
#if GRDBCUSTOMSQLITE || GRDBCIPHER
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
func updateChangesAndFetch<T>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution?,
|
||||
from container: PersistenceContainer,
|
||||
selection: [any SQLSelectable],
|
||||
fetch: (Statement) throws -> T?)
|
||||
throws -> T?
|
||||
{
|
||||
let changes = try PersistenceContainer(db, self).changesIterator(from: container)
|
||||
let changedColumns: Set<String> = changes.reduce(into: []) { $0.insert($1.0) }
|
||||
if changedColumns.isEmpty {
|
||||
return nil
|
||||
}
|
||||
return try updateAndFetch(
|
||||
db, onConflict: conflictResolution,
|
||||
columns: changedColumns,
|
||||
selection: selection,
|
||||
fetch: fetch)
|
||||
}
|
||||
#else
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+
|
||||
func updateChangesAndFetch<T>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution?,
|
||||
from container: PersistenceContainer,
|
||||
selection: [any SQLSelectable],
|
||||
fetch: (Statement) throws -> T?)
|
||||
throws -> T?
|
||||
{
|
||||
let changes = try PersistenceContainer(db, self).changesIterator(from: container)
|
||||
let changedColumns: Set<String> = changes.reduce(into: []) { $0.insert($1.0) }
|
||||
if changedColumns.isEmpty {
|
||||
return nil
|
||||
}
|
||||
return try updateAndFetch(
|
||||
db, onConflict: conflictResolution,
|
||||
columns: changedColumns,
|
||||
selection: selection,
|
||||
fetch: fetch)
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Executes an `UPDATE` statement, and runs update callbacks.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
func updateWithCallbacks(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution?,
|
||||
columns: Set<String>)
|
||||
throws -> PersistenceSuccess
|
||||
{
|
||||
let (updated, _) = try updateAndFetchWithCallbacks(
|
||||
db, onConflict: conflictResolution,
|
||||
columns: columns,
|
||||
selection: [],
|
||||
fetch: {
|
||||
// Nothing to fetch
|
||||
try $0.execute()
|
||||
})
|
||||
return updated
|
||||
}
|
||||
|
||||
/// Executes an `UPDATE` statement, with `RETURNING` clause if `selection`
|
||||
/// is not empty, and runs update callbacks.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
func updateAndFetchWithCallbacks<T>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution?,
|
||||
columns: Set<String>,
|
||||
selection: [any SQLSelectable],
|
||||
fetch: (Statement) throws -> T)
|
||||
throws -> (PersistenceSuccess, T)
|
||||
{
|
||||
try willUpdate(db, columns: columns)
|
||||
|
||||
var success: (updated: PersistenceSuccess, returned: T)?
|
||||
try aroundUpdate(db, columns: columns) {
|
||||
success = try updateAndFetchWithoutCallbacks(
|
||||
db, onConflict: conflictResolution,
|
||||
columns: columns,
|
||||
selection: selection,
|
||||
fetch: fetch)
|
||||
return success!.updated
|
||||
}
|
||||
|
||||
guard let success else {
|
||||
try persistenceCallbackMisuse("aroundUpdate")
|
||||
}
|
||||
didUpdate(success.updated)
|
||||
return success
|
||||
}
|
||||
|
||||
/// Executes an `UPDATE` statement, with `RETURNING` clause if `selection`
|
||||
/// is not empty, and DOES NOT run update callbacks.
|
||||
@usableFromInline
|
||||
func updateAndFetchWithoutCallbacks<T>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution?,
|
||||
columns: Set<String>,
|
||||
selection: [any SQLSelectable],
|
||||
fetch: (Statement) throws -> T)
|
||||
throws -> (PersistenceSuccess, T)
|
||||
{
|
||||
let conflictResolution = conflictResolution ?? type(of: self)
|
||||
.persistenceConflictPolicy
|
||||
.conflictResolutionForUpdate
|
||||
let dao = try DAO(db, self)
|
||||
guard let statement = try dao.updateStatement(
|
||||
columns: columns,
|
||||
onConflict: conflictResolution,
|
||||
returning: selection)
|
||||
else {
|
||||
// Nil primary key
|
||||
try dao.recordNotFound()
|
||||
}
|
||||
let returned = try fetch(statement)
|
||||
if db.changesCount == 0 {
|
||||
// No row was updated
|
||||
try dao.recordNotFound()
|
||||
}
|
||||
let updated = PersistenceSuccess(persistenceContainer: dao.persistenceContainer)
|
||||
return (updated, returned)
|
||||
}
|
||||
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
func updateChanges(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution?,
|
||||
from container: PersistenceContainer)
|
||||
throws -> Bool
|
||||
{
|
||||
let changes = try PersistenceContainer(db, self).changesIterator(from: container)
|
||||
let changedColumns: Set<String> = changes.reduce(into: []) { $0.insert($1.0) }
|
||||
if changedColumns.isEmpty {
|
||||
return false
|
||||
}
|
||||
try update(db, onConflict: conflictResolution, columns: changedColumns)
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,464 @@
|
||||
// MARK: - Upsert
|
||||
|
||||
extension MutablePersistableRecord {
|
||||
#if GRDBCUSTOMSQLITE || GRDBCIPHER
|
||||
/// Executes an `INSERT ON CONFLICT DO UPDATE` statement.
|
||||
///
|
||||
/// The upsert behavior is triggered by a violation of any uniqueness
|
||||
/// constraint on the table (primary key or unique index). In case of
|
||||
/// violation, all columns but the primary key are overwritten with the
|
||||
/// inserted values.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: Encodable, MutablePersistableRecord {
|
||||
/// var id: Int64
|
||||
/// var name: String
|
||||
/// var score: Int
|
||||
/// }
|
||||
///
|
||||
/// // INSERT INTO player (id, name, score)
|
||||
/// // VALUES (1, 'Arthur', 1000)
|
||||
/// // ON CONFLICT DO UPDATE SET
|
||||
/// // name = excluded.name,
|
||||
/// // score = excluded.score
|
||||
/// var player = Player(id: 1, name: "Arthur", score: 1000)
|
||||
/// try player.upsert(db)
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
public mutating func upsert(_ db: Database) throws {
|
||||
try willSave(db)
|
||||
|
||||
var saved: PersistenceSuccess?
|
||||
try aroundSave(db) {
|
||||
let inserted = try upsertWithCallbacks(db)
|
||||
saved = PersistenceSuccess(inserted)
|
||||
return saved!
|
||||
}
|
||||
|
||||
guard let saved else {
|
||||
try persistenceCallbackMisuse("aroundSave")
|
||||
}
|
||||
didSave(saved)
|
||||
}
|
||||
|
||||
/// Executes an `INSERT ON CONFLICT DO UPDATE RETURNING` statement, and
|
||||
/// returns the upserted record.
|
||||
///
|
||||
/// With default parameters (`upsertAndFetch(db)`), the upsert behavior is
|
||||
/// triggered by a violation of any uniqueness constraint on the table
|
||||
/// (primary key or unique index). In case of violation, all columns but the
|
||||
/// primary key are overwritten with the inserted values:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: Encodable, MutablePersistableRecord {
|
||||
/// var id: Int64
|
||||
/// var name: String
|
||||
/// var score: Int
|
||||
/// }
|
||||
///
|
||||
/// // INSERT INTO player (id, name, score)
|
||||
/// // VALUES (1, 'Arthur', 1000)
|
||||
/// // ON CONFLICT DO UPDATE SET
|
||||
/// // name = excluded.name,
|
||||
/// // score = excluded.score
|
||||
/// // RETURNING *
|
||||
/// var player = Player(id: 1, name: "Arthur", score: 1000)
|
||||
/// let upsertedPlayer = try player.upsertAndFetch(db)
|
||||
/// ```
|
||||
///
|
||||
/// With `conflictTarget` and `assignments` arguments, you can further
|
||||
/// control the upsert behavior. Make sure you check
|
||||
/// <https://www.sqlite.org/lang_UPSERT.html> for detailed information.
|
||||
///
|
||||
/// The conflict target are the columns of the uniqueness constraint
|
||||
/// (primary key or unique index) that triggers the upsert. If empty, all
|
||||
/// uniqueness constraint are considered.
|
||||
///
|
||||
/// The assignments describe how to update columns in case of violation of
|
||||
/// a uniqueness constraint. In the next example, we insert the new
|
||||
/// vocabulary word "jovial" if that word is not already in the dictionary.
|
||||
/// If the word is already in the dictionary, it increments the counter,
|
||||
/// does not overwrite the tainted flag, and overwrites the
|
||||
/// remaining columns:
|
||||
///
|
||||
/// ```swift
|
||||
/// // CREATE TABLE vocabulary(
|
||||
/// // word TEXT PRIMARY KEY,
|
||||
/// // kind TEXT NOT NULL,
|
||||
/// // isTainted BOOLEAN DEFAULT 0,
|
||||
/// // count INT DEFAULT 1))
|
||||
/// struct Vocabulary: Encodable, MutablePersistableRecord {
|
||||
/// var word: String
|
||||
/// var kind: String
|
||||
/// var isTainted: Bool
|
||||
/// }
|
||||
///
|
||||
/// // INSERT INTO vocabulary(word, kind, isTainted)
|
||||
/// // VALUES('jovial', 'adjective', 0)
|
||||
/// // ON CONFLICT(word) DO UPDATE SET \
|
||||
/// // count = count + 1,
|
||||
/// // kind = excluded.kind
|
||||
/// // RETURNING *
|
||||
/// var vocabulary = Vocabulary(word: "jovial", kind: "adjective", isTainted: false)
|
||||
/// let upserted = try vocabulary.upsertAndFetch(
|
||||
/// db,
|
||||
/// onConflict: ["word"],
|
||||
/// doUpdate: { _ in
|
||||
/// [Column("count") += 1,
|
||||
/// Column("isTainted").noOverwrite]
|
||||
/// })
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictTarget: The conflict target.
|
||||
/// - parameter assignments: An optional function that returns an array of
|
||||
/// ``ColumnAssignment``. In case of violation of a uniqueness
|
||||
/// constraints, these assignments are performed, and remaining columns
|
||||
/// are overwritten by inserted values.
|
||||
/// - returns: The upserted record.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
public mutating func upsertAndFetch(
|
||||
_ db: Database,
|
||||
onConflict conflictTarget: [String] = [],
|
||||
doUpdate assignments: ((_ excluded: TableAlias) -> [ColumnAssignment])? = nil)
|
||||
throws -> Self
|
||||
where Self: FetchableRecord
|
||||
{
|
||||
try upsertAndFetch(db, as: Self.self, onConflict: conflictTarget, doUpdate: assignments)
|
||||
}
|
||||
|
||||
/// Executes an `INSERT ON CONFLICT DO UPDATE RETURNING` statement, and
|
||||
/// returns the upserted record.
|
||||
///
|
||||
/// See `upsertAndFetch(_:onConflict:doUpdate:)` for more information about
|
||||
/// the `conflictTarget` and `assignments` parameters.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter returnedType: The type of the returned record.
|
||||
/// - parameter conflictTarget: The conflict target.
|
||||
/// - parameter assignments: An optional function that returns an array of
|
||||
/// ``ColumnAssignment``. In case of violation of a uniqueness
|
||||
/// constraints, these assignments are performed, and remaining columns
|
||||
/// are overwritten by inserted values.
|
||||
/// - returns: A record of type `returnedType`.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
public mutating func upsertAndFetch<T: FetchableRecord & TableRecord>(
|
||||
_ db: Database,
|
||||
as returnedType: T.Type,
|
||||
onConflict conflictTarget: [String] = [],
|
||||
doUpdate assignments: ((_ excluded: TableAlias) -> [ColumnAssignment])? = nil)
|
||||
throws -> T
|
||||
{
|
||||
try willSave(db)
|
||||
|
||||
var success: (inserted: InsertionSuccess, returned: T)?
|
||||
try aroundSave(db) {
|
||||
success = try upsertAndFetchWithCallbacks(
|
||||
db, onConflict: conflictTarget,
|
||||
doUpdate: assignments,
|
||||
selection: T.databaseSelection,
|
||||
decode: { try T(row: $0) })
|
||||
return PersistenceSuccess(success!.inserted)
|
||||
}
|
||||
|
||||
guard let success else {
|
||||
try persistenceCallbackMisuse("aroundSave")
|
||||
}
|
||||
didSave(PersistenceSuccess(success.inserted))
|
||||
return success.returned
|
||||
}
|
||||
#else
|
||||
/// Executes an `INSERT ON CONFLICT DO UPDATE` statement.
|
||||
///
|
||||
/// The upsert behavior is triggered by a violation of any uniqueness
|
||||
/// constraint on the table (primary key or unique index). In case of
|
||||
/// violation, all columns but the primary key are overwritten with the
|
||||
/// inserted values.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: Encodable, MutablePersistableRecord {
|
||||
/// var id: Int64
|
||||
/// var name: String
|
||||
/// var score: Int
|
||||
/// }
|
||||
///
|
||||
/// // INSERT INTO player (id, name, score)
|
||||
/// // VALUES (1, 'Arthur', 1000)
|
||||
/// // ON CONFLICT DO UPDATE SET
|
||||
/// // name = excluded.name,
|
||||
/// // score = excluded.score
|
||||
/// var player = Player(id: 1, name: "Arthur", score: 1000)
|
||||
/// try player.upsert(db)
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+
|
||||
public mutating func upsert(_ db: Database) throws {
|
||||
try willSave(db)
|
||||
|
||||
var saved: PersistenceSuccess?
|
||||
try aroundSave(db) {
|
||||
let inserted = try upsertWithCallbacks(db)
|
||||
saved = PersistenceSuccess(inserted)
|
||||
return saved!
|
||||
}
|
||||
|
||||
guard let saved else {
|
||||
try persistenceCallbackMisuse("aroundSave")
|
||||
}
|
||||
didSave(saved)
|
||||
}
|
||||
|
||||
/// Executes an `INSERT ON CONFLICT DO UPDATE RETURNING` statement, and
|
||||
/// returns the upserted record.
|
||||
///
|
||||
/// With default parameters (`upsertAndFetch(db)`), the upsert behavior is
|
||||
/// triggered by a violation of any uniqueness constraint on the table
|
||||
/// (primary key or unique index). In case of violation, all columns but the
|
||||
/// primary key are overwritten with the inserted values:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: Encodable, MutablePersistableRecord {
|
||||
/// var id: Int64
|
||||
/// var name: String
|
||||
/// var score: Int
|
||||
/// }
|
||||
///
|
||||
/// // INSERT INTO player (id, name, score)
|
||||
/// // VALUES (1, 'Arthur', 1000)
|
||||
/// // ON CONFLICT DO UPDATE SET
|
||||
/// // name = excluded.name,
|
||||
/// // score = excluded.score
|
||||
/// // RETURNING *
|
||||
/// var player = Player(id: 1, name: "Arthur", score: 1000)
|
||||
/// let upsertedPlayer = try player.upsertAndFetch(db)
|
||||
/// ```
|
||||
///
|
||||
/// With `conflictTarget` and `assignments` arguments, you can further
|
||||
/// control the upsert behavior. Make sure you check
|
||||
/// <https://www.sqlite.org/lang_UPSERT.html> for detailed information.
|
||||
///
|
||||
/// The conflict target are the columns of the uniqueness constraint
|
||||
/// (primary key or unique index) that triggers the upsert. If empty, all
|
||||
/// uniqueness constraint are considered.
|
||||
///
|
||||
/// The assignments describe how to update columns in case of violation of
|
||||
/// a uniqueness constraint. In the next example, we insert the new
|
||||
/// vocabulary word "jovial" if that word is not already in the dictionary.
|
||||
/// If the word is already in the dictionary, it increments the counter,
|
||||
/// does not overwrite the tainted flag, and overwrites the
|
||||
/// remaining columns:
|
||||
///
|
||||
/// ```swift
|
||||
/// // CREATE TABLE vocabulary(
|
||||
/// // word TEXT PRIMARY KEY,
|
||||
/// // kind TEXT NOT NULL,
|
||||
/// // isTainted BOOLEAN DEFAULT 0,
|
||||
/// // count INT DEFAULT 1))
|
||||
/// struct Vocabulary: Encodable, MutablePersistableRecord {
|
||||
/// var word: String
|
||||
/// var kind: String
|
||||
/// var isTainted: Bool
|
||||
/// }
|
||||
///
|
||||
/// // INSERT INTO vocabulary(word, kind, isTainted)
|
||||
/// // VALUES('jovial', 'adjective', 0)
|
||||
/// // ON CONFLICT(word) DO UPDATE SET \
|
||||
/// // count = count + 1,
|
||||
/// // kind = excluded.kind
|
||||
/// // RETURNING *
|
||||
/// var vocabulary = Vocabulary(word: "jovial", kind: "adjective", isTainted: false)
|
||||
/// let upserted = try vocabulary.upsertAndFetch(
|
||||
/// db,
|
||||
/// onConflict: ["word"],
|
||||
/// doUpdate: { _ in
|
||||
/// [Column("count") += 1,
|
||||
/// Column("isTainted").noOverwrite]
|
||||
/// })
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictTarget: The conflict target.
|
||||
/// - parameter assignments: An optional function that returns an array of
|
||||
/// ``ColumnAssignment``. In case of violation of a uniqueness
|
||||
/// constraints, these assignments are performed, and remaining columns
|
||||
/// are overwritten by inserted values.
|
||||
/// - returns: The upserted record.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+
|
||||
public mutating func upsertAndFetch(
|
||||
_ db: Database,
|
||||
onConflict conflictTarget: [String] = [],
|
||||
doUpdate assignments: ((_ excluded: TableAlias) -> [ColumnAssignment])? = nil)
|
||||
throws -> Self
|
||||
where Self: FetchableRecord
|
||||
{
|
||||
try upsertAndFetch(db, as: Self.self, onConflict: conflictTarget, doUpdate: assignments)
|
||||
}
|
||||
|
||||
/// Executes an `INSERT ON CONFLICT DO UPDATE RETURNING` statement, and
|
||||
/// returns the upserted record.
|
||||
///
|
||||
/// See ``upsertAndFetch(_:onConflict:doUpdate:)`` for more information
|
||||
/// about the `conflictTarget` and `assignments` parameters.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter returnedType: The type of the returned record.
|
||||
/// - parameter conflictTarget: The conflict target.
|
||||
/// - parameter assignments: An optional function that returns an array of
|
||||
/// ``ColumnAssignment``. In case of violation of a uniqueness
|
||||
/// constraints, these assignments are performed, and remaining columns
|
||||
/// are overwritten by inserted values.
|
||||
/// - returns: A record of type `returnedType`.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+
|
||||
public mutating func upsertAndFetch<T: FetchableRecord & TableRecord>(
|
||||
_ db: Database,
|
||||
as returnedType: T.Type,
|
||||
onConflict conflictTarget: [String] = [],
|
||||
doUpdate assignments: ((_ excluded: TableAlias) -> [ColumnAssignment])? = nil)
|
||||
throws -> T
|
||||
{
|
||||
try willSave(db)
|
||||
|
||||
var success: (inserted: InsertionSuccess, returned: T)?
|
||||
try aroundSave(db) {
|
||||
success = try upsertAndFetchWithCallbacks(
|
||||
db, onConflict: conflictTarget,
|
||||
doUpdate: assignments,
|
||||
selection: T.databaseSelection,
|
||||
decode: { try T(row: $0) })
|
||||
return PersistenceSuccess(success!.inserted)
|
||||
}
|
||||
|
||||
guard let success else {
|
||||
try persistenceCallbackMisuse("aroundSave")
|
||||
}
|
||||
didSave(PersistenceSuccess(success.inserted))
|
||||
return success.returned
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: - Internal
|
||||
|
||||
extension MutablePersistableRecord {
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
mutating func upsertWithCallbacks(_ db: Database)
|
||||
throws -> InsertionSuccess
|
||||
{
|
||||
let (inserted, _) = try upsertAndFetchWithCallbacks(
|
||||
db, onConflict: [],
|
||||
doUpdate: nil,
|
||||
selection: [],
|
||||
decode: { _ in /* Nothing to decode */ })
|
||||
return inserted
|
||||
}
|
||||
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
mutating func upsertAndFetchWithCallbacks<T>(
|
||||
_ db: Database,
|
||||
onConflict conflictTarget: [String],
|
||||
doUpdate assignments: ((_ excluded: TableAlias) -> [ColumnAssignment])?,
|
||||
selection: [any SQLSelectable],
|
||||
decode: (Row) throws -> T)
|
||||
throws -> (InsertionSuccess, T)
|
||||
{
|
||||
try willInsert(db)
|
||||
|
||||
var success: (inserted: InsertionSuccess, returned: T)?
|
||||
try aroundInsert(db) {
|
||||
success = try upsertAndFetchWithoutCallbacks(
|
||||
db, onConflict: conflictTarget,
|
||||
doUpdate: assignments,
|
||||
selection: selection,
|
||||
decode: decode)
|
||||
return success!.inserted
|
||||
}
|
||||
|
||||
guard let success else {
|
||||
try persistenceCallbackMisuse("aroundInsert")
|
||||
}
|
||||
didInsert(success.inserted)
|
||||
return success
|
||||
}
|
||||
|
||||
/// Executes an `INSERT RETURNING` statement, and DOES NOT run
|
||||
/// insertion callbacks.
|
||||
@usableFromInline
|
||||
func upsertAndFetchWithoutCallbacks<T>(
|
||||
_ db: Database,
|
||||
onConflict conflictTarget: [String],
|
||||
doUpdate assignments: ((_ excluded: TableAlias) -> [ColumnAssignment])?,
|
||||
selection: [any SQLSelectable],
|
||||
decode: (Row) throws -> T)
|
||||
throws -> (InsertionSuccess, T)
|
||||
{
|
||||
// Append the rowID to the returned columns
|
||||
let selection = selection + [Column.rowID]
|
||||
|
||||
let dao = try DAO(db, self)
|
||||
let statement = try dao.upsertStatement(
|
||||
db,
|
||||
onConflict: conflictTarget,
|
||||
doUpdate: assignments,
|
||||
updateCondition: nil,
|
||||
returning: selection)
|
||||
let cursor = try Row.fetchCursor(statement)
|
||||
|
||||
// Keep cursor alive until we can process the fetched row
|
||||
let (rowid, returned): (Int64, T) = try withExtendedLifetime(cursor) { cursor in
|
||||
guard let row = try cursor.next() else {
|
||||
throw DatabaseError(message: "Insertion failed")
|
||||
}
|
||||
|
||||
// Rowid is the last column
|
||||
let rowid: Int64 = row[row.count - 1]
|
||||
let returned = try decode(row)
|
||||
|
||||
// Now that we have fetched the values we need, we could stop
|
||||
// there. But let's make sure we fully consume the cursor
|
||||
// anyway, until SQLITE_DONE. This is necessary, for example,
|
||||
// for upserts in tables that are synchronized with an
|
||||
// FTS5 table.
|
||||
// See <https://github.com/groue/GRDB.swift/issues/1390>
|
||||
while try cursor.next() != nil { }
|
||||
|
||||
return (rowid, returned)
|
||||
}
|
||||
|
||||
// Update the persistenceContainer with the inserted rowid.
|
||||
// This allows the Record class to set its `hasDatabaseChanges` property
|
||||
// to false in its `aroundInsert` callback.
|
||||
var persistenceContainer = dao.persistenceContainer
|
||||
let rowIDColumn = dao.primaryKey.rowIDColumn
|
||||
if let rowIDColumn {
|
||||
persistenceContainer[caseInsensitive: rowIDColumn] = rowid
|
||||
}
|
||||
|
||||
let inserted = InsertionSuccess(
|
||||
rowID: rowid,
|
||||
rowIDColumn: rowIDColumn,
|
||||
persistenceContainer: persistenceContainer)
|
||||
return (inserted, returned)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
/// A type that can be persisted in the database, and mutates on insertion.
|
||||
///
|
||||
/// ## Overview
|
||||
///
|
||||
/// A `MutablePersistableRecord` instance mutates on insertion. This protocol
|
||||
/// is suited for record types that are a `struct`, and target a database table
|
||||
/// where ids are generated on insertion. Such records implement the
|
||||
/// ``didInsert(_:)-109jm`` callback in order to grab this id. For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// // CREATE TABLE player (
|
||||
/// // id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
/// // name TEXT NOT NULL,
|
||||
/// // score INTEGER NOT NULL
|
||||
/// // )
|
||||
/// struct Player: Encodable {
|
||||
/// var id: Int64?
|
||||
/// var name: String
|
||||
/// var score: Int
|
||||
/// }
|
||||
///
|
||||
/// extension Player: MutablePersistableRecord {
|
||||
/// mutating func didInsert(_ inserted: InsertionSuccess) {
|
||||
/// id = inserted.rowID
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// try dbQueue.write { db in
|
||||
/// var player = Player(id: nil, name:: "Arthur", score: 1000)
|
||||
/// try player.insert(db)
|
||||
/// print(player.id) // Some id that is not nil
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Other record types (classes, and generally records that do not mutate on
|
||||
/// insertion) should prefer the ``PersistableRecord`` protocol instead.
|
||||
///
|
||||
/// ## Conforming to the MutablePersistableRecord Protocol
|
||||
///
|
||||
/// To conform to `MutablePersistableRecord`, provide an implementation for the
|
||||
/// ``EncodableRecord/encode(to:)-k9pf`` method. This implementation is
|
||||
/// ready-made for `Encodable` types.
|
||||
///
|
||||
/// You configure the database table where records are persisted with the
|
||||
/// ``TableRecord`` inherited protocol.
|
||||
///
|
||||
/// ## Topics
|
||||
///
|
||||
/// ### Testing if a Record Exists in the Database
|
||||
///
|
||||
/// - ``exists(_:)``
|
||||
///
|
||||
/// ### Inserting a Record
|
||||
///
|
||||
/// - ``insert(_:onConflict:)``
|
||||
/// - ``inserted(_:onConflict:)``
|
||||
/// - ``upsert(_:)``
|
||||
///
|
||||
/// ### Inserting a Record and Fetching the Inserted Row
|
||||
///
|
||||
/// - ``insertAndFetch(_:onConflict:)``
|
||||
/// - ``insertAndFetch(_:onConflict:as:)``
|
||||
/// - ``insertAndFetch(_:onConflict:selection:fetch:)``
|
||||
/// - ``upsertAndFetch(_:onConflict:doUpdate:)``
|
||||
/// - ``upsertAndFetch(_:as:onConflict:doUpdate:)``
|
||||
///
|
||||
/// ### Updating a Record
|
||||
///
|
||||
/// See inherited ``TableRecord`` methods for batch updates.
|
||||
///
|
||||
/// - ``update(_:onConflict:)``
|
||||
/// - ``update(_:onConflict:columns:)-4foo1``
|
||||
/// - ``update(_:onConflict:columns:)-5hxyx``
|
||||
/// - ``updateChanges(_:onConflict:from:)``
|
||||
/// - ``updateChanges(_:onConflict:modify:)``
|
||||
///
|
||||
/// ### Updating a Record and Fetching the Updated Row
|
||||
///
|
||||
/// - ``updateAndFetch(_:onConflict:)``
|
||||
/// - ``updateAndFetch(_:onConflict:as:)``
|
||||
/// - ``updateAndFetch(_:onConflict:columns:selection:fetch:)-7s7y1``
|
||||
/// - ``updateAndFetch(_:onConflict:columns:selection:fetch:)-30d2v``
|
||||
/// - ``updateAndFetch(_:onConflict:selection:fetch:)``
|
||||
/// - ``updateChangesAndFetch(_:onConflict:modify:)``
|
||||
/// - ``updateChangesAndFetch(_:onConflict:as:modify:)``
|
||||
/// - ``updateChangesAndFetch(_:onConflict:selection:fetch:modify:)``
|
||||
///
|
||||
/// ### Saving a Record
|
||||
///
|
||||
/// - ``save(_:onConflict:)``
|
||||
/// - ``saved(_:onConflict:)``
|
||||
///
|
||||
/// ### Saving a Record and Fetching the Saved Row
|
||||
///
|
||||
/// - ``saveAndFetch(_:onConflict:)``
|
||||
/// - ``saveAndFetch(_:onConflict:as:)``
|
||||
/// - ``saveAndFetch(_:onConflict:selection:fetch:)``
|
||||
///
|
||||
/// ### Deleting a Record
|
||||
///
|
||||
/// See inherited ``TableRecord`` methods for batch deletes.
|
||||
///
|
||||
/// - ``delete(_:)``
|
||||
///
|
||||
/// ### Persistence Callbacks
|
||||
///
|
||||
/// - ``willDelete(_:)-7rmqk``
|
||||
/// - ``willInsert(_:)-1xfwo``
|
||||
/// - ``willSave(_:)-6jitc``
|
||||
/// - ``willUpdate(_:columns:)-3oko4``
|
||||
/// - ``didDelete(deleted:)-7sq9c``
|
||||
/// - ``didInsert(_:)-109jm``
|
||||
/// - ``didSave(_:)-177yz``
|
||||
/// - ``didUpdate(_:)-1oql8``
|
||||
/// - ``aroundDelete(_:delete:)-8w9ei``
|
||||
/// - ``aroundInsert(_:insert:)-67r8o``
|
||||
/// - ``aroundSave(_:save:)-5o9jz``
|
||||
/// - ``aroundUpdate(_:columns:update:)-ka41``
|
||||
/// - ``InsertionSuccess``
|
||||
/// - ``PersistenceSuccess``
|
||||
///
|
||||
/// ### Configuring Persistence
|
||||
///
|
||||
/// - ``persistenceConflictPolicy-1isyv``
|
||||
/// - ``PersistenceConflictPolicy``
|
||||
public protocol MutablePersistableRecord: EncodableRecord, TableRecord {
|
||||
/// The policy that handles SQLite conflicts when records are inserted
|
||||
/// or updated.
|
||||
///
|
||||
/// The default implementation uses the ABORT policy for both insertions and
|
||||
/// updates, and has GRDB generate regular INSERT and UPDATE queries.
|
||||
///
|
||||
/// See <https://www.sqlite.org/lang_conflict.html>
|
||||
static var persistenceConflictPolicy: PersistenceConflictPolicy { get }
|
||||
|
||||
// MARK: Insertion Callbacks
|
||||
|
||||
/// Persistence callback called before the record is inserted.
|
||||
///
|
||||
/// Default implementation does nothing.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
mutating func willInsert(_ db: Database) throws
|
||||
|
||||
/// Persistence callback called around the record insertion.
|
||||
///
|
||||
/// If you provide a custom implementation of this method, you must call
|
||||
/// the `insert` parameter at some point in your implementation, and you
|
||||
/// must rethrow its eventual error.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: MutablePersistableRecord {
|
||||
/// func aroundInsert(_ db: Database, insert: () throws -> InsertionSuccess) throws {
|
||||
/// print("Player will insert")
|
||||
/// _ = try insert()
|
||||
/// print("Player did insert")
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter insert: A function that inserts the record, and returns
|
||||
/// information about the inserted row.
|
||||
func aroundInsert(_ db: Database, insert: () throws -> InsertionSuccess) throws
|
||||
|
||||
/// Persistence callback called upon successful insertion.
|
||||
///
|
||||
/// The default implementation does nothing.
|
||||
///
|
||||
/// You can provide a custom implementation in order to grab the
|
||||
/// auto-incremented id:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: MutablePersistableRecord {
|
||||
/// var id: Int64?
|
||||
/// var name: String
|
||||
///
|
||||
/// mutating func didInsert(_ inserted: InsertionSuccess) {
|
||||
/// id = inserted.rowID
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter inserted: Information about the inserted row.
|
||||
mutating func didInsert(_ inserted: InsertionSuccess)
|
||||
|
||||
// MARK: Update Callbacks
|
||||
|
||||
/// Persistence callback called before the record is updated.
|
||||
///
|
||||
/// Default implementation does nothing.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
func willUpdate(_ db: Database, columns: Set<String>) throws
|
||||
|
||||
/// Persistence callback called around the record update.
|
||||
///
|
||||
/// If you provide a custom implementation of this method, you must call
|
||||
/// the `update` parameter at some point in your implementation, and you
|
||||
/// must rethrow its eventual error.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: MutablePersistableRecord {
|
||||
/// func aroundUpdate(_ db: Database, columns: Set<String>, update: () throws -> PersistenceSuccess) throws {
|
||||
/// print("Player will update")
|
||||
/// _ = try update()
|
||||
/// print("Player did update")
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter columns: The updated columns.
|
||||
/// - parameter update: A function that updates the record. Its result is
|
||||
/// reserved for GRDB usage.
|
||||
func aroundUpdate(_ db: Database, columns: Set<String>, update: () throws -> PersistenceSuccess) throws
|
||||
|
||||
/// Persistence callback called upon successful update.
|
||||
///
|
||||
/// Default implementation does nothing.
|
||||
///
|
||||
/// - parameter updated: Reserved for GRDB usage.
|
||||
func didUpdate(_ updated: PersistenceSuccess)
|
||||
|
||||
// MARK: Save Callbacks
|
||||
|
||||
/// Persistence callback called before the record is updated or inserted.
|
||||
///
|
||||
/// Default implementation does nothing.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
func willSave(_ db: Database) throws
|
||||
|
||||
/// Persistence callback called around the record update or insertion.
|
||||
///
|
||||
/// If you provide a custom implementation of this method, you must call
|
||||
/// the `save` parameter at some point in your implementation, and you
|
||||
/// must rethrow its eventual error.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: MutablePersistableRecord {
|
||||
/// func aroundSave(_ db: Database, save: () throws -> PersistenceSuccess) throws {
|
||||
/// print("Player will save")
|
||||
/// _ = try save()
|
||||
/// print("Player did save")
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter update: A function that updates the record. Its result is
|
||||
/// reserved for GRDB usage.
|
||||
func aroundSave(_ db: Database, save: () throws -> PersistenceSuccess) throws
|
||||
|
||||
/// Persistence callback called upon successful update or insertion.
|
||||
///
|
||||
/// Default implementation does nothing.
|
||||
///
|
||||
/// - parameter saved: Reserved for GRDB usage.
|
||||
func didSave(_ saved: PersistenceSuccess)
|
||||
|
||||
// MARK: Deletion Callbacks
|
||||
|
||||
/// Persistence callback called before the record is deleted.
|
||||
///
|
||||
/// Default implementation does nothing.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
func willDelete(_ db: Database) throws
|
||||
|
||||
/// Persistence callback called around the destruction of the record.
|
||||
///
|
||||
/// If you provide a custom implementation of this method, you must call
|
||||
/// the `delete` parameter at some point in your implementation, and you
|
||||
/// must rethrow its eventual error.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: MutablePersistableRecord {
|
||||
/// func aroundDelete(_ db: Database, delete: () throws -> Bool) throws {
|
||||
/// print("Player will delete")
|
||||
/// _ = try delete()
|
||||
/// print("Player did delete")
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter delete: A function that deletes the record and returns
|
||||
/// whether a row was deleted in the database.
|
||||
func aroundDelete(_ db: Database, delete: () throws -> Bool) throws
|
||||
|
||||
/// Persistence callback called upon successful deletion.
|
||||
///
|
||||
/// Default implementation does nothing.
|
||||
///
|
||||
/// - parameter deleted: Whether a row was deleted in the database.
|
||||
func didDelete(deleted: Bool)
|
||||
}
|
||||
|
||||
extension MutablePersistableRecord {
|
||||
public static var persistenceConflictPolicy: PersistenceConflictPolicy {
|
||||
PersistenceConflictPolicy(insert: .abort, update: .abort)
|
||||
}
|
||||
|
||||
/// Call for programmer errors from the `aroundXxx` callbacks.
|
||||
@usableFromInline
|
||||
func persistenceCallbackMisuse(_ callbackName: String) throws -> Never {
|
||||
let message = """
|
||||
Incorrect implementation of the `\(Self.self).\(callbackName)` persistence callback: \
|
||||
the action function was not called, or its error was not rethrown.
|
||||
"""
|
||||
// This is a programmer error, but we must not crash, because it can
|
||||
// only be detected in case of database errors, which happen
|
||||
// infrequently. That's why we gently throw SQLITE_MISUSE.
|
||||
throw DatabaseError(
|
||||
resultCode: .SQLITE_MISUSE,
|
||||
message: message)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Existence Check
|
||||
|
||||
extension MutablePersistableRecord {
|
||||
/// Returns whether the primary key of the record matches a row in
|
||||
/// the database.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public func exists(_ db: Database) throws -> Bool {
|
||||
guard let statement = try DAO(db, self).existsStatement() else {
|
||||
// Nil primary key
|
||||
return false
|
||||
}
|
||||
return try Bool.fetchOne(statement)!
|
||||
}
|
||||
}
|
||||
|
||||
/// The `MutablePersistableRecord` protocol uses this type in order to handle
|
||||
/// SQLite conflicts when records are inserted or updated.
|
||||
///
|
||||
/// See `MutablePersistableRecord.persistenceConflictPolicy`.
|
||||
///
|
||||
/// See <https://www.sqlite.org/lang_conflict.html>
|
||||
public struct PersistenceConflictPolicy: Sendable {
|
||||
/// The conflict resolution algorithm for insertions
|
||||
public let conflictResolutionForInsert: Database.ConflictResolution
|
||||
|
||||
/// The conflict resolution algorithm for updates
|
||||
public let conflictResolutionForUpdate: Database.ConflictResolution
|
||||
|
||||
/// Creates a policy
|
||||
public init(insert: Database.ConflictResolution = .abort, update: Database.ConflictResolution = .abort) {
|
||||
self.conflictResolutionForInsert = insert
|
||||
self.conflictResolutionForUpdate = update
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of a successful record insertion.
|
||||
///
|
||||
/// `InsertionSuccess` gives the auto-incremented id after a successful
|
||||
/// record insertion. For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: Encodable, MutablePersistableRecord {
|
||||
/// var id: Int64?
|
||||
/// var name: String
|
||||
///
|
||||
/// mutating func didInsert(_ inserted: InsertionSuccess) {
|
||||
/// id = inserted.rowID
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// try dbQueue.write { db in
|
||||
/// var player = Player(id: nil, name: "Alice")
|
||||
/// try player.insert(db)
|
||||
/// print(player.id) // The inserted id
|
||||
/// }
|
||||
/// ```
|
||||
public struct InsertionSuccess {
|
||||
/// The rowid of the inserted record.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: Encodable, MutablePersistableRecord {
|
||||
/// var id: Int64?
|
||||
/// var name: String
|
||||
///
|
||||
/// mutating func didInsert(_ inserted: InsertionSuccess) {
|
||||
/// id = inserted.rowID
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// To learn about rowids, see <https://www.sqlite.org/lang_createtable.html#rowids_and_the_integer_primary_key>.
|
||||
public var rowID: Int64
|
||||
|
||||
/// The name of the eventual INTEGER PRIMARY KEY column.
|
||||
public var rowIDColumn: String?
|
||||
|
||||
// Used by the Record class in order to manage its `hasDatabaseChanges` flag.
|
||||
/// The persistence container that was inserted.
|
||||
///
|
||||
/// If the database table has a rowid column, the persistence container
|
||||
/// contains the rowid of the inserted record.
|
||||
public var persistenceContainer: PersistenceContainer
|
||||
}
|
||||
|
||||
/// The result of a successful record persistence (insert or update).
|
||||
public struct PersistenceSuccess {
|
||||
// Used by the Record class in order to manage its `hasDatabaseChanges` flag.
|
||||
/// The persistence container that was saved.
|
||||
///
|
||||
/// After an insert, and if the database table has a rowid column, the
|
||||
/// persistence container contains the rowid of the inserted record.
|
||||
public var persistenceContainer: PersistenceContainer
|
||||
|
||||
init(persistenceContainer: PersistenceContainer) {
|
||||
self.persistenceContainer = persistenceContainer
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
init(_ inserted: InsertionSuccess) {
|
||||
self.init(persistenceContainer: inserted.persistenceContainer)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
// MARK: - Insert Callbacks
|
||||
|
||||
extension PersistableRecord {
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public func willInsert(_ db: Database) throws { }
|
||||
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public func didInsert(_ inserted: InsertionSuccess) { }
|
||||
}
|
||||
|
||||
// MARK: - Insert
|
||||
|
||||
extension PersistableRecord {
|
||||
/// Executes an `INSERT` statement.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.write { db in
|
||||
/// let player = Player(name: "Arthur")
|
||||
/// try player.insert(db)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
public func insert(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil)
|
||||
throws
|
||||
{
|
||||
try willSave(db)
|
||||
|
||||
var saved: PersistenceSuccess?
|
||||
try aroundSave(db) {
|
||||
let inserted = try insertWithCallbacks(db, onConflict: conflictResolution)
|
||||
saved = PersistenceSuccess(inserted)
|
||||
return saved!
|
||||
}
|
||||
|
||||
guard let saved else {
|
||||
try persistenceCallbackMisuse("aroundSave")
|
||||
}
|
||||
didSave(saved)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Insert and Fetch
|
||||
|
||||
extension PersistableRecord {
|
||||
#if GRDBCUSTOMSQLITE || GRDBCIPHER
|
||||
// TODO: GRDB7 make it unable to return an optional
|
||||
/// Executes an `INSERT RETURNING` statement, and returns a new record built
|
||||
/// from the inserted row.
|
||||
///
|
||||
/// This method helps dealing with default column values and
|
||||
/// generated columns.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// // A table with an auto-incremented primary key and a default value
|
||||
/// try dbQueue.write { db in
|
||||
/// try db.execute(sql: """
|
||||
/// CREATE TABLE player(
|
||||
/// id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
/// name TEXT,
|
||||
/// score INTEGER DEFAULT 1000)
|
||||
/// """)
|
||||
/// }
|
||||
///
|
||||
/// // A player with partial database information
|
||||
/// struct PartialPlayer: PersistableRecord {
|
||||
/// static let databaseTableName = "player"
|
||||
/// var name: String
|
||||
/// }
|
||||
///
|
||||
/// // A full player, with all database information
|
||||
/// struct Player: TableRecord, FetchableRecord {
|
||||
/// var id: Int64
|
||||
/// var name: String
|
||||
/// var score: Int
|
||||
/// }
|
||||
///
|
||||
/// // Insert a partial player, get a full one
|
||||
/// try dbQueue.write { db in
|
||||
/// let partialPlayer = PartialPlayer(name: "Alice")
|
||||
///
|
||||
/// // INSERT INTO player (name) VALUES ('Alice') RETURNING *
|
||||
/// if let player = try partialPlayer.insertAndFetch(db, as: FullPlayer.self) {
|
||||
/// print(player.id) // The inserted id
|
||||
/// print(player.name) // The inserted name
|
||||
/// print(player.score) // The default score
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - parameter returnedType: The type of the returned record.
|
||||
/// - returns: A record of type `returnedType`, if any. The result can be
|
||||
/// nil when the conflict policy is `IGNORE`.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
public func insertAndFetch<T: FetchableRecord & TableRecord>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
||||
as returnedType: T.Type)
|
||||
throws -> T?
|
||||
{
|
||||
try insertAndFetch(db, onConflict: conflictResolution, selection: T.databaseSelection) {
|
||||
try T.fetchOne($0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes an `INSERT RETURNING` statement, and returns the selected
|
||||
/// columns from the inserted row.
|
||||
///
|
||||
/// This method helps dealing with default column values and
|
||||
/// generated columns.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// // A table with an auto-incremented primary key and a default value
|
||||
/// try dbQueue.write { db in
|
||||
/// try db.execute(sql: """
|
||||
/// CREATE TABLE player(
|
||||
/// id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
/// name TEXT,
|
||||
/// score INTEGER DEFAULT 1000)
|
||||
/// """)
|
||||
/// }
|
||||
///
|
||||
/// // A player with partial database information
|
||||
/// struct PartialPlayer: PersistableRecord {
|
||||
/// static let databaseTableName = "player"
|
||||
/// var name: String
|
||||
/// }
|
||||
///
|
||||
/// // Insert a partial player, get the inserted score
|
||||
/// try dbQueue.write { db in
|
||||
/// let partialPlayer = PartialPlayer(name: "Alice")
|
||||
///
|
||||
/// // INSERT INTO player (name) VALUES ('Alice') RETURNING score
|
||||
/// let score = try partialPlayer.insertAndFetch(db, selection: [Column("score")]) { statement in
|
||||
/// try Int.fetchOne(statement)
|
||||
/// }
|
||||
/// print(score) // The inserted score
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - parameter selection: The returned columns (must not be empty).
|
||||
/// - parameter fetch: A closure that executes its ``Statement`` argument.
|
||||
/// If the conflict policy is `IGNORE`, the statement may return no row.
|
||||
/// - returns: The result of the `fetch` function.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type.
|
||||
/// - precondition: `selection` is not empty.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
public func insertAndFetch<T>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
||||
selection: [any SQLSelectable],
|
||||
fetch: (Statement) throws -> T)
|
||||
throws -> T
|
||||
{
|
||||
GRDBPrecondition(!selection.isEmpty, "Invalid empty selection")
|
||||
|
||||
try willSave(db)
|
||||
|
||||
var success: (inserted: InsertionSuccess, returned: T)?
|
||||
try aroundSave(db) {
|
||||
success = try insertAndFetchWithCallbacks(
|
||||
db, onConflict: conflictResolution,
|
||||
selection: selection,
|
||||
fetch: fetch)
|
||||
return PersistenceSuccess(success!.inserted)
|
||||
}
|
||||
|
||||
guard let success else {
|
||||
try persistenceCallbackMisuse("aroundSave")
|
||||
}
|
||||
didSave(PersistenceSuccess(success.inserted))
|
||||
return success.returned
|
||||
}
|
||||
#else
|
||||
// TODO: GRDB7 make it unable to return an optional
|
||||
/// Executes an `INSERT RETURNING` statement, and returns a new record built
|
||||
/// from the inserted row.
|
||||
///
|
||||
/// This method helps dealing with default column values and
|
||||
/// generated columns.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// // A table with an auto-incremented primary key and a default value
|
||||
/// try dbQueue.write { db in
|
||||
/// try db.execute(sql: """
|
||||
/// CREATE TABLE player(
|
||||
/// id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
/// name TEXT,
|
||||
/// score INTEGER DEFAULT 1000)
|
||||
/// """)
|
||||
/// }
|
||||
///
|
||||
/// // A player with partial database information
|
||||
/// struct PartialPlayer: PersistableRecord {
|
||||
/// static let databaseTableName = "player"
|
||||
/// var name: String
|
||||
/// }
|
||||
///
|
||||
/// // A full player, with all database information
|
||||
/// struct Player: TableRecord, FetchableRecord {
|
||||
/// var id: Int64
|
||||
/// var name: String
|
||||
/// var score: Int
|
||||
/// }
|
||||
///
|
||||
/// // Insert a partial player, get a full one
|
||||
/// try dbQueue.write { db in
|
||||
/// let partialPlayer = PartialPlayer(name: "Alice")
|
||||
///
|
||||
/// // INSERT INTO player (name) VALUES ('Alice') RETURNING *
|
||||
/// if let player = try partialPlayer.insertAndFetch(db, as: FullPlayer.self) {
|
||||
/// print(player.id) // The inserted id
|
||||
/// print(player.name) // The inserted name
|
||||
/// print(player.score) // The default score
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - parameter returnedType: The type of the returned record.
|
||||
/// - returns: A record of type `returnedType`, if any. The result can be
|
||||
/// nil when the conflict policy is `IGNORE`.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+
|
||||
public func insertAndFetch<T: FetchableRecord & TableRecord>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
||||
as returnedType: T.Type)
|
||||
throws -> T?
|
||||
{
|
||||
try insertAndFetch(db, onConflict: conflictResolution, selection: T.databaseSelection) {
|
||||
try T.fetchOne($0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes an `INSERT RETURNING` statement, and returns the selected
|
||||
/// columns from the inserted row.
|
||||
///
|
||||
/// This method helps dealing with default column values and
|
||||
/// generated columns.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// // A table with an auto-incremented primary key and a default value
|
||||
/// try dbQueue.write { db in
|
||||
/// try db.execute(sql: """
|
||||
/// CREATE TABLE player(
|
||||
/// id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
/// name TEXT,
|
||||
/// score INTEGER DEFAULT 1000)
|
||||
/// """)
|
||||
/// }
|
||||
///
|
||||
/// // A player with partial database information
|
||||
/// struct PartialPlayer: PersistableRecord {
|
||||
/// static let databaseTableName = "player"
|
||||
/// var name: String
|
||||
/// }
|
||||
///
|
||||
/// // Insert a partial player, get the inserted score
|
||||
/// try dbQueue.write { db in
|
||||
/// let partialPlayer = PartialPlayer(name: "Alice")
|
||||
///
|
||||
/// // INSERT INTO player (name) VALUES ('Alice') RETURNING score
|
||||
/// let score = try partialPlayer.insertAndFetch(db, selection: [Column("score")]) { statement in
|
||||
/// try Int.fetchOne(statement)
|
||||
/// }
|
||||
/// print(score) // The inserted score
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - parameter selection: The returned columns (must not be empty).
|
||||
/// - parameter fetch: A closure that executes its ``Statement`` argument.
|
||||
/// If the conflict policy is `IGNORE`, the statement may return no row.
|
||||
/// - returns: The result of the `fetch` function.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type.
|
||||
/// - precondition: `selection` is not empty.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+
|
||||
public func insertAndFetch<T>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
||||
selection: [any SQLSelectable],
|
||||
fetch: (Statement) throws -> T)
|
||||
throws -> T
|
||||
{
|
||||
GRDBPrecondition(!selection.isEmpty, "Invalid empty selection")
|
||||
|
||||
try willSave(db)
|
||||
|
||||
var success: (inserted: InsertionSuccess, returned: T)?
|
||||
try aroundSave(db) {
|
||||
success = try insertAndFetchWithCallbacks(
|
||||
db, onConflict: conflictResolution,
|
||||
selection: selection,
|
||||
fetch: fetch)
|
||||
return PersistenceSuccess(success!.inserted)
|
||||
}
|
||||
|
||||
guard let success else {
|
||||
try persistenceCallbackMisuse("aroundSave")
|
||||
}
|
||||
didSave(PersistenceSuccess(success.inserted))
|
||||
return success.returned
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: - Internals
|
||||
|
||||
extension PersistableRecord {
|
||||
/// Executes an `INSERT` statement, and runs insertion callbacks.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
func insertWithCallbacks(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution?)
|
||||
throws -> InsertionSuccess
|
||||
{
|
||||
let (inserted, _) = try insertAndFetchWithCallbacks(db, onConflict: conflictResolution, selection: []) {
|
||||
// Nothing to fetch
|
||||
try $0.execute()
|
||||
}
|
||||
return inserted
|
||||
}
|
||||
|
||||
/// Executes an `INSERT` statement, with `RETURNING` clause if `selection`
|
||||
/// is not empty, and runs insertion callbacks.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
func insertAndFetchWithCallbacks<T>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution?,
|
||||
selection: [any SQLSelectable],
|
||||
fetch: (Statement) throws -> T)
|
||||
throws -> (InsertionSuccess, T)
|
||||
{
|
||||
try willInsert(db)
|
||||
|
||||
var success: (inserted: InsertionSuccess, returned: T)?
|
||||
try aroundInsert(db) {
|
||||
success = try insertAndFetchWithoutCallbacks(
|
||||
db, onConflict: conflictResolution,
|
||||
selection: selection,
|
||||
fetch: fetch)
|
||||
return success!.inserted
|
||||
}
|
||||
|
||||
guard let success else {
|
||||
try persistenceCallbackMisuse("aroundInsert")
|
||||
}
|
||||
didInsert(success.inserted)
|
||||
return success
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
// MARK: - Save
|
||||
|
||||
extension PersistableRecord {
|
||||
/// Executes an `INSERT` or `UPDATE` statement.
|
||||
///
|
||||
/// If the receiver has a non-nil primary key and a matching row in the
|
||||
/// database, this method performs an update.
|
||||
///
|
||||
/// Otherwise, performs an insert.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
public func save(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil)
|
||||
throws
|
||||
{
|
||||
try willSave(db)
|
||||
|
||||
var saved: PersistenceSuccess?
|
||||
try aroundSave(db) {
|
||||
saved = try updateOrInsertWithCallbacks(db, onConflict: conflictResolution)
|
||||
return saved!
|
||||
}
|
||||
|
||||
guard let saved else {
|
||||
try persistenceCallbackMisuse("aroundSave")
|
||||
}
|
||||
didSave(saved)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Save and Fetch
|
||||
|
||||
extension PersistableRecord {
|
||||
#if GRDBCUSTOMSQLITE || GRDBCIPHER
|
||||
// TODO: GRDB7 make it unable to return an optional
|
||||
/// Executes an `INSERT RETURNING` or `UPDATE RETURNING` statement, and
|
||||
/// returns a new record built from the saved row.
|
||||
///
|
||||
/// If the receiver has a non-nil primary key and a matching row in the
|
||||
/// database, this method performs an update. Otherwise, it performs
|
||||
/// an insert.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - parameter returnedType: The type of the returned record.
|
||||
/// - returns: A record of type `returnedType`. The result can be nil when
|
||||
/// the conflict policy is `IGNORE`.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
public func saveAndFetch<T: FetchableRecord & TableRecord>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
||||
as returnedType: T.Type)
|
||||
throws -> T?
|
||||
{
|
||||
try willSave(db)
|
||||
|
||||
var success: (saved: PersistenceSuccess, returned: T?)?
|
||||
try aroundSave(db) {
|
||||
success = try updateOrInsertAndFetchWithCallbacks(
|
||||
db, onConflict: conflictResolution,
|
||||
selection: T.databaseSelection,
|
||||
fetch: {
|
||||
try T.fetchOne($0)
|
||||
})
|
||||
return success!.saved
|
||||
}
|
||||
|
||||
guard let success else {
|
||||
try persistenceCallbackMisuse("aroundSave")
|
||||
}
|
||||
didSave(success.saved)
|
||||
return success.returned
|
||||
}
|
||||
|
||||
/// Executes an `INSERT RETURNING` or `UPDATE RETURNING` statement, and
|
||||
/// returns the selected columns from the saved row.
|
||||
///
|
||||
/// If the receiver has a non-nil primary key and a matching row in the
|
||||
/// database, this method performs an update. Otherwise, it performs
|
||||
/// an insert.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - parameter selection: The returned columns (must not be empty).
|
||||
/// - parameter fetch: A function that executes it ``Statement`` argument.
|
||||
/// - returns: The result of the `fetch` function.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type.
|
||||
/// - precondition: `selection` is not empty.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
public func saveAndFetch<T>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
||||
selection: [any SQLSelectable],
|
||||
fetch: (Statement) throws -> T)
|
||||
throws -> T
|
||||
{
|
||||
GRDBPrecondition(!selection.isEmpty, "Invalid empty selection")
|
||||
|
||||
try willSave(db)
|
||||
|
||||
var success: (saved: PersistenceSuccess, returned: T)?
|
||||
try aroundSave(db) {
|
||||
success = try updateOrInsertAndFetchWithCallbacks(
|
||||
db, onConflict: conflictResolution,
|
||||
selection: selection,
|
||||
fetch: fetch)
|
||||
return success!.saved
|
||||
}
|
||||
|
||||
guard let success else {
|
||||
try persistenceCallbackMisuse("aroundSave")
|
||||
}
|
||||
didSave(success.saved)
|
||||
return success.returned
|
||||
}
|
||||
#else
|
||||
// TODO: GRDB7 make it unable to return an optional
|
||||
/// Executes an `INSERT RETURNING` or `UPDATE RETURNING` statement, and
|
||||
/// returns a new record built from the saved row.
|
||||
///
|
||||
/// If the receiver has a non-nil primary key and a matching row in the
|
||||
/// database, this method performs an update. Otherwise, it performs
|
||||
/// an insert.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - parameter returnedType: The type of the returned record.
|
||||
/// - returns: A record of type `returnedType`. The result can be nil when
|
||||
/// the conflict policy is `IGNORE`.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+
|
||||
public func saveAndFetch<T: FetchableRecord & TableRecord>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
||||
as returnedType: T.Type)
|
||||
throws -> T?
|
||||
{
|
||||
try willSave(db)
|
||||
|
||||
var success: (saved: PersistenceSuccess, returned: T?)?
|
||||
try aroundSave(db) {
|
||||
success = try updateOrInsertAndFetchWithCallbacks(
|
||||
db, onConflict: conflictResolution,
|
||||
selection: T.databaseSelection,
|
||||
fetch: {
|
||||
try T.fetchOne($0)
|
||||
})
|
||||
return success!.saved
|
||||
}
|
||||
|
||||
guard let success else {
|
||||
try persistenceCallbackMisuse("aroundSave")
|
||||
}
|
||||
didSave(success.saved)
|
||||
return success.returned
|
||||
}
|
||||
|
||||
/// Executes an `INSERT RETURNING` or `UPDATE RETURNING` statement, and
|
||||
/// returns the selected columns from the saved row.
|
||||
///
|
||||
/// If the receiver has a non-nil primary key and a matching row in the
|
||||
/// database, this method performs an update. Otherwise, it performs
|
||||
/// an insert.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution. If
|
||||
/// nil, <doc:/MutablePersistableRecord/persistenceConflictPolicy-1isyv>
|
||||
/// is used.
|
||||
/// - parameter selection: The returned columns (must not be empty).
|
||||
/// - parameter fetch: A function that executes it ``Statement`` argument.
|
||||
/// - returns: The result of the `fetch` function.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type.
|
||||
/// - precondition: `selection` is not empty.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+
|
||||
public func saveAndFetch<T>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
||||
selection: [any SQLSelectable],
|
||||
fetch: (Statement) throws -> T)
|
||||
throws -> T
|
||||
{
|
||||
GRDBPrecondition(!selection.isEmpty, "Invalid empty selection")
|
||||
|
||||
try willSave(db)
|
||||
|
||||
var success: (saved: PersistenceSuccess, returned: T)?
|
||||
try aroundSave(db) {
|
||||
success = try updateOrInsertAndFetchWithCallbacks(
|
||||
db, onConflict: conflictResolution,
|
||||
selection: selection,
|
||||
fetch: fetch)
|
||||
return success!.saved
|
||||
}
|
||||
|
||||
guard let success else {
|
||||
try persistenceCallbackMisuse("aroundSave")
|
||||
}
|
||||
didSave(success.saved)
|
||||
return success.returned
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: - Internal
|
||||
|
||||
extension PersistableRecord {
|
||||
/// Executes an `UPDATE` or `INSERT` statement, and runs insertion or
|
||||
/// update callbacks.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
func updateOrInsertWithCallbacks(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution?)
|
||||
throws -> PersistenceSuccess
|
||||
{
|
||||
let (saved, _) = try updateOrInsertAndFetchWithCallbacks(
|
||||
db, onConflict: conflictResolution,
|
||||
selection: [],
|
||||
fetch: {
|
||||
// Nothing to fetch
|
||||
try $0.execute()
|
||||
})
|
||||
return saved
|
||||
}
|
||||
|
||||
/// Executes an `UPDATE` or `INSERT` statement, with `RETURNING` clause
|
||||
/// if `selection` is not empty, and runs insertion or update callbacks.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
func updateOrInsertAndFetchWithCallbacks<T>(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution?,
|
||||
selection: [any SQLSelectable],
|
||||
fetch: (Statement) throws -> T)
|
||||
throws -> (PersistenceSuccess, T)
|
||||
{
|
||||
// Attempt at updating if the record has a primary key
|
||||
if let key = try primaryKey(db) {
|
||||
do {
|
||||
let databaseTableName = type(of: self).databaseTableName
|
||||
let columns = try Set(db.columns(in: databaseTableName).map(\.name))
|
||||
return try updateAndFetchWithCallbacks(
|
||||
db, onConflict: conflictResolution,
|
||||
columns: columns,
|
||||
selection: selection,
|
||||
fetch: fetch)
|
||||
} catch RecordError.recordNotFound(databaseTableName: type(of: self).databaseTableName, key: key) {
|
||||
// No row was updated: fallback on insert.
|
||||
}
|
||||
}
|
||||
|
||||
// Insert
|
||||
let (inserted, returned) = try insertAndFetchWithCallbacks(
|
||||
db, onConflict: conflictResolution,
|
||||
selection: selection,
|
||||
fetch: fetch)
|
||||
return (PersistenceSuccess(inserted), returned)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
// MARK: - Upsert
|
||||
|
||||
extension PersistableRecord {
|
||||
#if GRDBCUSTOMSQLITE || GRDBCIPHER
|
||||
/// Executes an `INSERT ON CONFLICT DO UPDATE` statement.
|
||||
///
|
||||
/// The upsert behavior is triggered by a violation of any uniqueness
|
||||
/// constraint on the table (primary key or unique index). In case of
|
||||
/// violation, all columns but the primary key are overwritten with the
|
||||
/// inserted values.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: Encodable, PersistableRecord {
|
||||
/// var id: Int64
|
||||
/// var name: String
|
||||
/// var score: Int
|
||||
/// }
|
||||
///
|
||||
/// // INSERT INTO player (id, name, score)
|
||||
/// // VALUES (1, 'Arthur', 1000)
|
||||
/// // ON CONFLICT DO UPDATE SET
|
||||
/// // name = excluded.name,
|
||||
/// // score = excluded.score
|
||||
/// let player = Player(id: 1, name: "Arthur", score: 1000)
|
||||
/// try player.upsert(db)
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
public func upsert(_ db: Database) throws {
|
||||
try willSave(db)
|
||||
|
||||
var saved: PersistenceSuccess?
|
||||
try aroundSave(db) {
|
||||
let inserted = try upsertWithCallbacks(db)
|
||||
saved = PersistenceSuccess(inserted)
|
||||
return saved!
|
||||
}
|
||||
|
||||
guard let saved else {
|
||||
try persistenceCallbackMisuse("aroundSave")
|
||||
}
|
||||
didSave(saved)
|
||||
}
|
||||
|
||||
/// Executes an `INSERT ON CONFLICT DO UPDATE RETURNING` statement, and
|
||||
/// returns the upserted record.
|
||||
///
|
||||
/// With default parameters (`upsertAndFetch(db)`), the upsert behavior is
|
||||
/// triggered by a violation of any uniqueness constraint on the table
|
||||
/// (primary key or unique index). In case of violation, all columns but the
|
||||
/// primary key are overwritten with the inserted values:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: Encodable, PersistableRecord {
|
||||
/// var id: Int64
|
||||
/// var name: String
|
||||
/// var score: Int
|
||||
/// }
|
||||
///
|
||||
/// // INSERT INTO player (id, name, score)
|
||||
/// // VALUES (1, 'Arthur', 1000)
|
||||
/// // ON CONFLICT DO UPDATE SET
|
||||
/// // name = excluded.name,
|
||||
/// // score = excluded.score
|
||||
/// // RETURNING *
|
||||
/// let player = Player(id: 1, name: "Arthur", score: 1000)
|
||||
/// let upsertedPlayer = try player.upsertAndFetch(db)
|
||||
/// ```
|
||||
///
|
||||
/// With `conflictTarget` and `assignments` arguments, you can further
|
||||
/// control the upsert behavior. Make sure you check
|
||||
/// <https://www.sqlite.org/lang_UPSERT.html> for detailed information.
|
||||
///
|
||||
/// The conflict target are the columns of the uniqueness constraint
|
||||
/// (primary key or unique index) that triggers the upsert. If empty, all
|
||||
/// uniqueness constraint are considered.
|
||||
///
|
||||
/// The assignments describe how to update columns in case of violation of
|
||||
/// a uniqueness constraint. In the next example, we insert the new
|
||||
/// vocabulary word "jovial" if that word is not already in the dictionary.
|
||||
/// If the word is already in the dictionary, it increments the counter,
|
||||
/// does not overwrite the tainted flag, and overwrites the
|
||||
/// remaining columns:
|
||||
///
|
||||
/// ```swift
|
||||
/// // CREATE TABLE vocabulary(
|
||||
/// // word TEXT PRIMARY KEY,
|
||||
/// // kind TEXT NOT NULL,
|
||||
/// // isTainted BOOLEAN DEFAULT 0,
|
||||
/// // count INT DEFAULT 1))
|
||||
/// struct Vocabulary: Encodable, PersistableRecord {
|
||||
/// var word: String
|
||||
/// var kind: String
|
||||
/// var isTainted: Bool
|
||||
/// }
|
||||
///
|
||||
/// // INSERT INTO vocabulary(word, kind, isTainted)
|
||||
/// // VALUES('jovial', 'adjective', 0)
|
||||
/// // ON CONFLICT(word) DO UPDATE SET \
|
||||
/// // count = count + 1,
|
||||
/// // kind = excluded.kind
|
||||
/// // RETURNING *
|
||||
/// let vocabulary = Vocabulary(word: "jovial", kind: "adjective", isTainted: false)
|
||||
/// let upserted = try vocabulary.upsertAndFetch(
|
||||
/// db,
|
||||
/// onConflict: ["word"],
|
||||
/// doUpdate: { _ in
|
||||
/// [Column("count") += 1,
|
||||
/// Column("isTainted").noOverwrite]
|
||||
/// })
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictTarget: The conflict target.
|
||||
/// - parameter assignments: An optional function that returns an array of
|
||||
/// ``ColumnAssignment``. In case of violation of a uniqueness
|
||||
/// constraints, these assignments are performed, and remaining columns
|
||||
/// are overwritten by inserted values.
|
||||
/// - returns: The upserted record.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
public func upsertAndFetch(
|
||||
_ db: Database,
|
||||
onConflict conflictTarget: [String] = [],
|
||||
doUpdate assignments: ((_ excluded: TableAlias) -> [ColumnAssignment])? = nil)
|
||||
throws -> Self
|
||||
where Self: FetchableRecord
|
||||
{
|
||||
try upsertAndFetch(db, as: Self.self, onConflict: conflictTarget, doUpdate: assignments)
|
||||
}
|
||||
|
||||
/// Executes an `INSERT ON CONFLICT DO UPDATE RETURNING` statement, and
|
||||
/// returns the upserted record.
|
||||
///
|
||||
/// See `upsertAndFetch(_:onConflict:doUpdate:)` for more information about
|
||||
/// the `conflictTarget` and `assignments` parameters.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter returnedType: The type of the returned record.
|
||||
/// - parameter conflictTarget: The conflict target.
|
||||
/// - parameter assignments: An optional function that returns an array of
|
||||
/// ``ColumnAssignment``. In case of violation of a uniqueness
|
||||
/// constraints, these assignments are performed, and remaining columns
|
||||
/// are overwritten by inserted values.
|
||||
/// - returns: A record of type `returnedType`.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
public func upsertAndFetch<T: FetchableRecord & TableRecord>(
|
||||
_ db: Database,
|
||||
as returnedType: T.Type,
|
||||
onConflict conflictTarget: [String] = [],
|
||||
doUpdate assignments: ((_ excluded: TableAlias) -> [ColumnAssignment])? = nil)
|
||||
throws -> T
|
||||
{
|
||||
try willSave(db)
|
||||
|
||||
var success: (inserted: InsertionSuccess, returned: T)?
|
||||
try aroundSave(db) {
|
||||
success = try upsertAndFetchWithCallbacks(
|
||||
db, onConflict: conflictTarget,
|
||||
doUpdate: assignments,
|
||||
selection: T.databaseSelection,
|
||||
decode: { try T(row: $0) })
|
||||
return PersistenceSuccess(success!.inserted)
|
||||
}
|
||||
|
||||
guard let success else {
|
||||
try persistenceCallbackMisuse("aroundSave")
|
||||
}
|
||||
didSave(PersistenceSuccess(success.inserted))
|
||||
return success.returned
|
||||
}
|
||||
#else
|
||||
/// Executes an `INSERT ON CONFLICT DO UPDATE` statement.
|
||||
///
|
||||
/// The upsert behavior is triggered by a violation of any uniqueness
|
||||
/// constraint on the table (primary key or unique index). In case of
|
||||
/// violation, all columns but the primary key are overwritten with the
|
||||
/// inserted values.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: Encodable, PersistableRecord {
|
||||
/// var id: Int64
|
||||
/// var name: String
|
||||
/// var score: Int
|
||||
/// }
|
||||
///
|
||||
/// // INSERT INTO player (id, name, score)
|
||||
/// // VALUES (1, 'Arthur', 1000)
|
||||
/// // ON CONFLICT DO UPDATE SET
|
||||
/// // name = excluded.name,
|
||||
/// // score = excluded.score
|
||||
/// let player = Player(id: 1, name: "Arthur", score: 1000)
|
||||
/// try player.upsert(db)
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+
|
||||
public func upsert(_ db: Database) throws {
|
||||
try willSave(db)
|
||||
|
||||
var saved: PersistenceSuccess?
|
||||
try aroundSave(db) {
|
||||
let inserted = try upsertWithCallbacks(db)
|
||||
saved = PersistenceSuccess(inserted)
|
||||
return saved!
|
||||
}
|
||||
|
||||
guard let saved else {
|
||||
try persistenceCallbackMisuse("aroundSave")
|
||||
}
|
||||
didSave(saved)
|
||||
}
|
||||
|
||||
/// Executes an `INSERT ON CONFLICT DO UPDATE RETURNING` statement, and
|
||||
/// returns the upserted record.
|
||||
///
|
||||
/// With default parameters (`upsertAndFetch(db)`), the upsert behavior is
|
||||
/// triggered by a violation of any uniqueness constraint on the table
|
||||
/// (primary key or unique index). In case of violation, all columns but the
|
||||
/// primary key are overwritten with the inserted values:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: Encodable, PersistableRecord {
|
||||
/// var id: Int64
|
||||
/// var name: String
|
||||
/// var score: Int
|
||||
/// }
|
||||
///
|
||||
/// // INSERT INTO player (id, name, score)
|
||||
/// // VALUES (1, 'Arthur', 1000)
|
||||
/// // ON CONFLICT DO UPDATE SET
|
||||
/// // name = excluded.name,
|
||||
/// // score = excluded.score
|
||||
/// // RETURNING *
|
||||
/// let player = Player(id: 1, name: "Arthur", score: 1000)
|
||||
/// let upsertedPlayer = try player.upsertAndFetch(db)
|
||||
/// ```
|
||||
///
|
||||
/// With `conflictTarget` and `assignments` arguments, you can further
|
||||
/// control the upsert behavior. Make sure you check
|
||||
/// <https://www.sqlite.org/lang_UPSERT.html> for detailed information.
|
||||
///
|
||||
/// The conflict target are the columns of the uniqueness constraint
|
||||
/// (primary key or unique index) that triggers the upsert. If empty, all
|
||||
/// uniqueness constraint are considered.
|
||||
///
|
||||
/// The assignments describe how to update columns in case of violation of
|
||||
/// a uniqueness constraint. In the next example, we insert the new
|
||||
/// vocabulary word "jovial" if that word is not already in the dictionary.
|
||||
/// If the word is already in the dictionary, it increments the counter,
|
||||
/// does not overwrite the tainted flag, and overwrites the
|
||||
/// remaining columns:
|
||||
///
|
||||
/// ```swift
|
||||
/// // CREATE TABLE vocabulary(
|
||||
/// // word TEXT PRIMARY KEY,
|
||||
/// // kind TEXT NOT NULL,
|
||||
/// // isTainted BOOLEAN DEFAULT 0,
|
||||
/// // count INT DEFAULT 1))
|
||||
/// struct Vocabulary: Encodable, PersistableRecord {
|
||||
/// var word: String
|
||||
/// var kind: String
|
||||
/// var isTainted: Bool
|
||||
/// }
|
||||
///
|
||||
/// // INSERT INTO vocabulary(word, kind, isTainted)
|
||||
/// // VALUES('jovial', 'adjective', 0)
|
||||
/// // ON CONFLICT(word) DO UPDATE SET \
|
||||
/// // count = count + 1,
|
||||
/// // kind = excluded.kind
|
||||
/// // RETURNING *
|
||||
/// let vocabulary = Vocabulary(word: "jovial", kind: "adjective", isTainted: false)
|
||||
/// let upserted = try vocabulary.upsertAndFetch(
|
||||
/// db,
|
||||
/// onConflict: ["word"],
|
||||
/// doUpdate: { _ in
|
||||
/// [Column("count") += 1,
|
||||
/// Column("isTainted").noOverwrite]
|
||||
/// })
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictTarget: The conflict target.
|
||||
/// - parameter assignments: An optional function that returns an array of
|
||||
/// ``ColumnAssignment``. In case of violation of a uniqueness
|
||||
/// constraints, these assignments are performed, and remaining columns
|
||||
/// are overwritten by inserted values.
|
||||
/// - returns: The upserted record.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+
|
||||
public func upsertAndFetch(
|
||||
_ db: Database,
|
||||
onConflict conflictTarget: [String] = [],
|
||||
doUpdate assignments: ((_ excluded: TableAlias) -> [ColumnAssignment])? = nil)
|
||||
throws -> Self
|
||||
where Self: FetchableRecord
|
||||
{
|
||||
try upsertAndFetch(db, as: Self.self, onConflict: conflictTarget, doUpdate: assignments)
|
||||
}
|
||||
|
||||
/// Executes an `INSERT ON CONFLICT DO UPDATE RETURNING` statement, and
|
||||
/// returns the upserted record.
|
||||
///
|
||||
/// See ``upsertAndFetch(_:onConflict:doUpdate:)`` for more information
|
||||
/// about the `conflictTarget` and `assignments` parameters.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter returnedType: The type of the returned record.
|
||||
/// - parameter conflictTarget: The conflict target.
|
||||
/// - parameter assignments: An optional function that returns an array of
|
||||
/// ``ColumnAssignment``. In case of violation of a uniqueness
|
||||
/// constraints, these assignments are performed, and remaining columns
|
||||
/// are overwritten by inserted values.
|
||||
/// - returns: A record of type `returnedType`.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
|
||||
/// error thrown by the persistence callbacks defined by the record type.
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+
|
||||
public func upsertAndFetch<T: FetchableRecord & TableRecord>(
|
||||
_ db: Database,
|
||||
as returnedType: T.Type,
|
||||
onConflict conflictTarget: [String] = [],
|
||||
doUpdate assignments: ((_ excluded: TableAlias) -> [ColumnAssignment])? = nil)
|
||||
throws -> T
|
||||
{
|
||||
try willSave(db)
|
||||
|
||||
var success: (inserted: InsertionSuccess, returned: T)?
|
||||
try aroundSave(db) {
|
||||
success = try upsertAndFetchWithCallbacks(
|
||||
db, onConflict: conflictTarget,
|
||||
doUpdate: assignments,
|
||||
selection: T.databaseSelection,
|
||||
decode: { try T(row: $0) })
|
||||
return PersistenceSuccess(success!.inserted)
|
||||
}
|
||||
|
||||
guard let success else {
|
||||
try persistenceCallbackMisuse("aroundSave")
|
||||
}
|
||||
didSave(PersistenceSuccess(success.inserted))
|
||||
return success.returned
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: - Internal
|
||||
|
||||
extension PersistableRecord {
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
func upsertWithCallbacks(_ db: Database)
|
||||
throws -> InsertionSuccess
|
||||
{
|
||||
let (inserted, _) = try upsertAndFetchWithCallbacks(
|
||||
db, onConflict: [],
|
||||
doUpdate: nil,
|
||||
selection: [],
|
||||
decode: { _ in /* Nothing to decode */ })
|
||||
return inserted
|
||||
}
|
||||
|
||||
@inlinable // allow specialization so that empty callbacks are removed
|
||||
func upsertAndFetchWithCallbacks<T>(
|
||||
_ db: Database,
|
||||
onConflict conflictTarget: [String],
|
||||
doUpdate assignments: ((_ excluded: TableAlias) -> [ColumnAssignment])?,
|
||||
selection: [any SQLSelectable],
|
||||
decode: (Row) throws -> T)
|
||||
throws -> (InsertionSuccess, T)
|
||||
{
|
||||
try willInsert(db)
|
||||
|
||||
var success: (inserted: InsertionSuccess, returned: T)?
|
||||
try aroundInsert(db) {
|
||||
success = try upsertAndFetchWithoutCallbacks(
|
||||
db, onConflict: conflictTarget,
|
||||
doUpdate: assignments,
|
||||
selection: selection,
|
||||
decode: decode)
|
||||
return success!.inserted
|
||||
}
|
||||
|
||||
guard let success else {
|
||||
try persistenceCallbackMisuse("aroundInsert")
|
||||
}
|
||||
didInsert(success.inserted)
|
||||
return success
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/// A type that can be persisted in the database.
|
||||
///
|
||||
/// ``PersistableRecord`` has non-mutating variants of
|
||||
/// ``MutablePersistableRecord`` methods.
|
||||
///
|
||||
/// ## Conforming to the PersistableRecord Protocol
|
||||
///
|
||||
/// To conform to `PersistableRecord`, provide an implementation for the
|
||||
/// ``EncodableRecord/encode(to:)-k9pf`` method. This implementation is
|
||||
/// ready-made for `Encodable` types.
|
||||
///
|
||||
/// You configure the database table where records are persisted with the
|
||||
/// ``TableRecord`` inherited protocol.
|
||||
///
|
||||
/// ## Topics
|
||||
///
|
||||
/// ### Inserting a Record
|
||||
///
|
||||
/// - ``insert(_:onConflict:)``
|
||||
/// - ``upsert(_:)``
|
||||
///
|
||||
/// ### Inserting a Record and Fetching the Inserted Row
|
||||
///
|
||||
/// - ``insertAndFetch(_:onConflict:as:)``
|
||||
/// - ``insertAndFetch(_:onConflict:selection:fetch:)``
|
||||
/// - ``upsertAndFetch(_:onConflict:doUpdate:)``
|
||||
/// - ``upsertAndFetch(_:as:onConflict:doUpdate:)``
|
||||
///
|
||||
/// ### Saving a Record
|
||||
///
|
||||
/// - ``save(_:onConflict:)``
|
||||
///
|
||||
/// ### Saving a Record and Fetching the Saved Row
|
||||
///
|
||||
/// - ``saveAndFetch(_:onConflict:as:)``
|
||||
/// - ``saveAndFetch(_:onConflict:selection:fetch:)``
|
||||
///
|
||||
/// ### Persistence Callbacks
|
||||
///
|
||||
/// - ``willInsert(_:)-5x6sh``
|
||||
/// - ``didInsert(_:)-9jpoy``
|
||||
public protocol PersistableRecord: MutablePersistableRecord {
|
||||
|
||||
// MARK: Insertion Callbacks
|
||||
|
||||
/// Persistence callback called before the record is inserted.
|
||||
///
|
||||
/// Default implementation does nothing.
|
||||
///
|
||||
/// - note: If you need a mutating variant of this method, adopt the
|
||||
/// ``MutablePersistableRecord`` protocol instead.
|
||||
func willInsert(_ db: Database) throws
|
||||
|
||||
/// Persistence callback called upon successful insertion.
|
||||
///
|
||||
/// The default implementation does nothing.
|
||||
///
|
||||
/// You can provide a custom implementation in order to grab the
|
||||
/// auto-incremented id:
|
||||
///
|
||||
/// ```swift
|
||||
/// class Player: PersistableRecord {
|
||||
/// var id: Int64?
|
||||
/// var name: String?
|
||||
///
|
||||
/// func didInsert(_ inserted: InsertionSuccess) {
|
||||
/// id = inserted.rowID
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - note: If you need a mutating variant of this method, adopt the
|
||||
/// ``MutablePersistableRecord`` protocol instead.
|
||||
///
|
||||
/// - parameter inserted: Information about the inserted row.
|
||||
func didInsert(_ inserted: InsertionSuccess)
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
// MARK: - Record
|
||||
|
||||
/// A base class for types that can be fetched and persisted in the database.
|
||||
///
|
||||
/// ## Topics
|
||||
///
|
||||
/// ### Creating Record Instances
|
||||
///
|
||||
/// - ``init()``
|
||||
/// - ``init(row:)``
|
||||
///
|
||||
/// ### Encoding a Database Row
|
||||
///
|
||||
/// - ``encode(to:)``
|
||||
///
|
||||
/// ### Changes Tracking
|
||||
///
|
||||
/// - ``databaseChanges``
|
||||
/// - ``hasDatabaseChanges``
|
||||
/// - ``updateChanges(_:)``
|
||||
///
|
||||
/// ### Persistence Callbacks
|
||||
///
|
||||
/// - ``willSave(_:)``
|
||||
/// - ``willInsert(_:)``
|
||||
/// - ``willUpdate(_:columns:)``
|
||||
/// - ``willDelete(_:)``
|
||||
/// - ``didSave(_:)``
|
||||
/// - ``didInsert(_:)``
|
||||
/// - ``didUpdate(_:)``
|
||||
/// - ``didDelete(deleted:)``
|
||||
/// - ``aroundSave(_:save:)``
|
||||
/// - ``aroundInsert(_:insert:)``
|
||||
/// - ``aroundUpdate(_:columns:update:)``
|
||||
/// - ``aroundDelete(_:delete:)``
|
||||
open class Record {
|
||||
|
||||
// MARK: - Initializers
|
||||
|
||||
/// Creates a Record.
|
||||
public init() { }
|
||||
|
||||
/// Creates a Record from a row.
|
||||
public required init(row: Row) throws {
|
||||
if row.isFetched {
|
||||
// Take care of the hasDatabaseChanges flag.
|
||||
//
|
||||
// Row may be a reused row which will turn invalid as soon as the
|
||||
// SQLite statement is iterated. We need to store an
|
||||
// immutable copy.
|
||||
referenceRow = row.copy()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Core methods
|
||||
|
||||
/// The name of the database table used to build SQL queries.
|
||||
///
|
||||
/// Subclasses must override this method. For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// class Player: Record {
|
||||
/// override class var databaseTableName: String {
|
||||
/// return "player"
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - returns: The name of a database table.
|
||||
open class var databaseTableName: String {
|
||||
// Programmer error
|
||||
fatalError("subclass must override")
|
||||
}
|
||||
|
||||
open class var persistenceConflictPolicy: PersistenceConflictPolicy {
|
||||
PersistenceConflictPolicy(insert: .abort, update: .abort)
|
||||
}
|
||||
|
||||
/// The columns selected by the record.
|
||||
///
|
||||
/// By default, all columns are selected:
|
||||
///
|
||||
/// ```swift
|
||||
/// class Player: Record { }
|
||||
///
|
||||
/// // SELECT * FROM player
|
||||
/// try Player.fetchAll(db)
|
||||
/// ```
|
||||
///
|
||||
/// You can override this property and provide an explicit selection.
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// class PartialPlayer: Record {
|
||||
/// override static var databaseSelection: [any SQLSelectable] {
|
||||
/// [Column("id"), Column("name")]
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // SELECT id, name FROM player
|
||||
/// try PartialPlayer.fetchAll(db)
|
||||
/// ```
|
||||
open class var databaseSelection: [any SQLSelectable] {
|
||||
[AllColumns()]
|
||||
}
|
||||
|
||||
/// Encodes the record into the provided persistence container.
|
||||
///
|
||||
/// In your implementation of this method, store in the `container` argument
|
||||
/// all values that should be stored in database columns.
|
||||
///
|
||||
/// Primary key columns, if any, must be included.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// class Player: Record {
|
||||
/// var id: Int64?
|
||||
/// var name: String?
|
||||
///
|
||||
/// override func encode(to container: inout PersistenceContainer) {
|
||||
/// container["id"] = id
|
||||
/// container["name"] = name
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// It is undefined behavior to set different values for the same column.
|
||||
/// Column names are case insensitive, so defining both "name" and "NAME"
|
||||
/// is considered undefined behavior.
|
||||
///
|
||||
/// - throws: An error is thrown if the record can't be encoded to its
|
||||
/// database representation.
|
||||
open func encode(to container: inout PersistenceContainer) throws { }
|
||||
|
||||
// MARK: - Compare with Previous Versions
|
||||
|
||||
/// A boolean value indicating whether the record has changes that have not
|
||||
/// been saved.
|
||||
///
|
||||
/// This flag is purely informative, and does not prevent insertions and
|
||||
/// updates from performing their database queries.
|
||||
///
|
||||
/// A record is *edited* if has been changed since last database
|
||||
/// synchronization (fetch, update, or insert). Comparison
|
||||
/// is performed between *values* (values stored in the ``encode(to:)``
|
||||
/// method, and values decoded from ``init(row:)``). Property setters do not
|
||||
/// trigger this flag.
|
||||
///
|
||||
/// You can rely on the ``Record`` base class to compute this flag for you,
|
||||
/// or you may set it to true or false when you know better. Setting it to
|
||||
/// false does not prevent it from turning true on subsequent modifications
|
||||
/// of the record.
|
||||
public var hasDatabaseChanges: Bool {
|
||||
do {
|
||||
return try databaseChangesIterator().next() != nil
|
||||
} catch {
|
||||
// Can't encode the record: surely it can't be saved.
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/// A dictionary of changes that have not been saved.
|
||||
///
|
||||
/// The keys of the dictionary are column names, and values are the old
|
||||
/// values that have been changed since last fetching or saving of
|
||||
/// the record.
|
||||
///
|
||||
/// Unless the record has actually been fetched or saved, the old values
|
||||
/// are nil.
|
||||
///
|
||||
/// See ``hasDatabaseChanges`` for more information.
|
||||
///
|
||||
/// - throws: An error is thrown if the record can't be encoded to its
|
||||
/// database representation.
|
||||
public var databaseChanges: [String: DatabaseValue?] {
|
||||
get throws {
|
||||
try Dictionary(uniqueKeysWithValues: databaseChangesIterator())
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets hasDatabaseChanges to true
|
||||
private func setHasDatabaseChanges() {
|
||||
referenceRow = nil
|
||||
}
|
||||
|
||||
/// Sets hasDatabaseChanges to false
|
||||
private func resetDatabaseChanges() throws {
|
||||
referenceRow = try Row(self)
|
||||
}
|
||||
|
||||
/// Sets hasDatabaseChanges to false
|
||||
private func resetDatabaseChanges(with persistenceContainer: PersistenceContainer) {
|
||||
referenceRow = Row(persistenceContainer)
|
||||
}
|
||||
|
||||
// A change iterator that is used by both hasDatabaseChanges and
|
||||
// persistentChangedValues properties.
|
||||
private func databaseChangesIterator() throws -> AnyIterator<(String, DatabaseValue?)> {
|
||||
let oldRow = referenceRow
|
||||
var newValueIterator = try PersistenceContainer(self).makeIterator()
|
||||
return AnyIterator {
|
||||
// Loop until we find a change, or exhaust columns:
|
||||
while let (column, newValue) = newValueIterator.next() {
|
||||
let newDbValue = newValue?.databaseValue ?? .null
|
||||
guard let oldRow, let oldDbValue: DatabaseValue = oldRow[column] else {
|
||||
return (column, nil)
|
||||
}
|
||||
if newDbValue != oldDbValue {
|
||||
return (column, oldDbValue)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Reference row for the *hasDatabaseChanges* property.
|
||||
var referenceRow: Row?
|
||||
|
||||
// MARK: Persistence Callbacks
|
||||
|
||||
/// Called before the record is inserted.
|
||||
///
|
||||
/// If you override this method, you must call `super` at some point in
|
||||
/// your implementation.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
open func willInsert(_ db: Database) throws { }
|
||||
|
||||
/// Called around the record insertion.
|
||||
///
|
||||
/// If you override this method, you must call `super` at some point in
|
||||
/// your implementation (this calls the `insert` parameter).
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// class Player: Record {
|
||||
/// func aroundInsert(_ db: Database, insert: () throws -> InsertionSuccess) throws {
|
||||
/// print("Player will insert")
|
||||
/// try super.aroundInsert(db, insert: insert)
|
||||
/// print("Player did insert")
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter insert: A function that inserts the record, and returns
|
||||
/// information about the inserted row.
|
||||
open func aroundInsert(_ db: Database, insert: () throws -> InsertionSuccess) throws {
|
||||
let inserted = try insert()
|
||||
resetDatabaseChanges(with: inserted.persistenceContainer)
|
||||
}
|
||||
|
||||
/// Called upon successful insertion.
|
||||
///
|
||||
/// You can override this method in order to grab the auto-incremented id:
|
||||
///
|
||||
/// ```swift
|
||||
/// class Player: Record {
|
||||
/// var id: Int64?
|
||||
/// var name: String
|
||||
///
|
||||
/// override func didInsert(_ inserted: InsertionSuccess) {
|
||||
/// super.didInsert(inserted)
|
||||
/// id = inserted.rowID
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// If you override this method, you must call `super` at some point in
|
||||
/// your implementation.
|
||||
///
|
||||
/// - parameter inserted: Information about the inserted row.
|
||||
open func didInsert(_ inserted: InsertionSuccess) { }
|
||||
|
||||
/// Called before the record is updated.
|
||||
///
|
||||
/// If you override this method, you must call `super` at some point in
|
||||
/// your implementation.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
open func willUpdate(_ db: Database, columns: Set<String>) throws { }
|
||||
|
||||
/// Called around the record update.
|
||||
///
|
||||
/// If you override this method, you must call `super` at some point in
|
||||
/// your implementation (this calls the `update` parameter).
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// class Player: Record {
|
||||
/// override func aroundUpdate(
|
||||
/// _ db: Database,
|
||||
/// columns: Set<String>,
|
||||
/// update: () throws -> PersistenceSuccess)
|
||||
/// throws
|
||||
/// {
|
||||
/// print("Player will update")
|
||||
/// try super.aroundUpdate(db, columns: columns, update: update)
|
||||
/// print("Player did update")
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter columns: The updated columns.
|
||||
/// - parameter update: A function that updates the record. Its result is
|
||||
/// reserved for GRDB usage.
|
||||
open func aroundUpdate(_ db: Database, columns: Set<String>, update: () throws -> PersistenceSuccess) throws {
|
||||
let updated = try update()
|
||||
resetDatabaseChanges(with: updated.persistenceContainer)
|
||||
}
|
||||
|
||||
/// Called upon successful update.
|
||||
///
|
||||
/// If you override this method, you must call `super` at some point in
|
||||
/// your implementation.
|
||||
///
|
||||
/// - parameter updated: Reserved for GRDB usage.
|
||||
open func didUpdate(_ updated: PersistenceSuccess) { }
|
||||
|
||||
/// Called before the record is updated or inserted.
|
||||
///
|
||||
/// If you override this method, you must call `super` at some point in
|
||||
/// your implementation.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
open func willSave(_ db: Database) throws { }
|
||||
|
||||
/// Called around the record update or insertion.
|
||||
///
|
||||
/// If you override this method, you must call `super` at some point in
|
||||
/// your implementation (this calls the `update` parameter).
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// class Player: Record {
|
||||
/// override func aroundSave(_ db: Database, save: () throws -> PersistenceSuccess) throws {
|
||||
/// print("Player will save")
|
||||
/// try super.aroundSave(db, save: save)
|
||||
/// print("Player did save")
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter update: A function that updates the record. Its result is
|
||||
/// reserved for GRDB usage.
|
||||
open func aroundSave(_ db: Database, save: () throws -> PersistenceSuccess) throws {
|
||||
_ = try save()
|
||||
}
|
||||
|
||||
/// Called upon successful update or insertion.
|
||||
///
|
||||
/// If you override this method, you must call `super` at some point in
|
||||
/// your implementation.
|
||||
///
|
||||
/// - parameter saved: Reserved for GRDB usage.
|
||||
open func didSave(_ saved: PersistenceSuccess) { }
|
||||
|
||||
/// Called before the record is deleted.
|
||||
///
|
||||
/// If you override this method, you must call `super` at some point in
|
||||
/// your implementation.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
open func willDelete(_ db: Database) throws { }
|
||||
|
||||
/// Called around the destruction of the record.
|
||||
///
|
||||
/// If you override this method, you must call `super` at some point in
|
||||
/// your implementation (this calls the `delete` parameter).
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// class Player: Record {
|
||||
/// override func aroundDelete(_ db: Database, delete: () throws -> Bool) throws {
|
||||
/// print("Player will delete")
|
||||
/// try super.aroundDelete(db, delete: delete)
|
||||
/// print("Player did delete")
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter delete: A function that deletes the record and returns
|
||||
/// whether a row was deleted in the database.
|
||||
open func aroundDelete(_ db: Database, delete: () throws -> Bool) throws {
|
||||
_ = try delete()
|
||||
setHasDatabaseChanges()
|
||||
}
|
||||
|
||||
/// Called upon successful deletion.
|
||||
///
|
||||
/// If you override this method, you must call `super` at some point in
|
||||
/// your implementation.
|
||||
///
|
||||
/// - parameter deleted: Whether a row was deleted in the database.
|
||||
open func didDelete(deleted: Bool) { }
|
||||
|
||||
// MARK: - CRUD
|
||||
|
||||
/// If the record has been changed, executes an `UPDATE` statement so that
|
||||
/// those changes and only those changes are saved in the database.
|
||||
///
|
||||
/// On success, this method sets the `hasDatabaseChanges` flag to false.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - returns: Whether the record had changes and was updated.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
/// ``RecordError/recordNotFound(databaseTableName:key:)`` is thrown
|
||||
/// if the primary key does not match any row in the database and record
|
||||
/// could not be updated.
|
||||
@discardableResult
|
||||
public final func updateChanges(_ db: Database) throws -> Bool {
|
||||
let changedColumns = try Set(databaseChanges.keys)
|
||||
if changedColumns.isEmpty {
|
||||
return false
|
||||
} else {
|
||||
try update(db, columns: changedColumns)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Record: TableRecord { }
|
||||
extension Record: PersistableRecord { }
|
||||
extension Record: FetchableRecord { }
|
||||
@@ -0,0 +1,760 @@
|
||||
import Foundation
|
||||
|
||||
/// A type that builds database queries with the Swift language instead of SQL.
|
||||
///
|
||||
/// A `TableRecord` type is tied to one database table, and can build SQL
|
||||
/// queries on that table.
|
||||
///
|
||||
/// To build SQL queries that involve several tables, define some ``Association``
|
||||
/// between two `TableRecord` types.
|
||||
///
|
||||
/// Most of the time, your record types will get `TableRecord` conformance
|
||||
/// through the ``MutablePersistableRecord`` or ``PersistableRecord`` protocols,
|
||||
/// which provide persistence methods.
|
||||
///
|
||||
/// ## Topics
|
||||
///
|
||||
/// ### Configuring the Generated SQL
|
||||
///
|
||||
/// - ``databaseTableName-3tcw2``
|
||||
/// - ``databaseSelection-7iphs``
|
||||
/// - ``numberOfSelectedColumns(_:)``
|
||||
///
|
||||
/// ### Counting Records
|
||||
///
|
||||
/// - ``fetchCount(_:)``
|
||||
///
|
||||
/// ### Testing for Record Existence
|
||||
///
|
||||
/// - ``exists(_:id:)``
|
||||
/// - ``exists(_:key:)-60hf2``
|
||||
/// - ``exists(_:key:)-6ha6``
|
||||
///
|
||||
/// ### Throwing Record Not Found Errors
|
||||
///
|
||||
/// - ``recordNotFound(_:id:)``
|
||||
/// - ``recordNotFound(_:key:)``
|
||||
/// - ``recordNotFound(key:)``
|
||||
///
|
||||
/// ### Deleting Records
|
||||
///
|
||||
/// - ``deleteAll(_:)``
|
||||
/// - ``deleteAll(_:ids:)``
|
||||
/// - ``deleteAll(_:keys:)-jbkm``
|
||||
/// - ``deleteAll(_:keys:)-5s1jg``
|
||||
/// - ``deleteOne(_:id:)``
|
||||
/// - ``deleteOne(_:key:)-413u8``
|
||||
/// - ``deleteOne(_:key:)-5pdh5``
|
||||
///
|
||||
/// ### Updating Records
|
||||
///
|
||||
/// - ``updateAll(_:onConflict:_:)-7vv9x``
|
||||
/// - ``updateAll(_:onConflict:_:)-7atfw``
|
||||
///
|
||||
/// ### Building Query Interface Requests
|
||||
///
|
||||
/// `TableRecord` provide convenience access to most ``DerivableRequest`` and
|
||||
/// ``QueryInterfaceRequest`` methods as static methods on the type itself.
|
||||
///
|
||||
/// - ``aliased(_:)``
|
||||
/// - ``all()``
|
||||
/// - ``annotated(with:)-3zi1n``
|
||||
/// - ``annotated(with:)-4xoen``
|
||||
/// - ``annotated(with:)-8ce7u``
|
||||
/// - ``annotated(with:)-79389``
|
||||
/// - ``annotated(withOptional:)``
|
||||
/// - ``annotated(withRequired:)``
|
||||
/// - ``filter(_:)``
|
||||
/// - ``filter(id:)``
|
||||
/// - ``filter(ids:)``
|
||||
/// - ``filter(key:)-9ey53``
|
||||
/// - ``filter(key:)-34lau``
|
||||
/// - ``filter(keys:)-4hq8y``
|
||||
/// - ``filter(keys:)-7skw1``
|
||||
/// - ``filter(literal:)``
|
||||
/// - ``filter(sql:arguments:)``
|
||||
/// - ``having(_:)``
|
||||
/// - ``including(all:)``
|
||||
/// - ``including(optional:)``
|
||||
/// - ``including(required:)``
|
||||
/// - ``joining(optional:)``
|
||||
/// - ``joining(required:)``
|
||||
/// - ``limit(_:offset:)``
|
||||
/// - ``matching(_:)-22m4o``
|
||||
/// - ``matching(_:)-1t8ph``
|
||||
/// - ``none()``
|
||||
/// - ``order(_:)-9rc11``
|
||||
/// - ``order(_:)-2033k``
|
||||
/// - ``order(literal:)``
|
||||
/// - ``order(sql:arguments:)``
|
||||
/// - ``orderByPrimaryKey()``
|
||||
/// - ``request(for:)``
|
||||
/// - ``select(_:)-1gvtj``
|
||||
/// - ``select(_:)-5oylt``
|
||||
/// - ``select(_:as:)-1puz3``
|
||||
/// - ``select(_:as:)-tjh0``
|
||||
/// - ``select(literal:)``
|
||||
/// - ``select(literal:as:)``
|
||||
/// - ``select(sql:arguments:)``
|
||||
/// - ``select(sql:arguments:as:)``
|
||||
/// - ``selectPrimaryKey(as:)``
|
||||
/// - ``with(_:)``
|
||||
///
|
||||
/// ### Defining Associations
|
||||
///
|
||||
/// - ``association(to:)``
|
||||
/// - ``association(to:on:)``
|
||||
/// - ``belongsTo(_:key:using:)-13t5r``
|
||||
/// - ``belongsTo(_:key:using:)-81six``
|
||||
/// - ``hasMany(_:key:using:)-45axo``
|
||||
/// - ``hasMany(_:key:using:)-10d4k``
|
||||
/// - ``hasMany(_:through:using:key:)``
|
||||
/// - ``hasOne(_:key:using:)-4g9tm``
|
||||
/// - ``hasOne(_:key:using:)-4v5xa``
|
||||
/// - ``hasOne(_:through:using:key:)``
|
||||
public protocol TableRecord {
|
||||
/// The name of the database table used to build SQL queries.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: TableRecord {
|
||||
/// static var databaseTableName = "player"
|
||||
/// }
|
||||
///
|
||||
/// // SELECT * FROM player
|
||||
/// try Player.fetchAll(db)
|
||||
/// ```
|
||||
static var databaseTableName: String { get }
|
||||
|
||||
/// The columns selected by the record.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: TableRecord {
|
||||
/// static let databaseSelection: [any SQLSelectable] = [AllColumns()]
|
||||
/// }
|
||||
///
|
||||
/// struct PartialPlayer: TableRecord {
|
||||
/// static let databaseTableName = "player"
|
||||
/// static let databaseSelection: [any SQLSelectable] = [
|
||||
/// Column("id"),
|
||||
/// Column("name"),
|
||||
/// ]
|
||||
/// }
|
||||
///
|
||||
/// // SELECT * FROM player
|
||||
/// try Player.fetchAll(db)
|
||||
///
|
||||
/// // SELECT id, name FROM player
|
||||
/// try PartialPlayer.fetchAll(db)
|
||||
/// ```
|
||||
///
|
||||
/// > Important: Make sure the `databaseSelection` property is
|
||||
/// > explicitly declared as `[any SQLSelectable]`. If it is not, the
|
||||
/// > Swift compiler may silently miss the protocol requirement,
|
||||
/// > resulting in sticky `SELECT *` requests.
|
||||
static var databaseSelection: [any SQLSelectable] { get }
|
||||
}
|
||||
|
||||
extension TableRecord {
|
||||
|
||||
/// The default name of the database table used to build requests.
|
||||
///
|
||||
/// - Player -> "player"
|
||||
/// - Place -> "place"
|
||||
/// - PostalAddress -> "postalAddress"
|
||||
/// - HTTPRequest -> "httpRequest"
|
||||
/// - TOEFL -> "toefl"
|
||||
static var defaultDatabaseTableName: String {
|
||||
if let cached = defaultDatabaseTableNameCache.object(forKey: "\(Self.self)" as NSString) {
|
||||
return cached as String
|
||||
}
|
||||
let typeName = "\(Self.self)".replacingOccurrences(of: "(.)\\b.*$", with: "$1", options: [.regularExpression])
|
||||
let initial = typeName.replacingOccurrences(of: "^([A-Z]+).*$", with: "$1", options: [.regularExpression])
|
||||
let tableName: String
|
||||
switch initial.count {
|
||||
case typeName.count:
|
||||
tableName = initial.lowercased()
|
||||
case 0:
|
||||
tableName = typeName
|
||||
case 1:
|
||||
tableName = initial.lowercased() + typeName.dropFirst()
|
||||
default:
|
||||
tableName = initial.dropLast().lowercased() + typeName.dropFirst(initial.count - 1)
|
||||
}
|
||||
defaultDatabaseTableNameCache.setObject(tableName as NSString, forKey: "\(Self.self)" as NSString)
|
||||
return tableName
|
||||
}
|
||||
|
||||
/// The default name of the database table is derived from the name of
|
||||
/// the type.
|
||||
///
|
||||
/// - `Player` -> "player"
|
||||
/// - `Place` -> "place"
|
||||
/// - `PostalAddress` -> "postalAddress"
|
||||
/// - `HTTPRequest` -> "httpRequest"
|
||||
/// - `TOEFL` -> "toefl"
|
||||
public static var databaseTableName: String {
|
||||
defaultDatabaseTableName
|
||||
}
|
||||
|
||||
/// The default selection is all columns: `[AllColumns()]`.
|
||||
public static var databaseSelection: [any SQLSelectable] {
|
||||
[AllColumns()]
|
||||
}
|
||||
}
|
||||
|
||||
extension TableRecord {
|
||||
|
||||
// MARK: - Counting All
|
||||
|
||||
/// Returns the number of records in the database table.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: TableRecord { }
|
||||
///
|
||||
/// try dbQueue.read { db in
|
||||
/// // SELECT COUNT(*) FROM player
|
||||
/// let count = try Player.fetchCount(db)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
public static func fetchCount(_ db: Database) throws -> Int {
|
||||
try all().fetchCount(db)
|
||||
}
|
||||
}
|
||||
|
||||
extension TableRecord {
|
||||
|
||||
// MARK: - SQL Generation
|
||||
|
||||
/// Returns the number of selected columns.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: TableRecord { }
|
||||
///
|
||||
/// struct PartialPlayer: TableRecord {
|
||||
/// static let databaseTableName = "player"
|
||||
/// static let databaseSelection = [Column("id"), Column("name")]
|
||||
/// }
|
||||
///
|
||||
/// try dbQueue.write { db in
|
||||
/// try db.create(table: "player") { t in
|
||||
/// t.autoIncrementedPrimaryKey("id")
|
||||
/// t.column("name", .text)
|
||||
/// t.column("score", .integer)
|
||||
/// }
|
||||
///
|
||||
/// try Player.numberOfSelectedColumns(db) // 3
|
||||
/// try PartialPlayer.numberOfSelectedColumns(db) // 2
|
||||
/// }
|
||||
/// ```
|
||||
public static func numberOfSelectedColumns(_ db: Database) throws -> Int {
|
||||
// The alias makes it possible to count the columns in `SELECT *`:
|
||||
let alias = TableAlias(tableName: databaseTableName)
|
||||
let context = SQLGenerationContext(db)
|
||||
return try databaseSelection
|
||||
.map { $0.sqlSelection.qualified(with: alias) }
|
||||
.columnCount(context)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Batch Delete
|
||||
|
||||
extension TableRecord {
|
||||
|
||||
/// Deletes all records, and returns the number of deleted records.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: TableRecord { }
|
||||
///
|
||||
/// try dbQueue.write { db in
|
||||
/// // DELETE FROM player
|
||||
/// let count = try Player.deleteAll(db)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - returns: The number of deleted records.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
@discardableResult
|
||||
public static func deleteAll(_ db: Database) throws -> Int {
|
||||
try all().deleteAll(db)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Check Existence by Single-Column Primary Key
|
||||
|
||||
extension TableRecord {
|
||||
/// Returns whether a record exists for this primary key.
|
||||
///
|
||||
/// All single-column primary keys are supported:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: TableRecord { }
|
||||
/// struct Country: TableRecord { }
|
||||
///
|
||||
/// try dbQueue.read { db in
|
||||
/// let playerExists = try Player.exists(db, key: 1)
|
||||
/// let countryExists = try Country.exists(db, key: "FR")
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameters:
|
||||
/// - db: A database connection.
|
||||
/// - key: A primary key value.
|
||||
/// - returns: Whether a record exists for this primary key.
|
||||
public static func exists(_ db: Database, key: some DatabaseValueConvertible) throws -> Bool {
|
||||
try !filter(key: key).isEmpty(db)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
extension TableRecord where Self: Identifiable, ID: DatabaseValueConvertible {
|
||||
/// Returns whether a record exists for this primary key.
|
||||
///
|
||||
/// All single-column primary keys are supported:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: TableRecord, Identifiable {
|
||||
/// var id: Int64
|
||||
/// }
|
||||
/// struct Country: TableRecord, Identifiable {
|
||||
/// var id: String
|
||||
/// }
|
||||
///
|
||||
/// try dbQueue.read { db in
|
||||
/// let playerExists = try Player.exists(db, id: 1)
|
||||
/// let countryExists = try Country.exists(db, id: "FR")
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameters:
|
||||
/// - db: A database connection.
|
||||
/// - id: A primary key value.
|
||||
/// - returns: Whether a record exists for this primary key.
|
||||
public static func exists(_ db: Database, id: ID) throws -> Bool {
|
||||
if id.databaseValue.isNull {
|
||||
// Don't hit the database
|
||||
return false
|
||||
}
|
||||
return try !filter(id: id).isEmpty(db)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Check Existence by Key
|
||||
|
||||
extension TableRecord {
|
||||
/// Returns whether a record exists for this primary or unique key.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: TableRecord { }
|
||||
/// struct Citizenship: TableRecord { }
|
||||
///
|
||||
/// try dbQueue.read { db in
|
||||
/// let playerExists = Player.exists(db, key: ["id": 1])
|
||||
/// let playerExists = Player.exists(db, key: ["email": "arthur@example.com"])
|
||||
/// let citizenshipExists = Citizenship.exists(db, key: [
|
||||
/// "citizenId": 1,
|
||||
/// "countryCode": "FR",
|
||||
/// ])
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// A fatal error is raised if no unique index exists on a subset of the
|
||||
/// key columns.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - db: A database connection.
|
||||
/// - key: A key dictionary.
|
||||
/// - returns: Whether a record exists for this key.
|
||||
public static func exists(_ db: Database, key: [String: (any DatabaseValueConvertible)?]) throws -> Bool {
|
||||
try !filter(key: key).isEmpty(db)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Deleting by Single-Column Primary Key
|
||||
|
||||
extension TableRecord {
|
||||
/// Deletes records identified by their primary keys, and returns the number
|
||||
/// of deleted records.
|
||||
///
|
||||
/// All single-column primary keys are supported:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: TableRecord { }
|
||||
/// struct Country: TableRecord { }
|
||||
///
|
||||
/// try dbQueue.write { db in
|
||||
/// // DELETE FROM player WHERE id IN (1, 2, 3)
|
||||
/// try Player.deleteAll(db, keys: [1, 2, 3])
|
||||
///
|
||||
/// // DELETE FROM country WHERE code IN ('FR', 'US')
|
||||
/// try Country.deleteAll(db, keys: ["FR", "US"])
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameters:
|
||||
/// - db: A database connection.
|
||||
/// - keys: A sequence of primary keys.
|
||||
/// - returns: The number of deleted records.
|
||||
@discardableResult
|
||||
public static func deleteAll<Keys>(_ db: Database, keys: Keys)
|
||||
throws -> Int
|
||||
where Keys: Sequence, Keys.Element: DatabaseValueConvertible
|
||||
{
|
||||
let keys = Array(keys)
|
||||
if keys.isEmpty {
|
||||
// Avoid hitting the database
|
||||
return 0
|
||||
}
|
||||
return try filter(keys: keys).deleteAll(db)
|
||||
}
|
||||
|
||||
/// Deletes the record identified by its primary key, and returns whether a
|
||||
/// record was deleted.
|
||||
///
|
||||
/// All single-column primary keys are supported:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: TableRecord { }
|
||||
/// struct Country: TableRecord { }
|
||||
///
|
||||
/// try dbQueue.write { db in
|
||||
/// // DELETE FROM player WHERE id = 1
|
||||
/// try Player.deleteOne(db, key: 1)
|
||||
///
|
||||
/// // DELETE FROM country WHERE code = 'FR'
|
||||
/// try Country.deleteOne(db, key: "FR")
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameters:
|
||||
/// - db: A database connection.
|
||||
/// - key: A primary key value.
|
||||
/// - returns: Whether a record was deleted.
|
||||
@discardableResult
|
||||
public static func deleteOne(_ db: Database, key: some DatabaseValueConvertible) throws -> Bool {
|
||||
if key.databaseValue.isNull {
|
||||
// Don't hit the database
|
||||
return false
|
||||
}
|
||||
return try deleteAll(db, keys: [key]) > 0
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
extension TableRecord where Self: Identifiable, ID: DatabaseValueConvertible {
|
||||
/// Deletes records identified by their primary keys, and returns the number
|
||||
/// of deleted records.
|
||||
///
|
||||
/// All single-column primary keys are supported:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: TableRecord, Identifiable {
|
||||
/// var id: Int64
|
||||
/// }
|
||||
/// struct Country: TableRecord, Identifiable {
|
||||
/// var id: String
|
||||
/// }
|
||||
///
|
||||
/// try dbQueue.write { db in
|
||||
/// // DELETE FROM player WHERE id IN (1, 2, 3)
|
||||
/// try Player.deleteAll(db, ids: [1, 2, 3])
|
||||
///
|
||||
/// // DELETE FROM country WHERE code IN ('FR', 'US')
|
||||
/// try Country.deleteAll(db, ids: ["FR", "US"])
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameters:
|
||||
/// - db: A database connection.
|
||||
/// - ids: A collection of primary keys.
|
||||
/// - returns: The number of deleted records.
|
||||
@discardableResult
|
||||
public static func deleteAll<IDS>(_ db: Database, ids: IDS) throws -> Int
|
||||
where IDS: Collection, IDS.Element == ID
|
||||
{
|
||||
if ids.isEmpty {
|
||||
// Avoid hitting the database
|
||||
return 0
|
||||
}
|
||||
return try filter(ids: ids).deleteAll(db)
|
||||
}
|
||||
|
||||
/// Deletes the record identified by its primary key, and returns whether a
|
||||
/// record was deleted.
|
||||
///
|
||||
/// All single-column primary keys are supported:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: TableRecord, Identifiable {
|
||||
/// var id: Int64
|
||||
/// }
|
||||
/// struct Country: TableRecord, Identifiable {
|
||||
/// var id: String
|
||||
/// }
|
||||
///
|
||||
/// try dbQueue.write { db in
|
||||
/// // DELETE FROM player WHERE id = 1
|
||||
/// try Player.deleteOne(db, id: 1)
|
||||
///
|
||||
/// // DELETE FROM country WHERE code = 'FR'
|
||||
/// try Country.deleteOne(db, id: "FR")
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameters:
|
||||
/// - db: A database connection.
|
||||
/// - id: A primary key value.
|
||||
/// - returns: Whether a record was deleted.
|
||||
@discardableResult
|
||||
public static func deleteOne(_ db: Database, id: ID) throws -> Bool {
|
||||
try deleteAll(db, ids: [id]) > 0
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Deleting by Key
|
||||
|
||||
extension TableRecord {
|
||||
/// Deletes records identified by their primary or unique keys, and returns
|
||||
/// the number of deleted records.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: TableRecord { }
|
||||
/// struct Citizenship: TableRecord { }
|
||||
///
|
||||
/// try dbQueue.write { db in
|
||||
/// // DELETE FROM player WHERE id = 1
|
||||
/// try Player.deleteAll(db, keys: [["id": 1]])
|
||||
///
|
||||
/// // DELETE FROM player WHERE email = 'arthur@example.com'
|
||||
/// try Player.deleteAll(db, keys: [["email": "arthur@example.com"]])
|
||||
///
|
||||
/// // DELETE FROM citizenship WHERE citizenId = 1 AND countryCode = 'FR'
|
||||
/// try Citizenship.deleteAll(db, keys: [
|
||||
/// ["citizenId": 1, "countryCode": "FR"],
|
||||
/// ])
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// A fatal error is raised if no unique index exists on a subset of the
|
||||
/// key columns.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - db: A database connection.
|
||||
/// - keys: An array of key dictionaries.
|
||||
/// - returns: The number of deleted records.
|
||||
@discardableResult
|
||||
public static func deleteAll(_ db: Database, keys: [[String: (any DatabaseValueConvertible)?]]) throws -> Int {
|
||||
if keys.isEmpty {
|
||||
// Avoid hitting the database
|
||||
return 0
|
||||
}
|
||||
return try filter(keys: keys).deleteAll(db)
|
||||
}
|
||||
|
||||
/// Deletes the record identified by its primary or unique key, and returns
|
||||
/// whether a record was deleted.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: TableRecord { }
|
||||
/// struct Citizenship: TableRecord { }
|
||||
///
|
||||
/// try dbQueue.write { db in
|
||||
/// // DELETE FROM player WHERE id = 1
|
||||
/// try Player.deleteOne(db, key: ["id": 1])
|
||||
///
|
||||
/// // DELETE FROM player WHERE email = 'arthur@example.com'
|
||||
/// try Player.deleteOne(db, key: ["email": "arthur@example.com"])
|
||||
///
|
||||
/// // DELETE FROM citizenship WHERE citizenId = 1 AND countryCode = 'FR'
|
||||
/// try Citizenship.deleteOne(db, key: [
|
||||
/// "citizenId": 1,
|
||||
/// "countryCode": "FR",
|
||||
/// ])
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// A fatal error is raised if no unique index exists on a subset of the
|
||||
/// key columns.
|
||||
/// - parameters:
|
||||
/// - db: A database connection.
|
||||
/// - key: A key dictionary.
|
||||
/// - returns: Whether a record was deleted.
|
||||
@discardableResult
|
||||
public static func deleteOne(_ db: Database, key: [String: (any DatabaseValueConvertible)?]) throws -> Bool {
|
||||
try deleteAll(db, keys: [key]) > 0
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Batch Update
|
||||
|
||||
extension TableRecord {
|
||||
|
||||
/// Updates all records, and returns the number of updated records.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: TableRecord { }
|
||||
///
|
||||
/// try dbQueue.write { db in
|
||||
/// // UPDATE player SET score = 0
|
||||
/// try Player.updateAll(db, [Column("score").set(to: 0)])
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution,
|
||||
/// defaulting to the record's persistenceConflictPolicy.
|
||||
/// - parameter assignments: An array of column assignments.
|
||||
/// - returns: The number of updated records.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
@discardableResult
|
||||
public static func updateAll(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
||||
_ assignments: [ColumnAssignment])
|
||||
throws -> Int
|
||||
{
|
||||
try all().updateAll(db, onConflict: conflictResolution, assignments)
|
||||
}
|
||||
|
||||
/// Updates all records, and returns the number of updated records.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: TableRecord { }
|
||||
///
|
||||
/// try dbQueue.write { db in
|
||||
/// // UPDATE player SET score = 0
|
||||
/// try Player.updateAll(db, Column("score").set(to: 0))
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter conflictResolution: A policy for conflict resolution,
|
||||
/// defaulting to the record's persistenceConflictPolicy.
|
||||
/// - parameter assignments: Column assignments.
|
||||
/// - returns: The number of updated records.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
@discardableResult
|
||||
public static func updateAll(
|
||||
_ db: Database,
|
||||
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
||||
_ assignments: ColumnAssignment...)
|
||||
throws -> Int
|
||||
{
|
||||
try updateAll(db, onConflict: conflictResolution, assignments)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - RecordError
|
||||
|
||||
/// A record error.
|
||||
///
|
||||
/// `RecordError` is thrown by ``MutablePersistableRecord`` types when an
|
||||
/// `update` method could not find any row to update:
|
||||
///
|
||||
/// ```swift
|
||||
/// do {
|
||||
/// try player.update(db)
|
||||
/// } catch let RecordError.recordNotFound(databaseTableName: table, key: key) {
|
||||
/// print("Key \(key) was not found in table \(table).")
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// `RecordError` is also thrown by ``FetchableRecord`` types when a
|
||||
/// `find` method does not find any record:
|
||||
///
|
||||
/// ```swift
|
||||
/// do {
|
||||
/// let player = try Player.find(db, id: 42)
|
||||
/// } catch let RecordError.recordNotFound(databaseTableName: table, key: key) {
|
||||
/// print("Key \(key) was not found in table \(table).")
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// You can create `RecordError` instances with the
|
||||
/// ``TableRecord/recordNotFound(_:id:)`` method and its variants.
|
||||
public enum RecordError: Error {
|
||||
/// A record does not exist in the database.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - databaseTableName: The table of the missing record.
|
||||
/// - key: The key of the missing record (column and values).
|
||||
case recordNotFound(databaseTableName: String, key: [String: DatabaseValue])
|
||||
}
|
||||
|
||||
extension RecordError: CustomStringConvertible {
|
||||
public var description: String {
|
||||
switch self {
|
||||
case let .recordNotFound(databaseTableName: databaseTableName, key: key):
|
||||
let row = Row(key) // For nice output
|
||||
return "Key not found in table \(databaseTableName): \(row.description)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TableRecord {
|
||||
/// Returns an error for a record that does not exist in the database.
|
||||
///
|
||||
/// - returns: ``RecordError/recordNotFound(databaseTableName:key:)``, or
|
||||
/// any error that prevented the `RecordError` from being constructed.
|
||||
public static func recordNotFound(_ db: Database, key: some DatabaseValueConvertible) -> any Error {
|
||||
do {
|
||||
let primaryKey = try db.primaryKey(databaseTableName)
|
||||
GRDBPrecondition(
|
||||
primaryKey.columns.count == 1,
|
||||
"Requesting by key requires a single-column primary key in the table \(databaseTableName)")
|
||||
return RecordError.recordNotFound(
|
||||
databaseTableName: databaseTableName,
|
||||
key: [primaryKey.columns[0]: key.databaseValue])
|
||||
} catch {
|
||||
return error
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an error for a record that does not exist in the database.
|
||||
public static func recordNotFound(key: [String: (any DatabaseValueConvertible)?]) -> RecordError {
|
||||
RecordError.recordNotFound(
|
||||
databaseTableName: databaseTableName,
|
||||
key: key.mapValues { $0?.databaseValue ?? .null })
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
extension TableRecord where Self: Identifiable, ID: DatabaseValueConvertible {
|
||||
/// Returns an error for a record that does not exist in the database.
|
||||
///
|
||||
/// - returns: ``RecordError/recordNotFound(databaseTableName:key:)``, or
|
||||
/// any error that prevented the `RecordError` from being constructed.
|
||||
public static func recordNotFound(_ db: Database, id: Self.ID) -> any Error {
|
||||
recordNotFound(db, key: id)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, deprecated, renamed: "RecordError")
|
||||
public typealias PersistenceError = RecordError
|
||||
|
||||
/// Calculating `defaultDatabaseTableName` is somewhat expensive due to the regular expression evaluation
|
||||
///
|
||||
/// This cache mitigates the cost of the calculation by storing the name for later retrieval
|
||||
private let defaultDatabaseTableNameCache = NSCache<NSString, NSString>()
|
||||
Reference in New Issue
Block a user