//
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import Foundation
import GRDB
import SignalCoreKit

// NOTE: This file is generated by /Scripts/sds_codegen/sds_generate.py.
// Do not manually edit it, instead run `sds_codegen.sh`.

// MARK: - Record

public struct ThreadRecord: SDSRecord {
    public weak var delegate: SDSRecordDelegate?

    public var tableMetadata: SDSTableMetadata {
        TSThreadSerializer.table
    }

    public static var databaseTableName: String {
        TSThreadSerializer.table.tableName
    }

    public var id: Int64?

    // This defines all of the columns used in the table
    // where this model (and any subclasses) are persisted.
    public let recordType: SDSRecordType
    public let uniqueId: String

    // Properties
    public let conversationColorName: String
    public let creationDate: Double?
    public let isArchived: Bool
    public let lastInteractionRowId: UInt64
    public let messageDraft: String?
    public let mutedUntilDate: Double?
    public let shouldThreadBeVisible: Bool
    public let contactPhoneNumber: String?
    public let contactUUID: String?
    public let groupModel: Data?
    public let hasDismissedOffers: Bool?
    public let isMarkedUnread: Bool
    public let lastVisibleSortIdOnScreenPercentage: Double
    public let lastVisibleSortId: UInt64
    public let messageDraftBodyRanges: Data?
    public let mentionNotificationMode: UInt
    public let mutedUntilTimestamp: UInt64
    public let allowsReplies: Bool?
    public let lastSentStoryTimestamp: UInt64?
    public let name: String?
    public let addresses: Data?
    public let storyViewMode: UInt
    public let editTargetTimestamp: UInt64?

    public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable {
        case id
        case recordType
        case uniqueId
        case conversationColorName
        case creationDate
        case isArchived
        case lastInteractionRowId
        case messageDraft
        case mutedUntilDate
        case shouldThreadBeVisible
        case contactPhoneNumber
        case contactUUID
        case groupModel
        case hasDismissedOffers
        case isMarkedUnread
        case lastVisibleSortIdOnScreenPercentage
        case lastVisibleSortId
        case messageDraftBodyRanges
        case mentionNotificationMode
        case mutedUntilTimestamp
        case allowsReplies
        case lastSentStoryTimestamp
        case name
        case addresses
        case storyViewMode
        case editTargetTimestamp
    }

    public static func columnName(_ column: ThreadRecord.CodingKeys, fullyQualified: Bool = false) -> String {
        fullyQualified ? "\(databaseTableName).\(column.rawValue)" : column.rawValue
    }

    public func didInsert(with rowID: Int64, for column: String?) {
        guard let delegate = delegate else {
            owsFailDebug("Missing delegate.")
            return
        }
        delegate.updateRowId(rowID)
    }
}

// MARK: - Row Initializer

public extension ThreadRecord {
    static var databaseSelection: [SQLSelectable] {
        CodingKeys.allCases
    }

    init(row: Row) {
        id = row[0]
        recordType = row[1]
        uniqueId = row[2]
        conversationColorName = row[3]
        creationDate = row[4]
        isArchived = row[5]
        lastInteractionRowId = row[6]
        messageDraft = row[7]
        mutedUntilDate = row[8]
        shouldThreadBeVisible = row[9]
        contactPhoneNumber = row[10]
        contactUUID = row[11]
        groupModel = row[12]
        hasDismissedOffers = row[13]
        isMarkedUnread = row[14]
        lastVisibleSortIdOnScreenPercentage = row[15]
        lastVisibleSortId = row[16]
        messageDraftBodyRanges = row[17]
        mentionNotificationMode = row[18]
        mutedUntilTimestamp = row[19]
        allowsReplies = row[20]
        lastSentStoryTimestamp = row[21]
        name = row[22]
        addresses = row[23]
        storyViewMode = row[24]
        editTargetTimestamp = row[25]
    }
}

// MARK: - StringInterpolation

public extension String.StringInterpolation {
    mutating func appendInterpolation(threadColumn column: ThreadRecord.CodingKeys) {
        appendLiteral(ThreadRecord.columnName(column))
    }
    mutating func appendInterpolation(threadColumnFullyQualified column: ThreadRecord.CodingKeys) {
        appendLiteral(ThreadRecord.columnName(column, fullyQualified: true))
    }
}

// MARK: - Deserialization

extension TSThread {
    // This method defines how to deserialize a model, given a
    // database row.  The recordType column is used to determine
    // the corresponding model class.
    class func fromRecord(_ record: ThreadRecord) throws -> TSThread {

        guard let recordId = record.id else {
            throw SDSError.invalidValue()
        }

        switch record.recordType {
        case .contactThread:

            let uniqueId: String = record.uniqueId
            let conversationColorNameObsolete: String = record.conversationColorName
            let creationDateInterval: Double? = record.creationDate
            let creationDate: Date? = SDSDeserialization.optionalDoubleAsDate(creationDateInterval, name: "creationDate")
            let editTargetTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.editTargetTimestamp, name: "editTargetTimestamp", conversion: { NSNumber(value: $0) })
            let isArchivedObsolete: Bool = record.isArchived
            let isMarkedUnreadObsolete: Bool = record.isMarkedUnread
            let lastInteractionRowId: UInt64 = record.lastInteractionRowId
            let lastSentStoryTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.lastSentStoryTimestamp, name: "lastSentStoryTimestamp", conversion: { NSNumber(value: $0) })
            let lastVisibleSortIdObsolete: UInt64 = record.lastVisibleSortId
            let lastVisibleSortIdOnScreenPercentageObsolete: Double = record.lastVisibleSortIdOnScreenPercentage
            let mentionNotificationMode: TSThreadMentionNotificationMode = TSThreadMentionNotificationMode(rawValue: record.mentionNotificationMode) ?? .default
            let messageDraft: String? = record.messageDraft
            let messageDraftBodyRangesSerialized: Data? = record.messageDraftBodyRanges
            let messageDraftBodyRanges: MessageBodyRanges? = try SDSDeserialization.optionalUnarchive(messageDraftBodyRangesSerialized, name: "messageDraftBodyRanges")
            let mutedUntilDateObsoleteInterval: Double? = record.mutedUntilDate
            let mutedUntilDateObsolete: Date? = SDSDeserialization.optionalDoubleAsDate(mutedUntilDateObsoleteInterval, name: "mutedUntilDateObsolete")
            let mutedUntilTimestampObsolete: UInt64 = record.mutedUntilTimestamp
            let shouldThreadBeVisible: Bool = record.shouldThreadBeVisible
            let storyViewMode: TSThreadStoryViewMode = TSThreadStoryViewMode(rawValue: record.storyViewMode) ?? .default
            let contactPhoneNumber: String? = record.contactPhoneNumber
            let contactUUID: String? = record.contactUUID
            let hasDismissedOffers: Bool = try SDSDeserialization.required(record.hasDismissedOffers, name: "hasDismissedOffers")

