Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/SignalServiceKit/Storage/Database/SDSCodableModel/SDSCodableModel.swift
1 views
//
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import Foundation
public import GRDB

/// Provides serialization for SDS models using Swift's ``Codable``.
///
/// Requires conformance to ``FetchableRecord``. Types that conform to
/// ``Decodable`` may take advantage of a default ``FetchableRecord``
/// conformance.
///
/// **This default implementation must not be used by types with inheritance**.
///
/// Types that support inheritance in which the base class wishes to conform
/// to ``SDSCodableModel`` should use
/// ``NeedsFactoryInitializationFromRecordType`` on the base class along with
/// ``FactoryInitializableFromRecordType`` on all subclasses.
///
/// See ``JobRecord`` for an example, and see below for additional context.
///
/// ---
///
/// Consider a base class conforming to ``SDSEncodableModel & Decodable``,
/// which has subclasses that override its `init(from:)`. Due to a
/// [Swift issue][0] calling static methods such as ``FetchableRecord.fetchOne``
/// in contexts where the inferrable static type is the base class will result
/// in the overridden initializer being ignored and only the base class being
/// initialized - even if the dynamic runtime type is the subclass.
///
/// This can lead to unexpected and undesirable behavior in generic contexts.
/// For example, consider the following snippet:
///
/// ```swift
/// func foo<Model: SDSCodableModel & Decodable>(model: Model) {
///     ...
///     let fetchedModel: Model = Model.fetchOne()
///     ...
/// }
///
/// class Base: SDSCodableModel { ... }
/// class Derived: Base {
///     override init(from: Decoder) throws { ... }
/// }
///
/// foo(model: Derived())
/// ```
///
/// Inside the call to `fetchOne()` (a static context), `self` will be the
/// expected dynamic type `Derived`. However, `Self.self` will be the inferred
/// static type `Base`, on which `fetchOne` is actually declared. If `fetchOne`
/// ends up using `Self` as a generic parameter, any subsequent code taking that
/// parameter will have no knowledge of `Derived`.
///
/// In the case of the real ``FetchableRecord.fetchOne`` implementation this is
/// exactly what happens; downstream code instantiates a `Self` using
/// `init(from:)`. In the example above, since `Self == Base` `fetchedModel`
/// will be of type `Base` and `Derived.init()` will never be called; this is
/// despite the fact that `type(of: model) == Derived`.
///
/// Another problematic example would include a "fetch all" scenario. Imagine
/// we have a base class `Base`, with subclasses `A`, `B`, and `C`. If we call
/// `Base.fetchAll()`, the subclass initializers will never be invoked.
/// Moreover, in this example it's not clear which initializer *should* be
/// invoked for a given fetched row.
///
/// As mentioned above, factory initialization is one pattern that allows us to
/// work around these issues and ensure subclasses are correctly initialized.
///
/// [0]: https://github.com/apple/swift/issues/61946
public protocol SDSCodableModel: AnyObject, Encodable, FetchableRecord, PersistableRecord, SDSIdentifiableModel {
    associatedtype CodingKeys: RawRepresentable<String>, CodingKey, ColumnExpression
    typealias Columns = CodingKeys
    typealias RowId = Int64

    var id: RowId? { get set }

    var uniqueId: String { get }

    func anyWillInsert(transaction: DBWriteTransaction)
    func anyDidInsert(transaction: DBWriteTransaction)
    func anyWillUpdate(transaction: DBWriteTransaction)
    func anyDidUpdate(transaction: DBWriteTransaction)
    func anyWillRemove(transaction: DBWriteTransaction)
    func anyDidRemove(transaction: DBWriteTransaction)
    func anyDidFetchOne(transaction: DBReadTransaction)
    func anyDidEnumerateOne(transaction: DBReadTransaction)
}

public extension SDSCodableModel {

    var grdbId: NSNumber? { id.map { NSNumber(value: $0) } }

    var sdsTableName: String { Self.databaseTableName }

    func anyWillInsert(transaction: DBWriteTransaction) {}
    func anyDidInsert(transaction: DBWriteTransaction) {}
    func anyWillUpdate(transaction: DBWriteTransaction) {}
    func anyDidUpdate(transaction: DBWriteTransaction) {}
    func anyWillRemove(transaction: DBWriteTransaction) {}
    func anyDidRemove(transaction: DBWriteTransaction) {}
    func anyDidFetchOne(transaction: DBReadTransaction) {}
    func anyDidEnumerateOne(transaction: DBReadTransaction) {}

