TM-SGNL-iOS/SignalServiceKit/Storage/Database/SDSCodableModel/SDSCodableModel.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

285 lines
11 KiB
Swift

//
// 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: Encodable, FetchableRecord, PersistableRecord, SDSIdentifiableModel {
associatedtype CodingKeys: RawRepresentable<String>, CodingKey, ColumnExpression, CaseIterable
typealias Columns = CodingKeys
typealias RowId = Int64
var id: RowId? { get set }
/// For compatibility with legacy SDS codegen (see ``SDSRecord`` and
/// friends). Subclasses should override to differentiate their records
/// from parent classes.
///
/// See ``NeedsFactoryInitializationFromRecordType`` for more details on
/// models with inheritance, and how that intersects with `recordType`.
///
/// Models using ``SDSCodableModel`` that never used codegen (i.e., were
/// written from the start using ``SDSCodableModel``, rather than migrated)
/// and which do not use inheritance may set this to zero. If they choose
/// to do so, they may never in the future use inheritance.
static var recordType: UInt { get }
var uniqueId: String { get }
var shouldBeSaved: Bool { get }
func anyWillInsert(transaction: SDSAnyWriteTransaction)
func anyDidInsert(transaction: SDSAnyWriteTransaction)
func anyWillUpdate(transaction: SDSAnyWriteTransaction)
func anyDidUpdate(transaction: SDSAnyWriteTransaction)
func anyWillRemove(transaction: SDSAnyWriteTransaction)
func anyDidRemove(transaction: SDSAnyWriteTransaction)
func anyDidFetchOne(transaction: SDSAnyReadTransaction)
func anyDidEnumerateOne(transaction: SDSAnyReadTransaction)
}
public extension SDSCodableModel {
var grdbId: NSNumber? { id.map { NSNumber(value: $0) } }
var shouldBeSaved: Bool { true }
var sdsTableName: String { Self.databaseTableName }
func anyWillInsert(transaction: SDSAnyWriteTransaction) {}
func anyDidInsert(transaction: SDSAnyWriteTransaction) {}
func anyWillUpdate(transaction: SDSAnyWriteTransaction) {}
func anyDidUpdate(transaction: SDSAnyWriteTransaction) {}
func anyWillRemove(transaction: SDSAnyWriteTransaction) {}
func anyDidRemove(transaction: SDSAnyWriteTransaction) {}
func anyDidFetchOne(transaction: SDSAnyReadTransaction) {}
func anyDidEnumerateOne(transaction: SDSAnyReadTransaction) {}
static var databaseUUIDEncodingStrategy: DatabaseUUIDEncodingStrategy { .uppercaseString }
static var databaseDateEncodingStrategy: DatabaseDateEncodingStrategy { .timeIntervalSince1970 }
static var databaseDateDecodingStrategy: DatabaseDateDecodingStrategy { .timeIntervalSince1970 }
mutating func didInsert(with rowID: Int64, for column: String?) {
id = rowID
}
}
public extension SDSCodableModel {
static func anyCount(
transaction: SDSAnyReadTransaction
) -> UInt {
SDSCodableModelDatabaseInterfaceImpl().countAllModels(
modelType: Self.self,
transaction: transaction.asV2Read
)
}
/// Convenience method delegating to ``SDSCodableModelDatabaseInterface``.
/// See that class for details.
static func anyFetch(
rowId: Int64,
transaction: SDSAnyReadTransaction
) -> Self? {
SDSCodableModelDatabaseInterfaceImpl().fetchModel(
modelType: Self.self,
rowId: rowId,
tx: transaction.asV2Read
)
}
/// Convenience method delegating to ``SDSCodableModelDatabaseInterface``.
/// See that class for details.
static func anyFetch(
uniqueId: String,
transaction: SDSAnyReadTransaction
) -> Self? {
SDSCodableModelDatabaseInterfaceImpl().fetchModel(
modelType: Self.self,
uniqueId: uniqueId,
transaction: transaction.asV2Read
)
}
static func anyFetch(
sql: String,
arguments: StatementArguments = [],
transaction: SDSAnyReadTransaction
) -> Self? {
SDSCodableModelDatabaseInterfaceImpl().fetchModel(
modelType: Self.self,
sql: sql,
arguments: arguments,
transaction: transaction.asV2Read
)
}
/// Convenience method delegating to ``SDSCodableModelDatabaseInterface``.
/// See that class for details.
static func anyFetchAll(
transaction: SDSAnyReadTransaction
) -> [Self] {
SDSCodableModelDatabaseInterfaceImpl().fetchAllModels(
modelType: Self.self,
transaction: transaction.asV2Read
)
}
/// Convenience method delegating to ``SDSCodableModelDatabaseInterface``.
/// See that class for details.
func anyInsert(transaction: SDSAnyWriteTransaction) {
SDSCodableModelDatabaseInterfaceImpl().insertModel(self, transaction: transaction.asV2Write)
}
/// Convenience method delegating to ``SDSCodableModelDatabaseInterface``.
/// See that class for details.
func anyUpsert(transaction: SDSAnyWriteTransaction) {
SDSCodableModelDatabaseInterfaceImpl().upsertModel(self, transaction: transaction.asV2Write)
}
/// Convenience method delegating to ``SDSCodableModelDatabaseInterface``.
/// See that class for details.
func anyOverwritingUpdate(transaction: SDSAnyWriteTransaction) {
SDSCodableModelDatabaseInterfaceImpl().overwritingUpdateModel(self, transaction: transaction.asV2Write)
}
/// Convenience method delegating to ``SDSCodableModelDatabaseInterface``.
/// See that class for details.
func anyRemove(transaction: SDSAnyWriteTransaction) {
SDSCodableModelDatabaseInterfaceImpl().removeModel(self, transaction: transaction.asV2Write)
}
}
public extension SDSCodableModel where Self: AnyObject {
/// Convenience method delegating to ``SDSCodableModelDatabaseInterface``.
/// See that class for details.
func anyUpdate(transaction: SDSAnyWriteTransaction, block: (Self) -> Void) {
SDSCodableModelDatabaseInterfaceImpl().updateModel(
self,
transaction: transaction.asV2Write,
block: block
)
}
static func anyRemoveAllWithInstantiation(transaction: SDSAnyWriteTransaction) {
SDSCodableModelDatabaseInterfaceImpl().removeAllModelsWithInstantiation(
modelType: Self.self,
transaction: transaction.asV2Write
)
}
}
public extension SDSCodableModel {
/// Convenience method delegating to ``SDSCodableModelDatabaseInterface``.
/// See that class for details.
static func anyEnumerate(
transaction: SDSAnyReadTransaction,
batchingPreference: BatchingPreference = .unbatched,
block: (Self, UnsafeMutablePointer<ObjCBool>) -> Void
) {
SDSCodableModelDatabaseInterfaceImpl().enumerateModels(
modelType: Self.self,
transaction: transaction.asV2Read,
batchingPreference: batchingPreference,
block: block
)
}
/// Convenience method delegating to ``SDSCodableModelDatabaseInterface``.
/// See that class for details.
static func anyEnumerateUniqueIds(
transaction: SDSAnyReadTransaction,
batched: Bool = false,
block: (String, UnsafeMutablePointer<ObjCBool>) -> Void
) {
SDSCodableModelDatabaseInterfaceImpl().enumerateModelUniqueIds(
modelType: Self.self,
transaction: transaction.asV2Read,
batched: batched,
block: block
)
}
/// Convenience method delegating to ``SDSCodableModelDatabaseInterface``.
/// See that class for details.
static func anyEnumerate(
transaction: SDSAnyReadTransaction,
sql: String,
arguments: StatementArguments,
block: (Self, UnsafeMutablePointer<ObjCBool>) -> Void
) {
SDSCodableModelDatabaseInterfaceImpl().enumerateModels(
modelType: Self.self,
transaction: transaction.asV2Read,
sql: sql,
arguments: arguments,
batchingPreference: .unbatched,
block: block
)
}
}