            return TSContactThread(grdbId: recordId,
                                   uniqueId: uniqueId,
                                   conversationColorNameObsolete: conversationColorNameObsolete,
                                   creationDate: creationDate,
                                   editTargetTimestamp: editTargetTimestamp,
                                   isArchivedObsolete: isArchivedObsolete,
                                   isMarkedUnreadObsolete: isMarkedUnreadObsolete,
                                   lastInteractionRowId: lastInteractionRowId,
                                   lastSentStoryTimestamp: lastSentStoryTimestamp,
                                   lastVisibleSortIdObsolete: lastVisibleSortIdObsolete,
                                   lastVisibleSortIdOnScreenPercentageObsolete: lastVisibleSortIdOnScreenPercentageObsolete,
                                   mentionNotificationMode: mentionNotificationMode,
                                   messageDraft: messageDraft,
                                   messageDraftBodyRanges: messageDraftBodyRanges,
                                   mutedUntilDateObsolete: mutedUntilDateObsolete,
                                   mutedUntilTimestampObsolete: mutedUntilTimestampObsolete,
                                   shouldThreadBeVisible: shouldThreadBeVisible,
                                   storyViewMode: storyViewMode,
                                   contactPhoneNumber: contactPhoneNumber,
                                   contactUUID: contactUUID,
                                   hasDismissedOffers: hasDismissedOffers)

        case .groupThread:

            let uniqueId: String = record.uniqueId
            let conversationColorNameObsolete: String = record.conversationColorName
            let creationDateInterval: Double? = record.creationDate
            let creationDate: Date? = SDSDeserialization.optionalDoubleAsDate(creationDateInterval, name: "creationDate")
            let editTargetTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.editTargetTimestamp, name: "editTargetTimestamp", conversion: { NSNumber(value: $0) })
            let isArchivedObsolete: Bool = record.isArchived
            let isMarkedUnreadObsolete: Bool = record.isMarkedUnread
            let lastInteractionRowId: UInt64 = record.lastInteractionRowId
            let lastSentStoryTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.lastSentStoryTimestamp, name: "lastSentStoryTimestamp", conversion: { NSNumber(value: $0) })
            let lastVisibleSortIdObsolete: UInt64 = record.lastVisibleSortId
            let lastVisibleSortIdOnScreenPercentageObsolete: Double = record.lastVisibleSortIdOnScreenPercentage
            let mentionNotificationMode: TSThreadMentionNotificationMode = TSThreadMentionNotificationMode(rawValue: record.mentionNotificationMode) ?? .default
            let messageDraft: String? = record.messageDraft
            let messageDraftBodyRangesSerialized: Data? = record.messageDraftBodyRanges
            let messageDraftBodyRanges: MessageBodyRanges? = try SDSDeserialization.optionalUnarchive(messageDraftBodyRangesSerialized, name: "messageDraftBodyRanges")
            let mutedUntilDateObsoleteInterval: Double? = record.mutedUntilDate
            let mutedUntilDateObsolete: Date? = SDSDeserialization.optionalDoubleAsDate(mutedUntilDateObsoleteInterval, name: "mutedUntilDateObsolete")
            let mutedUntilTimestampObsolete: UInt64 = record.mutedUntilTimestamp
            let shouldThreadBeVisible: Bool = record.shouldThreadBeVisible
            let storyViewMode: TSThreadStoryViewMode = TSThreadStoryViewMode(rawValue: record.storyViewMode) ?? .default
            let groupModelSerialized: Data? = record.groupModel
            let groupModel: TSGroupModel = try SDSDeserialization.unarchive(groupModelSerialized, name: "groupModel")

            return TSGroupThread(grdbId: recordId,
                                 uniqueId: uniqueId,
                                 conversationColorNameObsolete: conversationColorNameObsolete,
                                 creationDate: creationDate,
                                 editTargetTimestamp: editTargetTimestamp,
                                 isArchivedObsolete: isArchivedObsolete,
                                 isMarkedUnreadObsolete: isMarkedUnreadObsolete,
                                 lastInteractionRowId: lastInteractionRowId,
                                 lastSentStoryTimestamp: lastSentStoryTimestamp,
                                 lastVisibleSortIdObsolete: lastVisibleSortIdObsolete,
                                 lastVisibleSortIdOnScreenPercentageObsolete: lastVisibleSortIdOnScreenPercentageObsolete,
                                 mentionNotificationMode: mentionNotificationMode,
                                 messageDraft: messageDraft,
                                 messageDraftBodyRanges: messageDraftBodyRanges,
                                 mutedUntilDateObsolete: mutedUntilDateObsolete,
                                 mutedUntilTimestampObsolete: mutedUntilTimestampObsolete,
                                 shouldThreadBeVisible: shouldThreadBeVisible,
                                 storyViewMode: storyViewMode,
                                 groupModel: groupModel)

        case .privateStoryThread:

            let uniqueId: String = record.uniqueId
            let conversationColorNameObsolete: String = record.conversationColorName
            let creationDateInterval: Double? = record.creationDate
            let creationDate: Date? = SDSDeserialization.optionalDoubleAsDate(creationDateInterval, name: "creationDate")
            let editTargetTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.editTargetTimestamp, name: "editTargetTimestamp", conversion: { NSNumber(value: $0) })
            let isArchivedObsolete: Bool = record.isArchived
            let isMarkedUnreadObsolete: Bool = record.isMarkedUnread
            let lastInteractionRowId: UInt64 = record.lastInteractionRowId
            let lastSentStoryTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.lastSentStoryTimestamp, name: "lastSentStoryTimestamp", conversion: { NSNumber(value: $0) })
            let lastVisibleSortIdObsolete: UInt64 = record.lastVisibleSortId
            let lastVisibleSortIdOnScreenPercentageObsolete: Double = record.lastVisibleSortIdOnScreenPercentage
            let mentionNotificationMode: TSThreadMentionNotificationMode = TSThreadMentionNotificationMode(rawValue: record.mentionNotificationMode) ?? .default
            let messageDraft: String? = record.messageDraft
            let messageDraftBodyRangesSerialized: Data? = record.messageDraftBodyRanges
            let messageDraftBodyRanges: MessageBodyRanges? = try SDSDeserialization.optionalUnarchive(messageDraftBodyRangesSerialized, name: "messageDraftBodyRanges")
            let mutedUntilDateObsoleteInterval: Double? = record.mutedUntilDate
            let mutedUntilDateObsolete: Date? = SDSDeserialization.optionalDoubleAsDate(mutedUntilDateObsoleteInterval, name: "mutedUntilDateObsolete")
            let mutedUntilTimestampObsolete: UInt64 = record.mutedUntilTimestamp
            let shouldThreadBeVisible: Bool = record.shouldThreadBeVisible
            let storyViewMode: TSThreadStoryViewMode = TSThreadStoryViewMode(rawValue: record.storyViewMode) ?? .default
            let addressesSerialized: Data? = record.addresses
            let addresses: [SignalServiceAddress] = try SDSDeserialization.unarchive(addressesSerialized, name: "addresses")
            let allowsReplies: Bool = try SDSDeserialization.required(record.allowsReplies, name: "allowsReplies")
            let name: String = try SDSDeserialization.required(record.name, name: "name")

            return TSPrivateStoryThread(grdbId: recordId,
                                        uniqueId: uniqueId,
                                        conversationColorNameObsolete: conversationColorNameObsolete,
                                        creationDate: creationDate,
                                        editTargetTimestamp: editTargetTimestamp,
                                        isArchivedObsolete: isArchivedObsolete,
                                        isMarkedUnreadObsolete: isMarkedUnreadObsolete,
                                        lastInteractionRowId: lastInteractionRowId,
                                        lastSentStoryTimestamp: lastSentStoryTimestamp,
                                        lastVisibleSortIdObsolete: lastVisibleSortIdObsolete,
                                        lastVisibleSortIdOnScreenPercentageObsolete: lastVisibleSortIdOnScreenPercentageObsolete,
                                        mentionNotificationMode: mentionNotificationMode,
                                        messageDraft: messageDraft,
                                        messageDraftBodyRanges: messageDraftBodyRanges,
                                        mutedUntilDateObsolete: mutedUntilDateObsolete,
                                        mutedUntilTimestampObsolete: mutedUntilTimestampObsolete,
                                        shouldThreadBeVisible: shouldThreadBeVisible,
                                        storyViewMode: storyViewMode,
                                        addresses: addresses,
                                        allowsReplies: allowsReplies,
                                        name: name)

        case .thread:

            let uniqueId: String = record.uniqueId
            let conversationColorNameObsolete: String = record.conversationColorName
            let creationDateInterval: Double? = record.creationDate
            let creationDate: Date? = SDSDeserialization.optionalDoubleAsDate(creationDateInterval, name: "creationDate")
            let editTargetTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.editTargetTimestamp, name: "editTargetTimestamp", conversion: { NSNumber(value: $0) })
            let isArchivedObsolete: Bool = record.isArchived
            let isMarkedUnreadObsolete: Bool = record.isMarkedUnread
            let lastInteractionRowId: UInt64 = record.lastInteractionRowId
            let lastSentStoryTimestamp: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.lastSentStoryTimestamp, name: "lastSentStoryTimestamp", conversion: { NSNumber(value: $0) })
            let lastVisibleSortIdObsolete: UInt64 = record.lastVisibleSortId
            let lastVisibleSortIdOnScreenPercentageObsolete: Double = record.lastVisibleSortIdOnScreenPercentage
            let mentionNotificationMode: TSThreadMentionNotificationMode = TSThreadMentionNotificationMode(rawValue: record.mentionNotificationMode) ?? .default
            let messageDraft: String? = record.messageDraft
            let messageDraftBodyRangesSerialized: Data? = record.messageDraftBodyRanges
            let messageDraftBodyRanges: MessageBodyRanges? = try SDSDeserialization.optionalUnarchive(messageDraftBodyRangesSerialized, name: "messageDraftBodyRanges")
            let mutedUntilDateObsoleteInterval: Double? = record.mutedUntilDate
            let mutedUntilDateObsolete: Date? = SDSDeserialization.optionalDoubleAsDate(mutedUntilDateObsoleteInterval, name: "mutedUntilDateObsolete")
            let mutedUntilTimestampObsolete: UInt64 = record.mutedUntilTimestamp
            let shouldThreadBeVisible: Bool = record.shouldThreadBeVisible
            let storyViewMode: TSThreadStoryViewMode = TSThreadStoryViewMode(rawValue: record.storyViewMode) ?? .default

            return TSThread(grdbId: recordId,
                            uniqueId: uniqueId,
                            conversationColorNameObsolete: conversationColorNameObsolete,
                            creationDate: creationDate,
                            editTargetTimestamp: editTargetTimestamp,
                            isArchivedObsolete: isArchivedObsolete,
                            isMarkedUnreadObsolete: isMarkedUnreadObsolete,
                            lastInteractionRowId: lastInteractionRowId,
                            lastSentStoryTimestamp: lastSentStoryTimestamp,
                            lastVisibleSortIdObsolete: lastVisibleSortIdObsolete,
                            lastVisibleSortIdOnScreenPercentageObsolete: lastVisibleSortIdOnScreenPercentageObsolete,
                            mentionNotificationMode: mentionNotificationMode,
                            messageDraft: messageDraft,
                            messageDraftBodyRanges: messageDraftBodyRanges,
                            mutedUntilDateObsolete: mutedUntilDateObsolete,
                            mutedUntilTimestampObsolete: mutedUntilTimestampObsolete,
                            shouldThreadBeVisible: shouldThreadBeVisible,
                            storyViewMode: storyViewMode)

        default:
            owsFailDebug("Unexpected record type: \(record.recordType)")
            throw SDSError.invalidValue()
        }
    }
}

