229 lines
7.5 KiB
Swift
229 lines
7.5 KiB
Swift
//
|
|
// Copyright 2023 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
extension SDSCodableModelDatabaseInterfaceImpl {
|
|
|
|
/// Insert the given model to the database.
|
|
func insertModel<Model: SDSCodableModel>(
|
|
_ model: Model,
|
|
transaction: DBWriteTransaction
|
|
) {
|
|
let transaction = SDSDB.shimOnlyBridge(transaction)
|
|
saveModelToDatabase(model, saveMode: .insert, transaction: transaction)
|
|
}
|
|
|
|
/// If a persisted record exists for this model, do an overwriting update.
|
|
/// Otherwise, do an insertion.
|
|
///
|
|
/// When possible, avoid this method in preference of an explicit insert or
|
|
/// update.
|
|
func upsertModel<Model: SDSCodableModel>(
|
|
_ model: Model,
|
|
transaction: DBWriteTransaction
|
|
) {
|
|
let shouldInsert: Bool = fetchModel(
|
|
modelType: Model.self,
|
|
uniqueId: model.uniqueId,
|
|
transaction: transaction
|
|
) == nil
|
|
|
|
if shouldInsert {
|
|
insertModel(model, transaction: transaction)
|
|
} else {
|
|
overwritingUpdateModel(model, transaction: transaction)
|
|
}
|
|
}
|
|
|
|
/// Apply changes produced by the given block to the persisted copy of the
|
|
/// given model.
|
|
///
|
|
/// Used by `updateWith...` methods.
|
|
///
|
|
/// This model may be updated from many threads. We don't want to save this
|
|
/// instance, since it may be out of date. We also want to avoid re-saving
|
|
/// a model that has been deleted. Therefore, this method:
|
|
///
|
|
/// a) Updates the passed instance using the given block.
|
|
/// b) If a copy of the model exists in the database (which will be
|
|
/// up-to-date), load it, update it, and save that copy.
|
|
/// c) If a copy of the model does *not* exist in the database, do *not*
|
|
/// save the passed instance.
|
|
///
|
|
/// Afterwards:
|
|
///
|
|
/// a) Any copy of this model in the database will be updated.
|
|
/// b) The passed instance will be updated.
|
|
/// c) Other properties on the passed instance may be out of date.
|
|
func updateModel<Model: SDSCodableModel & AnyObject>(
|
|
_ model: Model,
|
|
transaction: DBWriteTransaction,
|
|
block: (Model) -> Void
|
|
) {
|
|
block(model)
|
|
|
|
guard let dbCopy: Model = fetchModel(
|
|
modelType: Model.self,
|
|
uniqueId: model.uniqueId,
|
|
transaction: transaction
|
|
) else {
|
|
return
|
|
}
|
|
|
|
// Don't apply the block twice to the same instance. At best it's
|
|
// unnecessary, and at worst it's wrong: `block: { $0.someField++ }`.
|
|
if dbCopy !== model {
|
|
block(dbCopy)
|
|
}
|
|
|
|
saveModelToDatabase(dbCopy, saveMode: .update, transaction: SDSDB.shimOnlyBridge(transaction))
|
|
}
|
|
|
|
/// Immediately persist the given model.
|
|
///
|
|
/// A faster alternative to ``updateModel(_:transaction:block:)`` that will
|
|
/// clobber columns modified by concurrent updates.
|
|
///
|
|
/// Safe to use if we are sure the model in question is up-to-date and not
|
|
/// being updated concurrently, such as when the model was just loaded in
|
|
/// the same transaction.
|
|
func overwritingUpdateModel<Model: SDSCodableModel>(
|
|
_ model: Model,
|
|
transaction: DBWriteTransaction
|
|
) {
|
|
saveModelToDatabase(model, saveMode: .update, transaction: SDSDB.shimOnlyBridge(transaction))
|
|
}
|
|
}
|
|
|
|
// MARK: - Helpers
|
|
|
|
private extension SDSCodableModelDatabaseInterface {
|
|
|
|
/// Get the row ID of this model if it has already been persisted.
|
|
func existingGrdbRowId<Model: SDSCodableModel>(
|
|
forModel model: Model,
|
|
transaction: GRDBReadTransaction
|
|
) -> SDSCodableModel.RowId? {
|
|
do {
|
|
let databaseTableName = Model.databaseTableName.quotedDatabaseIdentifier
|
|
let sql: String = """
|
|
SELECT id FROM \(databaseTableName)
|
|
WHERE uniqueId = ?
|
|
"""
|
|
|
|
return try SDSCodableModel.RowId.fetchOne(
|
|
transaction.database,
|
|
sql: sql,
|
|
arguments: [model.uniqueId]
|
|
)
|
|
} catch let error {
|
|
owsFailDebug("Failed to fetch GRDB row ID for uniqueId: \(error)")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
/// Persist the given model using the given mode.
|
|
///
|
|
/// - Parameter saveMode
|
|
/// The mode to use when saving. If this mode does not match persisted
|
|
/// state, the appropriate mode will be used instead. For example, if
|
|
/// `.insert` is given, but we already have a persisted record for this
|
|
/// model, `.update` will be used instead. (And vice versa.)
|
|
func saveModelToDatabase<Model: SDSCodableModel>(
|
|
_ model: Model,
|
|
saveMode: SDSSaveMode,
|
|
transaction: SDSAnyWriteTransaction
|
|
) {
|
|
guard model.shouldBeSaved else {
|
|
Logger.warn("Skipping save of: \(Model.self).")
|
|
return
|
|
}
|
|
|
|
switch saveMode {
|
|
case .insert:
|
|
model.anyWillInsert(transaction: transaction)
|
|
case .update:
|
|
model.anyWillUpdate(transaction: transaction)
|
|
}
|
|
|
|
faultTolerantSaveModelToDatabase(
|
|
model,
|
|
saveMode: saveMode,
|
|
transaction: transaction.unwrapGrdbWrite
|
|
)
|
|
|
|
switch saveMode {
|
|
case .insert:
|
|
model.anyDidInsert(transaction: transaction)
|
|
case .update:
|
|
model.anyDidUpdate(transaction: transaction)
|
|
}
|
|
}
|
|
|
|
/// "Fault-tolerant" save.
|
|
///
|
|
/// Upserts in production, triggers asserts in debug builds if the passed
|
|
/// `saveMode` does not align with database contents.
|
|
func faultTolerantSaveModelToDatabase<Model: SDSCodableModel>(
|
|
_ model: Model,
|
|
saveMode: SDSSaveMode,
|
|
transaction: GRDBWriteTransaction
|
|
) {
|
|
if let existingGrdbRowId = existingGrdbRowId(forModel: model, transaction: transaction) {
|
|
owsAssertDebug(
|
|
saveMode == .update,
|
|
"Could not insert existing record - updating instead."
|
|
)
|
|
|
|
updateModelInDatabase(
|
|
model,
|
|
existingGrdbRowId: existingGrdbRowId,
|
|
transaction: transaction
|
|
)
|
|
} else {
|
|
owsAssertDebug(
|
|
saveMode == .insert,
|
|
"Could not update non-existent record - inserting instead."
|
|
)
|
|
|
|
insertToDatabase(model: model, transaction: transaction)
|
|
}
|
|
}
|
|
|
|
func updateModelInDatabase<Model: SDSCodableModel>(
|
|
_ model: Model,
|
|
existingGrdbRowId: SDSCodableModel.RowId,
|
|
transaction: GRDBWriteTransaction
|
|
) {
|
|
do {
|
|
var recordCopy = model
|
|
recordCopy.id = existingGrdbRowId
|
|
|
|
try recordCopy.update(transaction.database)
|
|
} catch let error {
|
|
DatabaseCorruptionState.flagDatabaseCorruptionIfNecessary(
|
|
userDefaults: CurrentAppContext().appUserDefaults(),
|
|
error: error
|
|
)
|
|
|
|
owsFail("Update failed: \(error.grdbErrorForLogging)")
|
|
}
|
|
}
|
|
|
|
func insertToDatabase<Model: SDSCodableModel>(
|
|
model: Model,
|
|
transaction: GRDBWriteTransaction
|
|
) {
|
|
do {
|
|
try model.insert(transaction.database)
|
|
} catch let error {
|
|
DatabaseCorruptionState.flagDatabaseCorruptionIfNecessary(
|
|
userDefaults: CurrentAppContext().appUserDefaults(),
|
|
error: error
|
|
)
|
|
|
|
owsFail("Insert failed: \(error.grdbErrorForLogging)")
|
|
}
|
|
}
|
|
}
|