    static var databaseUUIDEncodingStrategy: DatabaseUUIDEncodingStrategy { .uppercaseString }
    static var databaseDateEncodingStrategy: DatabaseDateEncodingStrategy { .timeIntervalSince1970 }
    static var databaseDateDecodingStrategy: DatabaseDateDecodingStrategy { .timeIntervalSince1970 }

    func didInsert(with rowID: Int64, for column: String?) {
        id = rowID
    }
}

public extension SDSCodableModel {
    static func anyCount(
        transaction: DBReadTransaction,
    ) -> UInt {
        SDSCodableModelDatabaseInterfaceImpl().countAllModels(
            modelType: Self.self,
            transaction: transaction,
        )
    }

    /// Convenience method delegating to ``SDSCodableModelDatabaseInterface``.
    /// See that class for details.
    static func anyFetch(
        rowId: Int64,
        transaction: DBReadTransaction,
    ) -> Self? {
        SDSCodableModelDatabaseInterfaceImpl().fetchModel(
            modelType: Self.self,
            rowId: rowId,
            tx: transaction,
        )
    }

    /// Convenience method delegating to ``SDSCodableModelDatabaseInterface``.
    /// See that class for details.
    static func anyFetch(
        uniqueId: String,
        transaction: DBReadTransaction,
    ) -> Self? {
        SDSCodableModelDatabaseInterfaceImpl().fetchModel(
            modelType: Self.self,
            uniqueId: uniqueId,
            transaction: transaction,
        )
    }

    static func anyFetch(
        sql: String,
        arguments: StatementArguments = [],
        transaction: DBReadTransaction,
    ) -> Self? {
        SDSCodableModelDatabaseInterfaceImpl().fetchModel(
            modelType: Self.self,
            sql: sql,
            arguments: arguments,
            transaction: transaction,
        )
    }

    /// Convenience method delegating to ``SDSCodableModelDatabaseInterface``.
    /// See that class for details.
    static func anyFetchAll(
        transaction: DBReadTransaction,
    ) -> [Self] {
        SDSCodableModelDatabaseInterfaceImpl().fetchAllModels(
            modelType: Self.self,
            transaction: transaction,
        )
    }

    /// Convenience method delegating to ``SDSCodableModelDatabaseInterface``.
    /// See that class for details.
    func anyInsert(transaction: DBWriteTransaction) {
        SDSCodableModelDatabaseInterfaceImpl().insertModel(self, transaction: transaction)
    }

    /// Convenience method delegating to ``SDSCodableModelDatabaseInterface``.
    /// See that class for details.
    func anyUpsert(transaction: DBWriteTransaction) {
        SDSCodableModelDatabaseInterfaceImpl().upsertModel(self, transaction: transaction)
    }

    /// Convenience method delegating to ``SDSCodableModelDatabaseInterface``.
    /// See that class for details.
    func anyOverwritingUpdate(transaction: DBWriteTransaction) {
        SDSCodableModelDatabaseInterfaceImpl().overwritingUpdateModel(self, transaction: transaction)
    }

    /// Convenience method delegating to ``SDSCodableModelDatabaseInterface``.
    /// See that class for details.
    func anyRemove(transaction: DBWriteTransaction) {
        SDSCodableModelDatabaseInterfaceImpl().removeModel(self, transaction: transaction)
    }
}

public extension SDSCodableModel where Self: AnyObject {
    /// Convenience method delegating to ``SDSCodableModelDatabaseInterface``.
    /// See that class for details.
    func anyUpdate(transaction: DBWriteTransaction, block: (Self) -> Void) {
        SDSCodableModelDatabaseInterfaceImpl().updateModel(
            self,
            transaction: transaction,
            block: block,
        )
    }
}

public extension SDSCodableModel {
    /// Convenience method delegating to ``SDSCodableModelDatabaseInterface``.
    /// See that class for details.
    static func anyEnumerate(
        transaction: DBReadTransaction,
        batchingPreference: BatchingPreference = .unbatched,
        block: (Self, inout Bool) -> Void,
    ) {
        SDSCodableModelDatabaseInterfaceImpl().enumerateModels(
            modelType: Self.self,
            transaction: transaction,
            batchingPreference: batchingPreference,
            block: block,
        )
    }

    /// Convenience method delegating to ``SDSCodableModelDatabaseInterface``.
    /// See that class for details.
    static func anyEnumerate(
        transaction: DBReadTransaction,
        sql: String,
        arguments: StatementArguments,
        block: (Self, inout Bool) -> Void,
    ) {
        SDSCodableModelDatabaseInterfaceImpl().enumerateModels(
            modelType: Self.self,
            transaction: transaction,
            sql: sql,
            arguments: arguments,
            batchingPreference: .unbatched,
            block: block,
        )
    }
}