// MARK: - SDSModel

extension TSThread: SDSModel {
    public var serializer: SDSSerializer {
        // Any subclass can be cast to it's superclass,
        // so the order of this switch statement matters.
        // We need to do a "depth first" search by type.
        switch self {
        case let model as TSPrivateStoryThread:
            assert(type(of: model) == TSPrivateStoryThread.self)
            return TSPrivateStoryThreadSerializer(model: model)
        case let model as TSGroupThread:
            assert(type(of: model) == TSGroupThread.self)
            return TSGroupThreadSerializer(model: model)
        case let model as TSContactThread:
            assert(type(of: model) == TSContactThread.self)
            return TSContactThreadSerializer(model: model)
        default:
            return TSThreadSerializer(model: self)
        }
    }

    public func asRecord() throws -> SDSRecord {
        try serializer.asRecord()
    }

    public var sdsTableName: String {
        ThreadRecord.databaseTableName
    }

    public static var table: SDSTableMetadata {
        TSThreadSerializer.table
    }
}

// MARK: - DeepCopyable

extension TSThread: DeepCopyable {

    public func deepCopy() throws -> AnyObject {
        // Any subclass can be cast to it's superclass,
        // so the order of this switch statement matters.
        // We need to do a "depth first" search by type.
        guard let id = self.grdbId?.int64Value else {
            throw OWSAssertionError("Model missing grdbId.")
        }

        if let modelToCopy = self as? TSPrivateStoryThread {
            assert(type(of: modelToCopy) == TSPrivateStoryThread.self)
            let uniqueId: String = modelToCopy.uniqueId
            let conversationColorNameObsolete: String = modelToCopy.conversationColorNameObsolete
            let creationDate: Date? = modelToCopy.creationDate
            let editTargetTimestamp: NSNumber? = modelToCopy.editTargetTimestamp
            let isArchivedObsolete: Bool = modelToCopy.isArchivedObsolete
            let isMarkedUnreadObsolete: Bool = modelToCopy.isMarkedUnreadObsolete
            let lastInteractionRowId: UInt64 = modelToCopy.lastInteractionRowId
            let lastSentStoryTimestamp: NSNumber? = modelToCopy.lastSentStoryTimestamp
            let lastVisibleSortIdObsolete: UInt64 = modelToCopy.lastVisibleSortIdObsolete
            let lastVisibleSortIdOnScreenPercentageObsolete: Double = modelToCopy.lastVisibleSortIdOnScreenPercentageObsolete
            let mentionNotificationMode: TSThreadMentionNotificationMode = modelToCopy.mentionNotificationMode
            let messageDraft: String? = modelToCopy.messageDraft
            // NOTE: If this generates build errors, you made need to
            // modify DeepCopy.swift to support this type.
            //
            // That might mean:
            //
            // * Implement DeepCopyable for this type (e.g. a model).
            // * Modify DeepCopies.deepCopy() to support this type (e.g. a collection).
            let messageDraftBodyRanges: MessageBodyRanges?
            if let messageDraftBodyRangesForCopy = modelToCopy.messageDraftBodyRanges {
               messageDraftBodyRanges = try DeepCopies.deepCopy(messageDraftBodyRangesForCopy)
            } else {
               messageDraftBodyRanges = nil
            }
            let mutedUntilDateObsolete: Date? = modelToCopy.mutedUntilDateObsolete
            let mutedUntilTimestampObsolete: UInt64 = modelToCopy.mutedUntilTimestampObsolete
            let shouldThreadBeVisible: Bool = modelToCopy.shouldThreadBeVisible
            let storyViewMode: TSThreadStoryViewMode = modelToCopy.storyViewMode
            // NOTE: If this generates build errors, you made need to
            // implement DeepCopyable for this type in DeepCopy.swift.
            let addresses: [SignalServiceAddress] = try DeepCopies.deepCopy(modelToCopy.addresses)
            let allowsReplies: Bool = modelToCopy.allowsReplies
            let name: String = modelToCopy.name

            return TSPrivateStoryThread(grdbId: id,
                                        uniqueId: uniqueId,
                                        conversationColorNameObsolete: conversationColorNameObsolete,
                                        creationDate: creationDate,
                                        editTargetTimestamp: editTargetTimestamp,
                                        isArchivedObsolete: isArchivedObsolete,
                                        isMarkedUnreadObsolete: isMarkedUnreadObsolete,
                                        lastInteractionRowId: lastInteractionRowId,
                                        lastSentStoryTimestamp: lastSentStoryTimestamp,
                                        lastVisibleSortIdObsolete: lastVisibleSortIdObsolete,
                                        lastVisibleSortIdOnScreenPercentageObsolete: lastVisibleSortIdOnScreenPercentageObsolete,
                                        mentionNotificationMode: mentionNotificationMode,
                                        messageDraft: messageDraft,
                                        messageDraftBodyRanges: messageDraftBodyRanges,
                                        mutedUntilDateObsolete: mutedUntilDateObsolete,
                                        mutedUntilTimestampObsolete: mutedUntilTimestampObsolete,
                                        shouldThreadBeVisible: shouldThreadBeVisible,
                                        storyViewMode: storyViewMode,
                                        addresses: addresses,
                                        allowsReplies: allowsReplies,
                                        name: name)
        }

        if let modelToCopy = self as? TSGroupThread {
            assert(type(of: modelToCopy) == TSGroupThread.self)
            let uniqueId: String = modelToCopy.uniqueId
            let conversationColorNameObsolete: String = modelToCopy.conversationColorNameObsolete
            let creationDate: Date? = modelToCopy.creationDate
            let editTargetTimestamp: NSNumber? = modelToCopy.editTargetTimestamp
            let isArchivedObsolete: Bool = modelToCopy.isArchivedObsolete
            let isMarkedUnreadObsolete: Bool = modelToCopy.isMarkedUnreadObsolete
            let lastInteractionRowId: UInt64 = modelToCopy.lastInteractionRowId
            let lastSentStoryTimestamp: NSNumber? = modelToCopy.lastSentStoryTimestamp
            let lastVisibleSortIdObsolete: UInt64 = modelToCopy.lastVisibleSortIdObsolete
            let lastVisibleSortIdOnScreenPercentageObsolete: Double = modelToCopy.lastVisibleSortIdOnScreenPercentageObsolete
            let mentionNotificationMode: TSThreadMentionNotificationMode = modelToCopy.mentionNotificationMode
            let messageDraft: String? = modelToCopy.messageDraft
            // NOTE: If this generates build errors, you made need to
            // modify DeepCopy.swift to support this type.
            //
            // That might mean:
            //
            // * Implement DeepCopyable for this type (e.g. a model).
            // * Modify DeepCopies.deepCopy() to support this type (e.g. a collection).
            let messageDraftBodyRanges: MessageBodyRanges?
            if let messageDraftBodyRangesForCopy = modelToCopy.messageDraftBodyRanges {
               messageDraftBodyRanges = try DeepCopies.deepCopy(messageDraftBodyRangesForCopy)
            } else {
               messageDraftBodyRanges = nil
            }
            let mutedUntilDateObsolete: Date? = modelToCopy.mutedUntilDateObsolete
            let mutedUntilTimestampObsolete: UInt64 = modelToCopy.mutedUntilTimestampObsolete
            let shouldThreadBeVisible: Bool = modelToCopy.shouldThreadBeVisible
            let storyViewMode: TSThreadStoryViewMode = modelToCopy.storyViewMode
            // NOTE: If this generates build errors, you made need to
            // implement DeepCopyable for this type in DeepCopy.swift.
            let groupModel: TSGroupModel = try DeepCopies.deepCopy(modelToCopy.groupModel)

            return TSGroupThread(grdbId: id,
                                 uniqueId: uniqueId,
                                 conversationColorNameObsolete: conversationColorNameObsolete,
                                 creationDate: creationDate,
                                 editTargetTimestamp: editTargetTimestamp,
                                 isArchivedObsolete: isArchivedObsolete,
                                 isMarkedUnreadObsolete: isMarkedUnreadObsolete,
                                 lastInteractionRowId: lastInteractionRowId,
                                 lastSentStoryTimestamp: lastSentStoryTimestamp,
                                 lastVisibleSortIdObsolete: lastVisibleSortIdObsolete,
                                 lastVisibleSortIdOnScreenPercentageObsolete: lastVisibleSortIdOnScreenPercentageObsolete,
                                 mentionNotificationMode: mentionNotificationMode,
                                 messageDraft: messageDraft,
                                 messageDraftBodyRanges: messageDraftBodyRanges,
                                 mutedUntilDateObsolete: mutedUntilDateObsolete,
                                 mutedUntilTimestampObsolete: mutedUntilTimestampObsolete,
                                 shouldThreadBeVisible: shouldThreadBeVisible,
                                 storyViewMode: storyViewMode,
                                 groupModel: groupModel)
        }

        if let modelToCopy = self as? TSContactThread {
            assert(type(of: modelToCopy) == TSContactThread.self)
            let uniqueId: String = modelToCopy.uniqueId
            let conversationColorNameObsolete: String = modelToCopy.conversationColorNameObsolete
            let creationDate: Date? = modelToCopy.creationDate
            let editTargetTimestamp: NSNumber? = modelToCopy.editTargetTimestamp
            let isArchivedObsolete: Bool = modelToCopy.isArchivedObsolete
            let isMarkedUnreadObsolete: Bool = modelToCopy.isMarkedUnreadObsolete
            let lastInteractionRowId: UInt64 = modelToCopy.lastInteractionRowId
            let lastSentStoryTimestamp: NSNumber? = modelToCopy.lastSentStoryTimestamp
            let lastVisibleSortIdObsolete: UInt64 = modelToCopy.lastVisibleSortIdObsolete
            let lastVisibleSortIdOnScreenPercentageObsolete: Double = modelToCopy.lastVisibleSortIdOnScreenPercentageObsolete
            let mentionNotificationMode: TSThreadMentionNotificationMode = modelToCopy.mentionNotificationMode
            let messageDraft: String? = modelToCopy.messageDraft
            // NOTE: If this generates build errors, you made need to
            // modify DeepCopy.swift to support this type.
            //
            // That might mean:
            //
            // * Implement DeepCopyable for this type (e.g. a model).
            // * Modify DeepCopies.deepCopy() to support this type (e.g. a collection).
            let messageDraftBodyRanges: MessageBodyRanges?
            if let messageDraftBodyRangesForCopy = modelToCopy.messageDraftBodyRanges {
               messageDraftBodyRanges = try DeepCopies.deepCopy(messageDraftBodyRangesForCopy)
            } else {
               messageDraftBodyRanges = nil
            }
            let mutedUntilDateObsolete: Date? = modelToCopy.mutedUntilDateObsolete
            let mutedUntilTimestampObsolete: UInt64 = modelToCopy.mutedUntilTimestampObsolete
            let shouldThreadBeVisible: Bool = modelToCopy.shouldThreadBeVisible
            let storyViewMode: TSThreadStoryViewMode = modelToCopy.storyViewMode
            let contactPhoneNumber: String? = modelToCopy.contactPhoneNumber
            let contactUUID: String? = modelToCopy.contactUUID
            let hasDismissedOffers: Bool = modelToCopy.hasDismissedOffers

            return TSContactThread(grdbId: id,
                                   uniqueId: uniqueId,
                                   conversationColorNameObsolete: conversationColorNameObsolete,
                                   creationDate: creationDate,
                                   editTargetTimestamp: editTargetTimestamp,
                                   isArchivedObsolete: isArchivedObsolete,
                                   isMarkedUnreadObsolete: isMarkedUnreadObsolete,
                                   lastInteractionRowId: lastInteractionRowId,
                                   lastSentStoryTimestamp: lastSentStoryTimestamp,
                                   lastVisibleSortIdObsolete: lastVisibleSortIdObsolete,
                                   lastVisibleSortIdOnScreenPercentageObsolete: lastVisibleSortIdOnScreenPercentageObsolete,
                                   mentionNotificationMode: mentionNotificationMode,
                                   messageDraft: messageDraft,
                                   messageDraftBodyRanges: messageDraftBodyRanges,
                                   mutedUntilDateObsolete: mutedUntilDateObsolete,
                                   mutedUntilTimestampObsolete: mutedUntilTimestampObsolete,
                                   shouldThreadBeVisible: shouldThreadBeVisible,
                                   storyViewMode: storyViewMode,
                                   contactPhoneNumber: contactPhoneNumber,
                                   contactUUID: contactUUID,
                                   hasDismissedOffers: hasDismissedOffers)
        }

        do {
            let modelToCopy = self
            assert(type(of: modelToCopy) == TSThread.self)
            let uniqueId: String = modelToCopy.uniqueId
            let conversationColorNameObsolete: String = modelToCopy.conversationColorNameObsolete
            let creationDate: Date? = modelToCopy.creationDate
            let editTargetTimestamp: NSNumber? = modelToCopy.editTargetTimestamp
            let isArchivedObsolete: Bool = modelToCopy.isArchivedObsolete
            let isMarkedUnreadObsolete: Bool = modelToCopy.isMarkedUnreadObsolete
            let lastInteractionRowId: UInt64 = modelToCopy.lastInteractionRowId
            let lastSentStoryTimestamp: NSNumber? = modelToCopy.lastSentStoryTimestamp
            let lastVisibleSortIdObsolete: UInt64 = modelToCopy.lastVisibleSortIdObsolete
            let lastVisibleSortIdOnScreenPercentageObsolete: Double = modelToCopy.lastVisibleSortIdOnScreenPercentageObsolete
            let mentionNotificationMode: TSThreadMentionNotificationMode = modelToCopy.mentionNotificationMode
            let messageDraft: String? = modelToCopy.messageDraft
            // NOTE: If this generates build errors, you made need to
            // modify DeepCopy.swift to support this type.
            //
            // That might mean:
            //
            // * Implement DeepCopyable for this type (e.g. a model).
            // * Modify DeepCopies.deepCopy() to support this type (e.g. a collection).
            let messageDraftBodyRanges: MessageBodyRanges?
            if let messageDraftBodyRangesForCopy = modelToCopy.messageDraftBodyRanges {
               messageDraftBodyRanges = try DeepCopies.deepCopy(messageDraftBodyRangesForCopy)
            } else {
               messageDraftBodyRanges = nil
            }
            let mutedUntilDateObsolete: Date? = modelToCopy.mutedUntilDateObsolete
            let mutedUntilTimestampObsolete: UInt64 = modelToCopy.mutedUntilTimestampObsolete
            let shouldThreadBeVisible: Bool = modelToCopy.shouldThreadBeVisible
            let storyViewMode: TSThreadStoryViewMode = modelToCopy.storyViewMode

            return TSThread(grdbId: id,
                            uniqueId: uniqueId,
                            conversationColorNameObsolete: conversationColorNameObsolete,
                            creationDate: creationDate,
                            editTargetTimestamp: editTargetTimestamp,
                            isArchivedObsolete: isArchivedObsolete,
                            isMarkedUnreadObsolete: isMarkedUnreadObsolete,
                            lastInteractionRowId: lastInteractionRowId,
                            lastSentStoryTimestamp: lastSentStoryTimestamp,
                            lastVisibleSortIdObsolete: lastVisibleSortIdObsolete,
                            lastVisibleSortIdOnScreenPercentageObsolete: lastVisibleSortIdOnScreenPercentageObsolete,
                            mentionNotificationMode: mentionNotificationMode,
                            messageDraft: messageDraft,
                            messageDraftBodyRanges: messageDraftBodyRanges,
                            mutedUntilDateObsolete: mutedUntilDateObsolete,
                            mutedUntilTimestampObsolete: mutedUntilTimestampObsolete,
                            shouldThreadBeVisible: shouldThreadBeVisible,
                            storyViewMode: storyViewMode)
        }

    }
}

