Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/SignalServiceKit/Storage/Database/SDSRecord.swift
1 views
//
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import Foundation
public 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: DBWriteTransaction,
    ) {
        // 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: DBWriteTransaction) {
        failIfThrows {
            var recordCopy = self
            recordCopy.id = grdbId
            try recordCopy.update(transaction.database)
        }
    }

    private func sdsInsert(transaction: DBWriteTransaction) {
        failIfThrows {
            try self.insert(transaction.database)
        }
    }
}

// MARK: -

private extension SDSRecord {

    func grdbIdByUniqueId(transaction: DBReadTransaction) -> Int64? {
        BaseModel.grdbIdByUniqueId(
            tableMetadata: tableMetadata,
            uniqueIdColumnName: uniqueIdColumnName,
            uniqueIdColumnValue: uniqueIdColumnValue,
            transaction: transaction,
        )
    }
}

// MARK: -

extension BaseModel {
    static func grdbIdByUniqueId(
        tableMetadata: SDSTableMetadata,
        uniqueIdColumnName: String,
        uniqueIdColumnValue: String,
        transaction: DBReadTransaction,
    ) -> Int64? {
        return failIfThrows {
            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
        }
    }
}