This commit is contained in:
zeus
2025-01-22 16:22:33 +08:00
parent 09b9cec8ec
commit 738c373a77
2534 changed files with 0 additions and 486292 deletions
@@ -1,295 +0,0 @@
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)
}
}
@@ -1,725 +0,0 @@
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 doesnt 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
}
}
@@ -1,936 +0,0 @@
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)
}
}
}
@@ -1,537 +0,0 @@
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
@@ -1,365 +0,0 @@
/// 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))"
}
}
@@ -1,59 +0,0 @@
// 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
}
}
@@ -1,531 +0,0 @@
// 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)
}
}
@@ -1,411 +0,0 @@
// 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
}
}
@@ -1,976 +0,0 @@
// 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
}
}
@@ -1,464 +0,0 @@
// 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)
}
}
@@ -1,433 +0,0 @@
/// 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)
}
}
@@ -1,393 +0,0 @@
// 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
}
}
@@ -1,277 +0,0 @@
// 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)
}
}
@@ -1,404 +0,0 @@
// 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
}
}
@@ -1,77 +0,0 @@
/// 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)
}
@@ -1,433 +0,0 @@
// 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 { }
@@ -1,760 +0,0 @@
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>()