// MARK: - Table Metadata

extension TSThreadSerializer {

    // This defines all of the columns used in the table
    // where this model (and any subclasses) are persisted.
    static var idColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "id", columnType: .primaryKey) }
    static var recordTypeColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "recordType", columnType: .int64) }
    static var uniqueIdColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "uniqueId", columnType: .unicodeString, isUnique: true) }
    // Properties
    static var conversationColorNameObsoleteColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "conversationColorNameObsolete", columnType: .unicodeString) }
    static var creationDateColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "creationDate", columnType: .double, isOptional: true) }
    static var isArchivedObsoleteColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "isArchivedObsolete", columnType: .int) }
    static var lastInteractionRowIdColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "lastInteractionRowId", columnType: .int64) }
    static var messageDraftColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "messageDraft", columnType: .unicodeString, isOptional: true) }
    static var mutedUntilDateObsoleteColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "mutedUntilDateObsolete", columnType: .double, isOptional: true) }
    static var shouldThreadBeVisibleColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "shouldThreadBeVisible", columnType: .int) }
    static var contactPhoneNumberColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "contactPhoneNumber", columnType: .unicodeString, isOptional: true) }
    static var contactUUIDColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "contactUUID", columnType: .unicodeString, isOptional: true) }
    static var groupModelColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "groupModel", columnType: .blob, isOptional: true) }
    static var hasDismissedOffersColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "hasDismissedOffers", columnType: .int, isOptional: true) }
    static var isMarkedUnreadObsoleteColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "isMarkedUnreadObsolete", columnType: .int) }
    static var lastVisibleSortIdOnScreenPercentageObsoleteColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "lastVisibleSortIdOnScreenPercentageObsolete", columnType: .double) }
    static var lastVisibleSortIdObsoleteColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "lastVisibleSortIdObsolete", columnType: .int64) }
    static var messageDraftBodyRangesColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "messageDraftBodyRanges", columnType: .blob, isOptional: true) }
    static var mentionNotificationModeColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "mentionNotificationMode", columnType: .int) }
    static var mutedUntilTimestampObsoleteColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "mutedUntilTimestampObsolete", columnType: .int64) }
    static var allowsRepliesColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "allowsReplies", columnType: .int, isOptional: true) }
    static var lastSentStoryTimestampColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "lastSentStoryTimestamp", columnType: .int64, isOptional: true) }
    static var nameColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "name", columnType: .unicodeString, isOptional: true) }
    static var addressesColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "addresses", columnType: .blob, isOptional: true) }
    static var storyViewModeColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "storyViewMode", columnType: .int) }
    static var editTargetTimestampColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "editTargetTimestamp", columnType: .int64, isOptional: true) }

    public static var table: SDSTableMetadata {
        SDSTableMetadata(collection: TSThread.collection(),
                         tableName: "model_TSThread",
                         columns: [
        idColumn,
        recordTypeColumn,
        uniqueIdColumn,
        conversationColorNameObsoleteColumn,
        creationDateColumn,
        isArchivedObsoleteColumn,
        lastInteractionRowIdColumn,
        messageDraftColumn,
        mutedUntilDateObsoleteColumn,
        shouldThreadBeVisibleColumn,
        contactPhoneNumberColumn,
        contactUUIDColumn,
        groupModelColumn,
        hasDismissedOffersColumn,
        isMarkedUnreadObsoleteColumn,
        lastVisibleSortIdOnScreenPercentageObsoleteColumn,
        lastVisibleSortIdObsoleteColumn,
        messageDraftBodyRangesColumn,
        mentionNotificationModeColumn,
        mutedUntilTimestampObsoleteColumn,
        allowsRepliesColumn,
        lastSentStoryTimestampColumn,
        nameColumn,
        addressesColumn,
        storyViewModeColumn,
        editTargetTimestampColumn
        ])
    }
}

