Add CI/CD configuration and API documentation
This commit is contained in:
@@ -0,0 +1,433 @@
|
||||
// MARK: - Record
|
||||
|
||||
/// A base class for types that can be fetched and persisted in the database.
|
||||
///
|
||||
/// ## Topics
|
||||
///
|
||||
/// ### Creating Record Instances
|
||||
///
|
||||
/// - ``init()``
|
||||
/// - ``init(row:)``
|
||||
///
|
||||
/// ### Encoding a Database Row
|
||||
///
|
||||
/// - ``encode(to:)``
|
||||
///
|
||||
/// ### Changes Tracking
|
||||
///
|
||||
/// - ``databaseChanges``
|
||||
/// - ``hasDatabaseChanges``
|
||||
/// - ``updateChanges(_:)``
|
||||
///
|
||||
/// ### Persistence Callbacks
|
||||
///
|
||||
/// - ``willSave(_:)``
|
||||
/// - ``willInsert(_:)``
|
||||
/// - ``willUpdate(_:columns:)``
|
||||
/// - ``willDelete(_:)``
|
||||
/// - ``didSave(_:)``
|
||||
/// - ``didInsert(_:)``
|
||||
/// - ``didUpdate(_:)``
|
||||
/// - ``didDelete(deleted:)``
|
||||
/// - ``aroundSave(_:save:)``
|
||||
/// - ``aroundInsert(_:insert:)``
|
||||
/// - ``aroundUpdate(_:columns:update:)``
|
||||
/// - ``aroundDelete(_:delete:)``
|
||||
open class Record {
|
||||
|
||||
// MARK: - Initializers
|
||||
|
||||
/// Creates a Record.
|
||||
public init() { }
|
||||
|
||||
/// Creates a Record from a row.
|
||||
public required init(row: Row) throws {
|
||||
if row.isFetched {
|
||||
// Take care of the hasDatabaseChanges flag.
|
||||
//
|
||||
// Row may be a reused row which will turn invalid as soon as the
|
||||
// SQLite statement is iterated. We need to store an
|
||||
// immutable copy.
|
||||
referenceRow = row.copy()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Core methods
|
||||
|
||||
/// The name of the database table used to build SQL queries.
|
||||
///
|
||||
/// Subclasses must override this method. For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// class Player: Record {
|
||||
/// override class var databaseTableName: String {
|
||||
/// return "player"
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - returns: The name of a database table.
|
||||
open class var databaseTableName: String {
|
||||
// Programmer error
|
||||
fatalError("subclass must override")
|
||||
}
|
||||
|
||||
open class var persistenceConflictPolicy: PersistenceConflictPolicy {
|
||||
PersistenceConflictPolicy(insert: .abort, update: .abort)
|
||||
}
|
||||
|
||||
/// The columns selected by the record.
|
||||
///
|
||||
/// By default, all columns are selected:
|
||||
///
|
||||
/// ```swift
|
||||
/// class Player: Record { }
|
||||
///
|
||||
/// // SELECT * FROM player
|
||||
/// try Player.fetchAll(db)
|
||||
/// ```
|
||||
///
|
||||
/// You can override this property and provide an explicit selection.
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// class PartialPlayer: Record {
|
||||
/// override static var databaseSelection: [any SQLSelectable] {
|
||||
/// [Column("id"), Column("name")]
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // SELECT id, name FROM player
|
||||
/// try PartialPlayer.fetchAll(db)
|
||||
/// ```
|
||||
open class var databaseSelection: [any SQLSelectable] {
|
||||
[AllColumns()]
|
||||
}
|
||||
|
||||
/// Encodes the record into the provided persistence container.
|
||||
///
|
||||
/// In your implementation of this method, store in the `container` argument
|
||||
/// all values that should be stored in database columns.
|
||||
///
|
||||
/// Primary key columns, if any, must be included.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// class Player: Record {
|
||||
/// var id: Int64?
|
||||
/// var name: String?
|
||||
///
|
||||
/// override func encode(to container: inout PersistenceContainer) {
|
||||
/// container["id"] = id
|
||||
/// container["name"] = name
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// It is undefined behavior to set different values for the same column.
|
||||
/// Column names are case insensitive, so defining both "name" and "NAME"
|
||||
/// is considered undefined behavior.
|
||||
///
|
||||
/// - throws: An error is thrown if the record can't be encoded to its
|
||||
/// database representation.
|
||||
open func encode(to container: inout PersistenceContainer) throws { }
|
||||
|
||||
// MARK: - Compare with Previous Versions
|
||||
|
||||
/// A boolean value indicating whether the record has changes that have not
|
||||
/// been saved.
|
||||
///
|
||||
/// This flag is purely informative, and does not prevent insertions and
|
||||
/// updates from performing their database queries.
|
||||
///
|
||||
/// A record is *edited* if has been changed since last database
|
||||
/// synchronization (fetch, update, or insert). Comparison
|
||||
/// is performed between *values* (values stored in the ``encode(to:)``
|
||||
/// method, and values decoded from ``init(row:)``). Property setters do not
|
||||
/// trigger this flag.
|
||||
///
|
||||
/// You can rely on the ``Record`` base class to compute this flag for you,
|
||||
/// or you may set it to true or false when you know better. Setting it to
|
||||
/// false does not prevent it from turning true on subsequent modifications
|
||||
/// of the record.
|
||||
public var hasDatabaseChanges: Bool {
|
||||
do {
|
||||
return try databaseChangesIterator().next() != nil
|
||||
} catch {
|
||||
// Can't encode the record: surely it can't be saved.
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/// A dictionary of changes that have not been saved.
|
||||
///
|
||||
/// The keys of the dictionary are column names, and values are the old
|
||||
/// values that have been changed since last fetching or saving of
|
||||
/// the record.
|
||||
///
|
||||
/// Unless the record has actually been fetched or saved, the old values
|
||||
/// are nil.
|
||||
///
|
||||
/// See ``hasDatabaseChanges`` for more information.
|
||||
///
|
||||
/// - throws: An error is thrown if the record can't be encoded to its
|
||||
/// database representation.
|
||||
public var databaseChanges: [String: DatabaseValue?] {
|
||||
get throws {
|
||||
try Dictionary(uniqueKeysWithValues: databaseChangesIterator())
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets hasDatabaseChanges to true
|
||||
private func setHasDatabaseChanges() {
|
||||
referenceRow = nil
|
||||
}
|
||||
|
||||
/// Sets hasDatabaseChanges to false
|
||||
private func resetDatabaseChanges() throws {
|
||||
referenceRow = try Row(self)
|
||||
}
|
||||
|
||||
/// Sets hasDatabaseChanges to false
|
||||
private func resetDatabaseChanges(with persistenceContainer: PersistenceContainer) {
|
||||
referenceRow = Row(persistenceContainer)
|
||||
}
|
||||
|
||||
// A change iterator that is used by both hasDatabaseChanges and
|
||||
// persistentChangedValues properties.
|
||||
private func databaseChangesIterator() throws -> AnyIterator<(String, DatabaseValue?)> {
|
||||
let oldRow = referenceRow
|
||||
var newValueIterator = try PersistenceContainer(self).makeIterator()
|
||||
return AnyIterator {
|
||||
// Loop until we find a change, or exhaust columns:
|
||||
while let (column, newValue) = newValueIterator.next() {
|
||||
let newDbValue = newValue?.databaseValue ?? .null
|
||||
guard let oldRow, let oldDbValue: DatabaseValue = oldRow[column] else {
|
||||
return (column, nil)
|
||||
}
|
||||
if newDbValue != oldDbValue {
|
||||
return (column, oldDbValue)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Reference row for the *hasDatabaseChanges* property.
|
||||
var referenceRow: Row?
|
||||
|
||||
// MARK: Persistence Callbacks
|
||||
|
||||
/// Called before the record is inserted.
|
||||
///
|
||||
/// If you override this method, you must call `super` at some point in
|
||||
/// your implementation.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
open func willInsert(_ db: Database) throws { }
|
||||
|
||||
/// Called around the record insertion.
|
||||
///
|
||||
/// If you override this method, you must call `super` at some point in
|
||||
/// your implementation (this calls the `insert` parameter).
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// class Player: Record {
|
||||
/// func aroundInsert(_ db: Database, insert: () throws -> InsertionSuccess) throws {
|
||||
/// print("Player will insert")
|
||||
/// try super.aroundInsert(db, insert: insert)
|
||||
/// print("Player did insert")
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter insert: A function that inserts the record, and returns
|
||||
/// information about the inserted row.
|
||||
open func aroundInsert(_ db: Database, insert: () throws -> InsertionSuccess) throws {
|
||||
let inserted = try insert()
|
||||
resetDatabaseChanges(with: inserted.persistenceContainer)
|
||||
}
|
||||
|
||||
/// Called upon successful insertion.
|
||||
///
|
||||
/// You can override this method in order to grab the auto-incremented id:
|
||||
///
|
||||
/// ```swift
|
||||
/// class Player: Record {
|
||||
/// var id: Int64?
|
||||
/// var name: String
|
||||
///
|
||||
/// override func didInsert(_ inserted: InsertionSuccess) {
|
||||
/// super.didInsert(inserted)
|
||||
/// id = inserted.rowID
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// If you override this method, you must call `super` at some point in
|
||||
/// your implementation.
|
||||
///
|
||||
/// - parameter inserted: Information about the inserted row.
|
||||
open func didInsert(_ inserted: InsertionSuccess) { }
|
||||
|
||||
/// Called before the record is updated.
|
||||
///
|
||||
/// If you override this method, you must call `super` at some point in
|
||||
/// your implementation.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
open func willUpdate(_ db: Database, columns: Set<String>) throws { }
|
||||
|
||||
/// Called around the record update.
|
||||
///
|
||||
/// If you override this method, you must call `super` at some point in
|
||||
/// your implementation (this calls the `update` parameter).
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// class Player: Record {
|
||||
/// override func aroundUpdate(
|
||||
/// _ db: Database,
|
||||
/// columns: Set<String>,
|
||||
/// update: () throws -> PersistenceSuccess)
|
||||
/// throws
|
||||
/// {
|
||||
/// print("Player will update")
|
||||
/// try super.aroundUpdate(db, columns: columns, update: update)
|
||||
/// print("Player did update")
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter columns: The updated columns.
|
||||
/// - parameter update: A function that updates the record. Its result is
|
||||
/// reserved for GRDB usage.
|
||||
open func aroundUpdate(_ db: Database, columns: Set<String>, update: () throws -> PersistenceSuccess) throws {
|
||||
let updated = try update()
|
||||
resetDatabaseChanges(with: updated.persistenceContainer)
|
||||
}
|
||||
|
||||
/// Called upon successful update.
|
||||
///
|
||||
/// If you override this method, you must call `super` at some point in
|
||||
/// your implementation.
|
||||
///
|
||||
/// - parameter updated: Reserved for GRDB usage.
|
||||
open func didUpdate(_ updated: PersistenceSuccess) { }
|
||||
|
||||
/// Called before the record is updated or inserted.
|
||||
///
|
||||
/// If you override this method, you must call `super` at some point in
|
||||
/// your implementation.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
open func willSave(_ db: Database) throws { }
|
||||
|
||||
/// Called around the record update or insertion.
|
||||
///
|
||||
/// If you override this method, you must call `super` at some point in
|
||||
/// your implementation (this calls the `update` parameter).
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// class Player: Record {
|
||||
/// override func aroundSave(_ db: Database, save: () throws -> PersistenceSuccess) throws {
|
||||
/// print("Player will save")
|
||||
/// try super.aroundSave(db, save: save)
|
||||
/// print("Player did save")
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter update: A function that updates the record. Its result is
|
||||
/// reserved for GRDB usage.
|
||||
open func aroundSave(_ db: Database, save: () throws -> PersistenceSuccess) throws {
|
||||
_ = try save()
|
||||
}
|
||||
|
||||
/// Called upon successful update or insertion.
|
||||
///
|
||||
/// If you override this method, you must call `super` at some point in
|
||||
/// your implementation.
|
||||
///
|
||||
/// - parameter saved: Reserved for GRDB usage.
|
||||
open func didSave(_ saved: PersistenceSuccess) { }
|
||||
|
||||
/// Called before the record is deleted.
|
||||
///
|
||||
/// If you override this method, you must call `super` at some point in
|
||||
/// your implementation.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
open func willDelete(_ db: Database) throws { }
|
||||
|
||||
/// Called around the destruction of the record.
|
||||
///
|
||||
/// If you override this method, you must call `super` at some point in
|
||||
/// your implementation (this calls the `delete` parameter).
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// class Player: Record {
|
||||
/// override func aroundDelete(_ db: Database, delete: () throws -> Bool) throws {
|
||||
/// print("Player will delete")
|
||||
/// try super.aroundDelete(db, delete: delete)
|
||||
/// print("Player did delete")
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter delete: A function that deletes the record and returns
|
||||
/// whether a row was deleted in the database.
|
||||
open func aroundDelete(_ db: Database, delete: () throws -> Bool) throws {
|
||||
_ = try delete()
|
||||
setHasDatabaseChanges()
|
||||
}
|
||||
|
||||
/// Called upon successful deletion.
|
||||
///
|
||||
/// If you override this method, you must call `super` at some point in
|
||||
/// your implementation.
|
||||
///
|
||||
/// - parameter deleted: Whether a row was deleted in the database.
|
||||
open func didDelete(deleted: Bool) { }
|
||||
|
||||
// MARK: - CRUD
|
||||
|
||||
/// If the record has been changed, executes an `UPDATE` statement so that
|
||||
/// those changes and only those changes are saved in the database.
|
||||
///
|
||||
/// On success, this method sets the `hasDatabaseChanges` flag to false.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - returns: Whether the record had changes and was updated.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
/// ``RecordError/recordNotFound(databaseTableName:key:)`` is thrown
|
||||
/// if the primary key does not match any row in the database and record
|
||||
/// could not be updated.
|
||||
@discardableResult
|
||||
public final func updateChanges(_ db: Database) throws -> Bool {
|
||||
let changedColumns = try Set(databaseChanges.keys)
|
||||
if changedColumns.isEmpty {
|
||||
return false
|
||||
} else {
|
||||
try update(db, columns: changedColumns)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Record: TableRecord { }
|
||||
extension Record: PersistableRecord { }
|
||||
extension Record: FetchableRecord { }
|
||||
Reference in New Issue
Block a user