285 lines
11 KiB
Swift
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
|
|
)
|
|
}
|
|
}
|