// MARK: - Save/Remove/Update

@objc
public extension TSThread {
    func anyInsert(transaction: SDSAnyWriteTransaction) {
        sdsSave(saveMode: .insert, transaction: transaction)
    }

    // Avoid this method whenever feasible.
    //
    // If the record has previously been saved, this method does an overwriting
    // update of the corresponding row, otherwise if it's a new record, this
    // method inserts a new row.
    //
    // For performance, when possible, you should explicitly specify whether
    // you are inserting or updating rather than calling this method.
    func anyUpsert(transaction: SDSAnyWriteTransaction) {
        let isInserting: Bool
        if TSThread.anyFetch(uniqueId: uniqueId, transaction: transaction) != nil {
            isInserting = false
        } else {
            isInserting = true
        }
        sdsSave(saveMode: isInserting ? .insert : .update, transaction: transaction)
    }

    // This method is used by "updateWith..." methods.
    //
    // This model may be updated from many threads. We don't want to save
    // our local copy (this instance) since it may be out of date.  We also
    // want to avoid re-saving a model that has been deleted.  Therefore, we
    // use "updateWith..." methods to:
    //
    // a) Update a property of this instance.
    // b) If a copy of this model exists in the database, load an up-to-date copy,
    //    and update and save that copy.
    // b) If a copy of this model _DOES NOT_ exist in the database, do _NOT_ save
    //    this local instance.
    //
    // After "updateWith...":
    //
    // a) Any copy of this model in the database will have been updated.
    // b) The local property on this instance will always have been updated.
    // c) Other properties on this instance may be out of date.
    //
    // All mutable properties of this class have been made read-only to
    // prevent accidentally modifying them directly.
    //
    // This isn't a perfect arrangement, but in practice this will prevent
    // data loss and will resolve all known issues.
    func anyUpdate(transaction: SDSAnyWriteTransaction, block: (TSThread) -> Void) {

        block(self)

        guard let dbCopy = type(of: self).anyFetch(uniqueId: uniqueId,
                                                   transaction: transaction) else {
            return
        }

        // Don't apply the block twice to the same instance.
        // It's at least unnecessary and actually wrong for some blocks.
        // e.g. `block: { $0 in $0.someField++ }`
        if dbCopy !== self {
            block(dbCopy)
        }

        dbCopy.sdsSave(saveMode: .update, transaction: transaction)
    }

