434 lines
15 KiB
Swift
434 lines
15 KiB
Swift
/// 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)
|
|
}
|
|
}
|