// // Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // extension SDSCodableModelDatabaseInterfaceImpl { /// Insert the given model to the database. func insertModel( _ model: Model, transaction: DBWriteTransaction ) { 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: 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: 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: 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: Model, transaction: DBWriteTransaction ) { saveModelToDatabase(model, saveMode: .update, transaction: transaction) } } // MARK: - Helpers private extension SDSCodableModelDatabaseInterface { /// Get the row ID of this model if it has already been persisted. func existingGrdbRowId( forModel model: Model, transaction: DBReadTransaction ) -> 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: Model, saveMode: SDSSaveMode, transaction: DBWriteTransaction ) { 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 ) 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: Model, saveMode: SDSSaveMode, transaction: DBWriteTransaction ) { 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: Model, existingGrdbRowId: SDSCodableModel.RowId, transaction: DBWriteTransaction ) { failIfThrows { var recordCopy = model recordCopy.id = existingGrdbRowId try recordCopy.update(transaction.database) } } func insertToDatabase( model: Model, transaction: DBWriteTransaction ) { failIfThrows { try model.insert(transaction.database) } } }