    // This method is an alternative to `anyUpdate(transaction:block:)` methods.
    //
    // We should generally use `anyUpdate` to ensure we're not unintentionally
    // clobbering other columns in the database when another concurrent update
    // has occurred.
    //
    // There are cases when this doesn't make sense, e.g. when  we know we've
    // just loaded the model in the same transaction. In those cases it is
    // safe and faster to do a "overwriting" update
    func anyOverwritingUpdate(transaction: SDSAnyWriteTransaction) {
        sdsSave(saveMode: .update, transaction: transaction)
    }

    func anyReload(transaction: SDSAnyReadTransaction) {
        anyReload(transaction: transaction, ignoreMissing: false)
    }

    func anyReload(transaction: SDSAnyReadTransaction, ignoreMissing: Bool) {
        guard let latestVersion = type(of: self).anyFetch(uniqueId: uniqueId, transaction: transaction) else {
            if !ignoreMissing {
                owsFailDebug("`latest` was unexpectedly nil")
            }
            return
        }

        setValuesForKeys(latestVersion.dictionaryValue)
    }
}

// MARK: - TSThreadCursor

@objc
public class TSThreadCursor: NSObject, SDSCursor {
    private let transaction: GRDBReadTransaction
    private let cursor: RecordCursor<ThreadRecord>?

