// // 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: 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, 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, ) } }