// // Copyright 2019 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // import Foundation import GRDB public protocol SDSRecord: Codable, FetchableRecord, PersistableRecord { var delegate: SDSRecordDelegate? { get set } var id: Int64? { get set } var uniqueId: String { get } var tableMetadata: SDSTableMetadata { get } } // MARK: - Save (Upsert) public enum SDSSaveMode { case insert case update } public extension SDSRecord { private var uniqueIdColumnName: String { return "uniqueId" } private var uniqueIdColumnValue: String { return self.uniqueId } // This is a "fault-tolerant" save method that will upsert in production. // In DEBUG builds it will fail if the intention (insert v. update) // doesn't match the database contents. func sdsSave(saveMode: SDSSaveMode, transaction: GRDBWriteTransaction) { // GRDB TODO: the record has an id property, but we can't use it here // until we modify the upsert logic. // grdbIdByUniqueId() verifies that the model hasn't been // deleted from the db. if let grdbId: Int64 = grdbIdByUniqueId(transaction: transaction) { if saveMode == .insert { owsFailDebug("Could not insert existing record.") } sdsUpdate(grdbId: grdbId, transaction: transaction) } else { if saveMode == .update { owsFailDebug("Could not update missing record.") } sdsInsert(transaction: transaction) } } private func sdsUpdate(grdbId: Int64, transaction: GRDBWriteTransaction) { do { var recordCopy = self recordCopy.id = grdbId try recordCopy.update(transaction.database) } catch { DatabaseCorruptionState.flagDatabaseCorruptionIfNecessary( userDefaults: CurrentAppContext().appUserDefaults(), error: error ) owsFail("Update failed: \(error.grdbErrorForLogging)") } } private func sdsInsert(transaction: GRDBWriteTransaction) { do { try self.insert(transaction.database) } catch { DatabaseCorruptionState.flagDatabaseCorruptionIfNecessary( userDefaults: CurrentAppContext().appUserDefaults(), error: error ) owsFail("Insert failed: \(error.grdbErrorForLogging)") } } func sdsRemove(transaction: GRDBWriteTransaction) { do { let tableName = tableMetadata.tableName let whereSQL = "\(uniqueIdColumnName.quotedDatabaseIdentifier)=?" let sql: String = "DELETE FROM \(tableName.quotedDatabaseIdentifier) WHERE \(whereSQL)" let statement = try transaction.database.cachedStatement(sql: sql) try statement.setArguments([uniqueIdColumnValue]) try statement.execute() } catch { DatabaseCorruptionState.flagDatabaseCorruptionIfNecessary( userDefaults: CurrentAppContext().appUserDefaults(), error: error ) owsFail("Write failed: \(error.grdbErrorForLogging)") } } } // MARK: - fileprivate extension SDSRecord { func grdbIdByUniqueId(transaction: GRDBReadTransaction) -> Int64? { BaseModel.grdbIdByUniqueId(tableMetadata: tableMetadata, uniqueIdColumnName: uniqueIdColumnName, uniqueIdColumnValue: uniqueIdColumnValue, transaction: transaction) } } // MARK: - extension BaseModel { static func grdbIdByUniqueId(tableMetadata: SDSTableMetadata, uniqueIdColumnName: String, uniqueIdColumnValue: String, transaction: GRDBReadTransaction) -> Int64? { do { let tableName = tableMetadata.tableName let sql = "SELECT id FROM \(tableName.quotedDatabaseIdentifier) WHERE \(uniqueIdColumnName.quotedDatabaseIdentifier)=?" guard let value = try Int64.fetchOne(transaction.database, sql: sql, arguments: [uniqueIdColumnValue]) else { return nil } return value } catch { DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( userDefaults: CurrentAppContext().appUserDefaults(), error: error ) owsFailDebug("Could not find grdb id: \(error)") return nil } } }