    init(transaction: GRDBReadTransaction, cursor: RecordCursor<ThreadRecord>?) {
        self.transaction = transaction
        self.cursor = cursor
    }

    public func next() throws -> TSThread? {
        guard let cursor = cursor else {
            return nil
        }
        guard let record = try cursor.next() else {
            return nil
        }
        let value = try TSThread.fromRecord(record)
        Self.modelReadCaches.threadReadCache.didReadThread(value, transaction: transaction.asAnyRead)
        return value
    }

    public func all() throws -> [TSThread] {
        var result = [TSThread]()
        while true {
            guard let model = try next() else {
                break
            }
            result.append(model)
        }
        return result
    }
}

// MARK: - Obj-C Fetch

@objc
public extension TSThread {
    class func grdbFetchCursor(transaction: GRDBReadTransaction) -> TSThreadCursor {
        let database = transaction.database
        do {
            let cursor = try ThreadRecord.fetchCursor(database)
            return TSThreadCursor(transaction: transaction, cursor: cursor)
        } catch {
            DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary(
                userDefaults: CurrentAppContext().appUserDefaults(),
                error: error
            )
            owsFailDebug("Read failed: \(error)")
            return TSThreadCursor(transaction: transaction, cursor: nil)
        }
    }

    // Fetches a single model by "unique id".
    class func anyFetch(uniqueId: String,
                        transaction: SDSAnyReadTransaction) -> TSThread? {
        assert(!uniqueId.isEmpty)

        return anyFetch(uniqueId: uniqueId, transaction: transaction, ignoreCache: false)
    }

    // Fetches a single model by "unique id".
    class func anyFetch(uniqueId: String,
                        transaction: SDSAnyReadTransaction,
                        ignoreCache: Bool) -> TSThread? {
        assert(!uniqueId.isEmpty)

        if !ignoreCache,
            let cachedCopy = Self.modelReadCaches.threadReadCache.getThread(uniqueId: uniqueId, transaction: transaction) {
            return cachedCopy
        }

        switch transaction.readTransaction {
        case .grdbRead(let grdbTransaction):
            let sql = "SELECT * FROM \(ThreadRecord.databaseTableName) WHERE \(threadColumn: .uniqueId) = ?"
            return grdbFetchOne(sql: sql, arguments: [uniqueId], transaction: grdbTransaction)
        }
    }

    // Traverses all records.
    // Records are not visited in any particular order.
    class func anyEnumerate(
        transaction: SDSAnyReadTransaction,
        block: (TSThread, UnsafeMutablePointer<ObjCBool>) -> Void
    ) {
        anyEnumerate(transaction: transaction, batched: false, block: block)
    }

    // Traverses all records.
    // Records are not visited in any particular order.
    class func anyEnumerate(
        transaction: SDSAnyReadTransaction,
        batched: Bool = false,
        block: (TSThread, UnsafeMutablePointer<ObjCBool>) -> Void
    ) {
        let batchSize = batched ? Batching.kDefaultBatchSize : 0
        anyEnumerate(transaction: transaction, batchSize: batchSize, block: block)
    }

    // Traverses all records.
    // Records are not visited in any particular order.
    //
    // If batchSize > 0, the enumeration is performed in autoreleased batches.
    class func anyEnumerate(
        transaction: SDSAnyReadTransaction,
        batchSize: UInt,
        block: (TSThread, UnsafeMutablePointer<ObjCBool>) -> Void
    ) {
        switch transaction.readTransaction {
        case .grdbRead(let grdbTransaction):
            let cursor = TSThread.grdbFetchCursor(transaction: grdbTransaction)
            Batching.loop(batchSize: batchSize,
                          loopBlock: { stop in
                                do {
                                    guard let value = try cursor.next() else {
                                        stop.pointee = true
                                        return
                                    }
                                    block(value, stop)
                                } catch let error {
                                    owsFailDebug("Couldn't fetch model: \(error)")
                                }
                              })
        }
    }

    // Traverses all records' unique ids.
    // Records are not visited in any particular order.
    class func anyEnumerateUniqueIds(
        transaction: SDSAnyReadTransaction,
        block: (String, UnsafeMutablePointer<ObjCBool>) -> Void
    ) {
        anyEnumerateUniqueIds(transaction: transaction, batched: false, block: block)
    }

    // Traverses all records' unique ids.
    // Records are not visited in any particular order.
    class func anyEnumerateUniqueIds(
        transaction: SDSAnyReadTransaction,
        batched: Bool = false,
        block: (String, UnsafeMutablePointer<ObjCBool>) -> Void
    ) {
        let batchSize = batched ? Batching.kDefaultBatchSize : 0
        anyEnumerateUniqueIds(transaction: transaction, batchSize: batchSize, block: block)
    }

    // Traverses all records' unique ids.
    // Records are not visited in any particular order.
    //
    // If batchSize > 0, the enumeration is performed in autoreleased batches.
    class func anyEnumerateUniqueIds(
        transaction: SDSAnyReadTransaction,
        batchSize: UInt,
        block: (String, UnsafeMutablePointer<ObjCBool>) -> Void
    ) {
        switch transaction.readTransaction {
        case .grdbRead(let grdbTransaction):
            grdbEnumerateUniqueIds(transaction: grdbTransaction,
                                   sql: """
                    SELECT \(threadColumn: .uniqueId)
                    FROM \(ThreadRecord.databaseTableName)
                """,
                batchSize: batchSize,
                block: block)
        }
    }

    // Does not order the results.
    class func anyFetchAll(transaction: SDSAnyReadTransaction) -> [TSThread] {
        var result = [TSThread]()
        anyEnumerate(transaction: transaction) { (model, _) in
            result.append(model)
        }
        return result
    }

    // Does not order the results.
    class func anyAllUniqueIds(transaction: SDSAnyReadTransaction) -> [String] {
        var result = [String]()
        anyEnumerateUniqueIds(transaction: transaction) { (uniqueId, _) in
            result.append(uniqueId)
        }
        return result
    }

    class func anyCount(transaction: SDSAnyReadTransaction) -> UInt {
        switch transaction.readTransaction {
        case .grdbRead(let grdbTransaction):
            return ThreadRecord.ows_fetchCount(grdbTransaction.database)
        }
    }

    class func anyExists(
        uniqueId: String,
        transaction: SDSAnyReadTransaction
    ) -> Bool {
        assert(!uniqueId.isEmpty)

        switch transaction.readTransaction {
        case .grdbRead(let grdbTransaction):
            let sql = "SELECT EXISTS ( SELECT 1 FROM \(ThreadRecord.databaseTableName) WHERE \(threadColumn: .uniqueId) = ? )"
            let arguments: StatementArguments = [uniqueId]
            do {
                return try Bool.fetchOne(grdbTransaction.database, sql: sql, arguments: arguments) ?? false
            } catch {
                DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary(
                    userDefaults: CurrentAppContext().appUserDefaults(),
                    error: error
                )
                owsFail("Missing instance.")
            }
        }
    }
}

// MARK: - Swift Fetch

public extension TSThread {
    class func grdbFetchCursor(sql: String,
                               arguments: StatementArguments = StatementArguments(),
                               transaction: GRDBReadTransaction) -> TSThreadCursor {
        do {
            let sqlRequest = SQLRequest<Void>(sql: sql, arguments: arguments, cached: true)
            let cursor = try ThreadRecord.fetchCursor(transaction.database, sqlRequest)
            return TSThreadCursor(transaction: transaction, cursor: cursor)
        } catch {
            DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary(
                userDefaults: CurrentAppContext().appUserDefaults(),
                error: error
            )
            owsFailDebug("Read failed: \(error)")
            return TSThreadCursor(transaction: transaction, cursor: nil)
        }
    }

    class func grdbFetchOne(sql: String,
                            arguments: StatementArguments = StatementArguments(),
                            transaction: GRDBReadTransaction) -> TSThread? {
        assert(!sql.isEmpty)

        do {
            let sqlRequest = SQLRequest<Void>(sql: sql, arguments: arguments, cached: true)
            guard let record = try ThreadRecord.fetchOne(transaction.database, sqlRequest) else {
                return nil
            }

            let value = try TSThread.fromRecord(record)
            Self.modelReadCaches.threadReadCache.didReadThread(value, transaction: transaction.asAnyRead)
            return value
        } catch {
            owsFailDebug("error: \(error)")
            return nil
        }
    }
}

// MARK: - SDSSerializer

// The SDSSerializer protocol specifies how to insert and update the
// row that corresponds to this model.
class TSThreadSerializer: SDSSerializer {

    private let model: TSThread
    public init(model: TSThread) {
        self.model = model
    }

    // MARK: - Record

    func asRecord() throws -> SDSRecord {
        let id: Int64? = model.grdbId?.int64Value

        let recordType: SDSRecordType = .thread
        let uniqueId: String = model.uniqueId

        // Properties
        let conversationColorName: String = model.conversationColorNameObsolete
        let creationDate: Double? = archiveOptionalDate(model.creationDate)
        let isArchived: Bool = model.isArchivedObsolete
        let lastInteractionRowId: UInt64 = model.lastInteractionRowId
        let messageDraft: String? = model.messageDraft
        let mutedUntilDate: Double? = archiveOptionalDate(model.mutedUntilDateObsolete)
        let shouldThreadBeVisible: Bool = model.shouldThreadBeVisible
        let contactPhoneNumber: String? = nil
        let contactUUID: String? = nil
        let groupModel: Data? = nil
        let hasDismissedOffers: Bool? = nil
        let isMarkedUnread: Bool = model.isMarkedUnreadObsolete
        let lastVisibleSortIdOnScreenPercentage: Double = model.lastVisibleSortIdOnScreenPercentageObsolete
        let lastVisibleSortId: UInt64 = model.lastVisibleSortIdObsolete
        let messageDraftBodyRanges: Data? = optionalArchive(model.messageDraftBodyRanges)
        let mentionNotificationMode: UInt = model.mentionNotificationMode.rawValue
        let mutedUntilTimestamp: UInt64 = model.mutedUntilTimestampObsolete
        let allowsReplies: Bool? = nil
        let lastSentStoryTimestamp: UInt64? = archiveOptionalNSNumber(model.lastSentStoryTimestamp, conversion: { $0.uint64Value })
        let name: String? = nil
        let addresses: Data? = nil
        let storyViewMode: UInt = model.storyViewMode.rawValue
        let editTargetTimestamp: UInt64? = archiveOptionalNSNumber(model.editTargetTimestamp, conversion: { $0.uint64Value })

        return ThreadRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, conversationColorName: conversationColorName, creationDate: creationDate, isArchived: isArchived, lastInteractionRowId: lastInteractionRowId, messageDraft: messageDraft, mutedUntilDate: mutedUntilDate, shouldThreadBeVisible: shouldThreadBeVisible, contactPhoneNumber: contactPhoneNumber, contactUUID: contactUUID, groupModel: groupModel, hasDismissedOffers: hasDismissedOffers, isMarkedUnread: isMarkedUnread, lastVisibleSortIdOnScreenPercentage: lastVisibleSortIdOnScreenPercentage, lastVisibleSortId: lastVisibleSortId, messageDraftBodyRanges: messageDraftBodyRanges, mentionNotificationMode: mentionNotificationMode, mutedUntilTimestamp: mutedUntilTimestamp, allowsReplies: allowsReplies, lastSentStoryTimestamp: lastSentStoryTimestamp, name: name, addresses: addresses, storyViewMode: storyViewMode, editTargetTimestamp: editTargetTimestamp)
    }
}

// MARK: - Deep Copy

#if TESTABLE_BUILD
@objc
public extension TSThread {
    // We're not using this method at the moment,
    // but we might use it for validation of
    // other deep copy methods.
    func deepCopyUsingRecord() throws -> TSThread {
        guard let record = try asRecord() as? ThreadRecord else {
            throw OWSAssertionError("Could not convert to record.")
        }
        return try TSThread.fromRecord(record)
    }
}
#endif
