diff --git a/Scripts/sds_codegen/sds_generate.py b/Scripts/sds_codegen/sds_generate.py index 4527bae3d6..7adf246096 100755 --- a/Scripts/sds_codegen/sds_generate.py +++ b/Scripts/sds_codegen/sds_generate.py @@ -1760,8 +1760,8 @@ extension %sRecord { // This defines all of the columns used in the table // where this model (and any subclasses) are persisted. - internal func asArguments() -> StatementArguments { - let databaseValues: [DatabaseValueConvertible?] = [ + internal func asValues() -> [DatabaseValueConvertible?] { + return [ """ % str( remove_prefix_from_class_name(clazz.name) ) @@ -1784,7 +1784,10 @@ extension %sRecord { swift_body += """ ] - return StatementArguments(databaseValues) + } + + internal func asArguments() -> StatementArguments { + return StatementArguments(asValues()) } } """ diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 98bacd7068..e153ec231d 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -813,7 +813,6 @@ 66232AD92CB9D00400AE6A76 /* MessageBackupThreadStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66232AD82CB9D00000AE6A76 /* MessageBackupThreadStore.swift */; }; 66232ADB2CB9E33600AE6A76 /* MessageBackupStoryStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66232ADA2CB9E32F00AE6A76 /* MessageBackupStoryStore.swift */; }; 66232AE12CC0272900AE6A76 /* MessageBackupFullTextSearchIndexer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66232AE02CC0271F00AE6A76 /* MessageBackupFullTextSearchIndexer.swift */; }; - 662590BB2B50922D001FDCDD /* GroupUpdateInfoMessageInserterBackupHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 662590BA2B50922D001FDCDD /* GroupUpdateInfoMessageInserterBackupHelper.swift */; }; 662590BF2B56ECA8001FDCDD /* MessageBackupGroupUpdateMessageArchiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 662590BE2B56ECA8001FDCDD /* MessageBackupGroupUpdateMessageArchiver.swift */; }; 662590CB2B5994D2001FDCDD /* MessageBackupGroupUpdateProtoToSwiftConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 662590CA2B5994D2001FDCDD /* MessageBackupGroupUpdateProtoToSwiftConverter.swift */; }; 662590CD2B5994DC001FDCDD /* MessageBackupGroupUpdateSwiftToProtoConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 662590CC2B5994DC001FDCDD /* MessageBackupGroupUpdateSwiftToProtoConverter.swift */; }; @@ -2452,6 +2451,8 @@ D93EDC042AE9E3CD0004BDD9 /* DonationSettingsViewController+MySupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93EDC032AE9E3CD0004BDD9 /* DonationSettingsViewController+MySupport.swift */; }; D941863C2ACE252D002FE2D3 /* CallRecordLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = D941863B2ACE252D002FE2D3 /* CallRecordLogger.swift */; }; D943F3EF2892F89B008C0C8B /* NSELogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = D943F3EE2892F89B008C0C8B /* NSELogger.swift */; }; + D94441312D55956B005B2A54 /* UUIDv7.swift in Sources */ = {isa = PBXBuildFile; fileRef = D94441302D559567005B2A54 /* UUIDv7.swift */; }; + D94441332D559C6F005B2A54 /* UUIDv7Test.swift in Sources */ = {isa = PBXBuildFile; fileRef = D94441322D559C6B005B2A54 /* UUIDv7Test.swift */; }; D945319E2CE53CEB004DAB30 /* SubscriptionRedemptionNecessityChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D945319D2CE53CC8004DAB30 /* SubscriptionRedemptionNecessityChecker.swift */; }; D9495A6D2C7683D100843BC1 /* TSOutgoingMessageRecipientState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9495A6C2C7683D100843BC1 /* TSOutgoingMessageRecipientState.swift */; }; D9495A702C76965600843BC1 /* TSOutgoingMessageRecipientStateTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9495A6E2C76963F00843BC1 /* TSOutgoingMessageRecipientStateTest.swift */; }; @@ -4634,7 +4635,6 @@ 66232AD82CB9D00000AE6A76 /* MessageBackupThreadStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBackupThreadStore.swift; sourceTree = ""; }; 66232ADA2CB9E32F00AE6A76 /* MessageBackupStoryStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBackupStoryStore.swift; sourceTree = ""; }; 66232AE02CC0271F00AE6A76 /* MessageBackupFullTextSearchIndexer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBackupFullTextSearchIndexer.swift; sourceTree = ""; }; - 662590BA2B50922D001FDCDD /* GroupUpdateInfoMessageInserterBackupHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupUpdateInfoMessageInserterBackupHelper.swift; sourceTree = ""; }; 662590BE2B56ECA8001FDCDD /* MessageBackupGroupUpdateMessageArchiver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBackupGroupUpdateMessageArchiver.swift; sourceTree = ""; }; 662590CA2B5994D2001FDCDD /* MessageBackupGroupUpdateProtoToSwiftConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBackupGroupUpdateProtoToSwiftConverter.swift; sourceTree = ""; }; 662590CC2B5994DC001FDCDD /* MessageBackupGroupUpdateSwiftToProtoConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBackupGroupUpdateSwiftToProtoConverter.swift; sourceTree = ""; }; @@ -6281,6 +6281,8 @@ D93EDC032AE9E3CD0004BDD9 /* DonationSettingsViewController+MySupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DonationSettingsViewController+MySupport.swift"; sourceTree = ""; }; D941863B2ACE252D002FE2D3 /* CallRecordLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallRecordLogger.swift; sourceTree = ""; }; D943F3EE2892F89B008C0C8B /* NSELogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSELogger.swift; sourceTree = ""; }; + D94441302D559567005B2A54 /* UUIDv7.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UUIDv7.swift; sourceTree = ""; }; + D94441322D559C6B005B2A54 /* UUIDv7Test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UUIDv7Test.swift; sourceTree = ""; }; D945319D2CE53CC8004DAB30 /* SubscriptionRedemptionNecessityChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionRedemptionNecessityChecker.swift; sourceTree = ""; }; D9495A6C2C7683D100843BC1 /* TSOutgoingMessageRecipientState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSOutgoingMessageRecipientState.swift; sourceTree = ""; }; D9495A6E2C76963F00843BC1 /* TSOutgoingMessageRecipientStateTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSOutgoingMessageRecipientStateTest.swift; sourceTree = ""; }; @@ -12801,6 +12803,7 @@ F94261E8289B1B5400460798 /* UnfairLockTest.swift */, 6600F34D298C81E300B1EDB7 /* UnknownEnumCodableTest.swift */, F9D5BFD02979B027001737E5 /* URLPathComponentsTest.swift */, + D94441322D559C6B005B2A54 /* UUIDv7Test.swift */, F94261FD289B1B5400460798 /* ViewOnceMessagesTest.swift */, ); name = Util; @@ -14067,6 +14070,7 @@ 6600F34B298C81CD00B1EDB7 /* UnknownEnumCodable.swift */, F9D5BFCE2979AFF4001737E5 /* URLPathComponents.swift */, F9C5CB54289453B200548EEE /* UserProfileFinder.swift */, + D94441302D559567005B2A54 /* UUIDv7.swift */, F9C5CB03289453B200548EEE /* ViewOnceMessages.swift */, F9C5CB7F289453B200548EEE /* Weak.swift */, F9C5CB29289453B200548EEE /* WeakTimer.swift */, @@ -14132,7 +14136,6 @@ 34BB3C5823C6644B001651FC /* GroupsV2Utils.swift */, D99A0F6129F35CE1002E02E3 /* GroupUpdateInfoMessageInserter+FoldIntoExistingMessage.swift */, D99A0F5F29F34FDA002E02E3 /* GroupUpdateInfoMessageInserter.swift */, - 662590BA2B50922D001FDCDD /* GroupUpdateInfoMessageInserterBackupHelper.swift */, 34BB3C5B23C6644B001651FC /* GroupV2Params.swift */, 34BB3C5A23C6644B001651FC /* GroupV2Snapshot.swift */, 340B870D23DF3E3A00BE0AFC /* GroupV2UpdatesImpl.swift */, @@ -17358,7 +17361,6 @@ 724D47BD2B97C5B9001BE973 /* GroupsV2Utils.swift in Sources */, D99A0F6229F35CE1002E02E3 /* GroupUpdateInfoMessageInserter+FoldIntoExistingMessage.swift in Sources */, D99A0F6029F34FDA002E02E3 /* GroupUpdateInfoMessageInserter.swift in Sources */, - 662590BB2B50922D001FDCDD /* GroupUpdateInfoMessageInserterBackupHelper.swift in Sources */, 667AF9E22B4DC5EE008AEE5D /* GroupUpdateSource.swift in Sources */, 724D47BB2B97C558001BE973 /* GroupV2Params.swift in Sources */, 724D47BC2B97C57C001BE973 /* GroupV2Snapshot.swift in Sources */, @@ -18186,6 +18188,7 @@ 5037F1942A43A6A300C372AD /* UserProfileMerger.swift in Sources */, 506ABE6B2A43AECA008844D1 /* UserProfileStore.swift in Sources */, 72901D2B2C9B11A3000406DC /* UserProfileWriter.swift in Sources */, + D94441312D55956B005B2A54 /* UUIDv7.swift in Sources */, 504F397C29D23B1700E849A6 /* ValidatedIncomingEnvelope.swift in Sources */, 66F6D69C2C73F01900EFAF75 /* VersionedDisappearingMessageToken.swift in Sources */, F9C5CE3B289453B400548EEE /* VersionedProfiles.swift in Sources */, @@ -18427,6 +18430,7 @@ D9A3E2322A0DBDFC00E2A8B5 /* Usernames+UsernameLinkTests.swift in Sources */, D93830812A7065C7006CDCDE /* UsernameValidationManagerTests.swift in Sources */, 506ABE6E2A43B2FE008844D1 /* UserProfileMergerTest.swift in Sources */, + D94441332D559C6F005B2A54 /* UUIDv7Test.swift in Sources */, D9B95A9D29E894A600D7CB95 /* ValidatableModel.swift in Sources */, F942626A289B1B5500460798 /* ViewOnceMessagesTest.swift in Sources */, D9C964092BE44D700058F143 /* XCTest+Thenable.swift in Sources */, diff --git a/SignalServiceKit/Environment/AppSetup.swift b/SignalServiceKit/Environment/AppSetup.swift index 5da83a42f4..94469eef5a 100644 --- a/SignalServiceKit/Environment/AppSetup.swift +++ b/SignalServiceKit/Environment/AppSetup.swift @@ -1104,7 +1104,6 @@ public class AppSetup { dateProvider: dateProvider, editMessageStore: editMessageStore, groupCallRecordManager: groupCallRecordManager, - groupUpdateHelper: GroupUpdateInfoMessageInserterBackupHelperImpl(), groupUpdateItemBuilder: groupUpdateItemBuilder, individualCallRecordManager: individualCallRecordManager, interactionStore: backupInteractionStore, diff --git a/SignalServiceKit/Groups/GroupUpdateInfoMessageInserter+FoldIntoExistingMessage.swift b/SignalServiceKit/Groups/GroupUpdateInfoMessageInserter+FoldIntoExistingMessage.swift index 06c938fcf5..d07e42b1ce 100644 --- a/SignalServiceKit/Groups/GroupUpdateInfoMessageInserter+FoldIntoExistingMessage.swift +++ b/SignalServiceKit/Groups/GroupUpdateInfoMessageInserter+FoldIntoExistingMessage.swift @@ -211,79 +211,6 @@ extension GroupUpdateInfoMessageInserterImpl { } } - /// See ``GroupUpdateInfoMessageInserterBackupHelper``. - public static func collapseFromBackupIfNeeded( - updates: inout [TSInfoMessage.PersistableGroupUpdateItem], - localIdentifiers: LocalIdentifiers, - groupThread: TSGroupThread, - transaction: SDSAnyWriteTransaction - ) { - if - updates.count == 2, - let (sequenceRequestor, count) = updates[0] - .representsSequenceOfRequestsAndCancels(), - let singleRequestor = updates[1] - .representsCollapsibleSingleRequestToJoin(), - sequenceRequestor == singleRequestor - { - // These updates are collapsible in isolation. - // Mark the first one as not the tail; thats the only change - // we need to make to "collapse" this one. - updates[0] = - .sequenceOfInviteLinkRequestAndCancels( - requester: sequenceRequestor.codableUuid, - count: count, - isTail: false - ) - return - } - - guard - updates.count == 1, - let requestingAci = updates[0].representsCollapsibleSingleRequestToJoin() - else { - // No change needed. - return - } - - // This latest message is collapsible; try and collapse it. - - guard let (mostRecentInfoMsg, _) = - mostRecentVisibleInteractionsAsInfoMessages( - forGroupThread: groupThread, - withTransaction: transaction - ) - else { - return - } - - guard let (mostRecentMsgAci, count) = mostRecentInfoMsg - .representsSingleSequenceOfRequestsAndCancels( - localIdentifiers: localIdentifiers - ), - requestingAci == mostRecentMsgAci - else { - return - } - - mostRecentInfoMsg.anyUpdateInfoMessage( - transaction: transaction, - block: { - $0.setSingleUpdateItem( - singleUpdateItem: .sequenceOfInviteLinkRequestAndCancels( - requester: mostRecentMsgAci.codableUuid, - // Count stays the same. - count: count, - // Its not the tail because of the subsequent request. - isTail: false - ) - ) - } - ) - // No need to change the updates in the new info message. - return - } - private static func mostRecentVisibleInteractionsAsInfoMessages( forGroupThread groupThread: TSGroupThread, withTransaction transaction: SDSAnyReadTransaction diff --git a/SignalServiceKit/Groups/GroupUpdateInfoMessageInserterBackupHelper.swift b/SignalServiceKit/Groups/GroupUpdateInfoMessageInserterBackupHelper.swift deleted file mode 100644 index a2005341be..0000000000 --- a/SignalServiceKit/Groups/GroupUpdateInfoMessageInserterBackupHelper.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// Copyright 2024 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only -// - -import Foundation -import LibSignalClient - -public protocol GroupUpdateInfoMessageInserterBackupHelper { - - /** - * When processing a backup, we may encounter a single group update item - * representing a collapsed sequence of requests and cancels from the same user - * (``GroupSequenceOfRequestsAndCancelsUpdate`` proto, which becomes - * ``TSInfoMessage.PersistableGroupUpdateItem.sequenceOfInviteLinkRequestAndCancels``). - * - * If we do, and it is immediately followed by a request to join by the same user in the same group, - * we have to update the prior collapsed sequence message to mark it as _not_ the tail (isTail = false). - * This method is a helper for that, which co-locates the code with the same code for processing - * incoming group updates during normal app functioning. - * - * This method handles two cases: - * 1. The sequence and subsequent request occur in the same TSInfoMessage, meaning they are both - * in the passed-in updates. In this case it updates the sequence's isTail to false, and returns the - * modified updates without touching the database. - * 2. The sequence is in the most recently inserted TSInfoMessage, and the passed-in updates have just - * the new request from the same user. In this case it updates the most recent TSInfoMessage in the db - * and returns the same update(s). - * - * MUST BE CALLED BEFORE INSERTING THE NEW TSINFOMESSAGE. - * - * - parameter updates: the updates pulled from the backup that we are about to generate - * a new TSInfoMessage for. This method may modify the updates. - */ - func collapseIfNeeded( - updates: inout [TSInfoMessage.PersistableGroupUpdateItem], - localIdentifiers: LocalIdentifiers, - groupThread: TSGroupThread, - tx: DBWriteTransaction - ) -} - -public class GroupUpdateInfoMessageInserterBackupHelperImpl: GroupUpdateInfoMessageInserterBackupHelper { - - public init() {} - - public func collapseIfNeeded( - updates: inout [TSInfoMessage.PersistableGroupUpdateItem], - localIdentifiers: LocalIdentifiers, - groupThread: TSGroupThread, - tx: DBWriteTransaction - ) { - return GroupUpdateInfoMessageInserterImpl - .collapseFromBackupIfNeeded( - updates: &updates, - localIdentifiers: localIdentifiers, - groupThread: groupThread, - transaction: SDSDB.shimOnlyBridge(tx) - ) - } -} diff --git a/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/GroupUpdates/MessageBackupGroupUpdateMessageArchiver.swift b/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/GroupUpdates/MessageBackupGroupUpdateMessageArchiver.swift index 723c531c74..43f1b6c339 100644 --- a/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/GroupUpdates/MessageBackupGroupUpdateMessageArchiver.swift +++ b/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/GroupUpdates/MessageBackupGroupUpdateMessageArchiver.swift @@ -15,16 +15,13 @@ final class MessageBackupGroupUpdateMessageArchiver { private typealias PersistableGroupUpdateItem = TSInfoMessage.PersistableGroupUpdateItem private let groupUpdateBuilder: GroupUpdateItemBuilder - private let groupUpdateHelper: GroupUpdateInfoMessageInserterBackupHelper private let interactionStore: MessageBackupInteractionStore public init( groupUpdateBuilder: GroupUpdateItemBuilder, - groupUpdateHelper: GroupUpdateInfoMessageInserterBackupHelper, interactionStore: MessageBackupInteractionStore ) { self.groupUpdateBuilder = groupUpdateBuilder - self.groupUpdateHelper = groupUpdateHelper self.interactionStore = interactionStore } @@ -224,16 +221,6 @@ final class MessageBackupGroupUpdateMessageArchiver { )]) } - // FIRST, try and do any collapsing. This might collapse - // the passed in array of updates (modifying it), or - // may update the most recent TSInfoMessage on disk, or both. - groupUpdateHelper.collapseIfNeeded( - updates: &persistableUpdates, - localIdentifiers: context.recipientContext.localIdentifiers, - groupThread: groupThread, - tx: context.tx - ) - guard persistableUpdates.isEmpty.negated else { // If we got an empty array, that means it got collapsed! // Ok to skip, as any updates should be applied to the diff --git a/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/MessageBackupChatUpdateMessageArchiver.swift b/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/MessageBackupChatUpdateMessageArchiver.swift index 1bbe113271..7e003266b9 100644 --- a/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/MessageBackupChatUpdateMessageArchiver.swift +++ b/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/MessageBackupChatUpdateMessageArchiver.swift @@ -22,14 +22,12 @@ final class MessageBackupChatUpdateMessageArchiver: MessageBackupProtoArchiver { callRecordStore: any CallRecordStore, contactManager: any MessageBackup.Shims.ContactManager, groupCallRecordManager: any GroupCallRecordManager, - groupUpdateHelper: any GroupUpdateInfoMessageInserterBackupHelper, groupUpdateItemBuilder: any GroupUpdateItemBuilder, individualCallRecordManager: any IndividualCallRecordManager, interactionStore: MessageBackupInteractionStore ) { groupUpdateMessageArchiver = MessageBackupGroupUpdateMessageArchiver( groupUpdateBuilder: groupUpdateItemBuilder, - groupUpdateHelper: groupUpdateHelper, interactionStore: interactionStore ) expirationTimerChatUpdateArchiver = MessageBackupExpirationTimerChatUpdateArchiver( diff --git a/SignalServiceKit/MessageBackup/Archivers/ChatItem/MessageBackupChatItemArchiverImpl.swift b/SignalServiceKit/MessageBackup/Archivers/ChatItem/MessageBackupChatItemArchiverImpl.swift index 81de15acf9..1796552a07 100644 --- a/SignalServiceKit/MessageBackup/Archivers/ChatItem/MessageBackupChatItemArchiverImpl.swift +++ b/SignalServiceKit/MessageBackup/Archivers/ChatItem/MessageBackupChatItemArchiverImpl.swift @@ -17,7 +17,6 @@ public class MessageBackupChatItemArchiverImpl: MessageBackupChatItemArchiver { private let dateProvider: DateProvider private let editMessageStore: EditMessageStore private let groupCallRecordManager: GroupCallRecordManager - private let groupUpdateHelper: GroupUpdateInfoMessageInserterBackupHelper private let groupUpdateItemBuilder: GroupUpdateItemBuilder private let individualCallRecordManager: IndividualCallRecordManager private let interactionStore: MessageBackupInteractionStore @@ -34,7 +33,6 @@ public class MessageBackupChatItemArchiverImpl: MessageBackupChatItemArchiver { dateProvider: @escaping DateProvider, editMessageStore: EditMessageStore, groupCallRecordManager: GroupCallRecordManager, - groupUpdateHelper: GroupUpdateInfoMessageInserterBackupHelper, groupUpdateItemBuilder: GroupUpdateItemBuilder, individualCallRecordManager: IndividualCallRecordManager, interactionStore: MessageBackupInteractionStore, @@ -50,7 +48,6 @@ public class MessageBackupChatItemArchiverImpl: MessageBackupChatItemArchiver { self.dateProvider = dateProvider self.editMessageStore = editMessageStore self.groupCallRecordManager = groupCallRecordManager - self.groupUpdateHelper = groupUpdateHelper self.groupUpdateItemBuilder = groupUpdateItemBuilder self.individualCallRecordManager = individualCallRecordManager self.interactionStore = interactionStore @@ -92,7 +89,6 @@ public class MessageBackupChatItemArchiverImpl: MessageBackupChatItemArchiver { callRecordStore: callRecordStore, contactManager: contactManager, groupCallRecordManager: groupCallRecordManager, - groupUpdateHelper: groupUpdateHelper, groupUpdateItemBuilder: groupUpdateItemBuilder, individualCallRecordManager: individualCallRecordManager, interactionStore: interactionStore diff --git a/SignalServiceKit/MessageBackup/Archivers/ChatItem/MessageBackupInteractionStore.swift b/SignalServiceKit/MessageBackup/Archivers/ChatItem/MessageBackupInteractionStore.swift index 15da2b049f..5c7f3f965d 100644 --- a/SignalServiceKit/MessageBackup/Archivers/ChatItem/MessageBackupInteractionStore.swift +++ b/SignalServiceKit/MessageBackup/Archivers/ChatItem/MessageBackupInteractionStore.swift @@ -4,6 +4,8 @@ // import LibSignalClient +import SQLite3 +import GRDB public final class MessageBackupInteractionStore { @@ -156,11 +158,8 @@ public final class MessageBackupInteractionStore { ) } - // MARK: Insert + // MARK: - Insert - // Even generating the sql string itself is expensive when multiplied by 200k messages. - // So we generate the string once and cache it (on top of caching the Statement) - private var cachedSQL: String? private func insert( interaction: TSInteraction, in thread: MessageBackup.ChatThread, @@ -200,22 +199,7 @@ public final class MessageBackupInteractionStore { // and restore, we'll only send back a Null message. (Until such a day // when resends use the interactions table and not MSL at all). - let sql: String - if let cachedSQL { - sql = cachedSQL - } else { - let columnsSQL = InteractionRecord.CodingKeys.allCases.filter({ $0 != .id }).map(\.name).joined(separator: ", ") - let valuesSQL = InteractionRecord.CodingKeys.allCases.filter({ $0 != .id }).map({ _ in "?" }).joined(separator: ", ") - sql = """ - INSERT INTO \(InteractionRecord.databaseTableName) (\(columnsSQL)) \ - VALUES (\(valuesSQL)) - """ - cachedSQL = sql - } - - let statement = try context.tx.databaseConnection.cachedStatement(sql: sql) - statement.setUncheckedArguments((interaction.asRecord() as! InteractionRecord).asArguments()) - try statement.execute() + try insertInteractionWithDirectSQLiteCalls(interaction, databaseConnection: context.tx.databaseConnection) interaction.updateRowId(context.tx.databaseConnection.lastInsertedRowID) guard let interactionRowId = interaction.sqliteRowId else { @@ -249,8 +233,110 @@ public final class MessageBackupInteractionStore { } } } + + /// Reuse the same SQL string, since generating the same string for each + /// message gets expensive. + private lazy var insertInteractionSQL: String = { + let columnsSQL = InteractionRecord.CodingKeys.allCases.filter({ $0 != .id }).map(\.name).joined(separator: ", ") + let valuesSQL = InteractionRecord.CodingKeys.allCases.filter({ $0 != .id }).map({ _ in "?" }).joined(separator: ", ") + return """ + INSERT INTO \(InteractionRecord.databaseTableName) (\(columnsSQL)) \ + VALUES (\(valuesSQL)) + """ + }() + + /// Inserts the given interaction using direct `sqlite3_*` function calls, + /// sidestepping GRDB abstractions. + /// + /// Profiling showed that the GRDB methods for creating `Statement` and + /// `StatementArgument` structs for insertion were taking a shocking amount + /// of time, primarily due to their use of Swift methods like `zip` that + /// instantiate opaque iterator structs. To avoid that cost, this method + /// sidesteps the GRDB abstractions and makes direct `sqlite3_*` method + /// calls instead. + /// + /// In normal app functioning, the cost of those abstractions probably isn't + /// worth managing these `sqlite3_*` methods ourselves. However, the savings + /// over hundreds of thousands of interaction inserts during a restore are. + private func insertInteractionWithDirectSQLiteCalls( + _ interaction: TSInteraction, + databaseConnection: GRDB.Database + ) throws { + guard let sqliteConnection = databaseConnection.sqliteConnection else { + throw OWSAssertionError("Missing SQLite connection!") + } + + /// SQLite compiles SQL strings into its internal bytecode. The bytecode + /// statements can be cached by SQLite, to avoid re-compiling an + /// identical SQL string repeatedly. + /// + /// This line uses GRDB to make the necessary SQLite calls to compile + /// and cache our "insert interaction" statement, which is returned as a + /// pointer we can pass back into SQLite,, since those calls involve + /// tricky pointer math. GRDB then holds a reference to that compiled + /// statement pointer in a package-level cache, from which we can + /// retrieve it. + let cachedSqliteStatement: GRDB.SQLiteStatement = try databaseConnection.cachedStatement( + sql: insertInteractionSQL + ).sqliteStatement + + /// The compiled "insert interaction" SQLite statement contains `?` + /// placeholders, which must have real values "bound" to them before the + /// statement can be used to actually insert a database row. Those bound + /// values are specific to each interaction being inserted; so, before + /// we can use the cached statement we must reset any values from + /// previous interaction inserts and bind new values. + var sqliteReturnCode: Int32 + + // Reset the cached statement. + sqliteReturnCode = sqlite3_reset(cachedSqliteStatement) + guard sqliteReturnCode == SQLITE_OK else { + let errmsg = String(cString: sqlite3_errmsg(sqliteConnection)!) + throw OWSAssertionError("Failed to reset interaction insert statement! \(errmsg)") + } + + // Clear any previously bound arguments. + sqliteReturnCode = sqlite3_clear_bindings(cachedSqliteStatement) + guard sqliteReturnCode == SQLITE_OK else { + let errmsg = String(cString: sqlite3_errmsg(sqliteConnection)!) + throw OWSAssertionError("Failed to clear argument bindings from interaction insert statement! \(errmsg)") + } + + // Bind new values from the current interaction. + let args = (interaction.asRecord() as! InteractionRecord).asValues() + var count: Int32 = 1 + for arg in args { + defer { count += 1 } + + guard let arg else { + continue + } + + let code = arg.databaseValue.bind(to: cachedSqliteStatement, at: count) + guard code == SQLITE_OK else { + let errmsg = String(cString: sqlite3_errmsg(sqliteConnection)!) + throw OWSAssertionError("Failed to bind argument to interaction insert statement! \(errmsg)") + } + } + + /// Now that we've bound values for the current interaction, we can + /// execute the statement. + insertLoop: while true { + switch sqlite3_step(cachedSqliteStatement) { + case SQLITE_DONE: + break insertLoop + case SQLITE_ROW, SQLITE_OK: + break + case let code: + let errmsg = String(cString: sqlite3_errmsg(sqliteConnection)!) + throw OWSAssertionError("Unexpected SQLite return code \(code) while executing interaction insert statement! \(errmsg)") + } + } + } } +// MARK: - + extension BackupProto_ChatItem.OneOf_DirectionalDetails { var wasRead: Bool { diff --git a/SignalServiceKit/MessageBackup/MessageBackupManagerImpl.swift b/SignalServiceKit/MessageBackup/MessageBackupManagerImpl.swift index 8f4f020b3d..0b899ff691 100644 --- a/SignalServiceKit/MessageBackup/MessageBackupManagerImpl.swift +++ b/SignalServiceKit/MessageBackup/MessageBackupManagerImpl.swift @@ -4,6 +4,7 @@ // public import LibSignalClient +import GRDB public enum BackupValidationError: Error { case unknownFields([String]) @@ -674,6 +675,14 @@ public class MessageBackupManagerImpl: MessageBackupManager { ) async throws { let result: Result = await db.awaitableWriteWithTxCompletion { tx in do { + /// Drops all indexes on the `TSInteraction` table before doing + /// the import, which dramatically speeds up the import. We'll + /// then recreate all these indexes in bulk afterwards. + let interactionIndexes = try dropAllIndexes( + forTable: InteractionRecord.databaseTableName, + tx: tx + ) + let backupInfo = try Bench( title: benchTitle, memorySamplerRatio: FeatureFlags.messageBackupMemorySamplerRatio, @@ -700,6 +709,19 @@ public class MessageBackupManagerImpl: MessageBackupManager { } } + let timeBeforeCreatingIndexes = dateProviderMonotonic() + + /// Now that we've imported successfully, we want to recreate + /// the indexes we temporarily dropped. + try createIndexes( + interactionIndexes, + onTable: InteractionRecord.databaseTableName, + tx: tx + ) + + let timeAfterCreatingIndexes = dateProviderMonotonic() + Logger.info("Created indexes in \(timeAfterCreatingIndexes.millisSince(timeBeforeCreatingIndexes))ms") + return .commit(.success(backupInfo)) } catch let error { return .rollback(.failure(error)) @@ -1059,6 +1081,62 @@ public class MessageBackupManagerImpl: MessageBackupManager { // MARK: - + private struct SQLiteIndexInfo { + let tableName: String + let sqlThatCreatedIndex: String + } + + private func dropAllIndexes( + forTable tableName: String, + tx: DBWriteTransaction + ) throws -> [SQLiteIndexInfo] { + let allIndexesOnTable: [GRDB.IndexInfo] = try tx.databaseConnection.indexes(on: tableName) + + var sqliteIndexInfos = [SQLiteIndexInfo]() + + for index in allIndexesOnTable { + if index.name.contains("autoindex") { + // Skip indexes automatically created by SQLite, such as on + // primary keys. + continue + } + + guard let sqlThatCreatedIndex = try String.fetchOne( + tx.databaseConnection, + sql: """ + SELECT sql FROM sqlite_master + WHERE type = 'index' + AND name = '\(index.name)' + """ + ) else { + throw OWSAssertionError("Failed to get SQL for creating index \(index.name)!") + } + + sqliteIndexInfos.append(SQLiteIndexInfo( + tableName: tableName, + sqlThatCreatedIndex: sqlThatCreatedIndex + )) + + try tx.databaseConnection.drop(index: index.name) + } + + return sqliteIndexInfos + } + + private func createIndexes( + _ indexInfos: [SQLiteIndexInfo], + onTable tableName: String, + tx: DBWriteTransaction + ) throws { + owsPrecondition(indexInfos.allSatisfy { $0.tableName == tableName }) + + for indexInfo in indexInfos { + try tx.databaseConnection.execute(sql: indexInfo.sqlThatCreatedIndex) + } + } + + // MARK: - + private func processErrors( errors: [LoggableErrorAndProto], didFail: Bool, diff --git a/SignalServiceKit/Messages/Interactions/TSInteraction+SDS.swift b/SignalServiceKit/Messages/Interactions/TSInteraction+SDS.swift index 5206df0d42..e7ee093b99 100644 --- a/SignalServiceKit/Messages/Interactions/TSInteraction+SDS.swift +++ b/SignalServiceKit/Messages/Interactions/TSInteraction+SDS.swift @@ -4950,8 +4950,8 @@ extension InteractionRecord { // This defines all of the columns used in the table // where this model (and any subclasses) are persisted. - internal func asArguments() -> StatementArguments { - let databaseValues: [DatabaseValueConvertible?] = [ + internal func asValues() -> [DatabaseValueConvertible?] { + return [ recordType, uniqueId, receivedAtTimestamp, @@ -5029,7 +5029,10 @@ extension InteractionRecord { isSmsMessageRestoredFromBackup, ] - return StatementArguments(databaseValues) + } + + internal func asArguments() -> StatementArguments { + return StatementArguments(asValues()) } } diff --git a/SignalServiceKit/Messages/Interactions/TSInteraction.m b/SignalServiceKit/Messages/Interactions/TSInteraction.m index 789aad4678..d903341743 100644 --- a/SignalServiceKit/Messages/Interactions/TSInteraction.m +++ b/SignalServiceKit/Messages/Interactions/TSInteraction.m @@ -75,7 +75,10 @@ NSString *NSStringFromOWSInteractionType(OWSInteractionType value) receivedAtTimestamp:(uint64_t)receivedAtTimestamp thread:(TSThread *)thread { - NSString *uniqueId = [[self class] generateUniqueId]; + // Use a sequential UUID for interaction inserts, as an optimization for the + // corresponding insert into the index on `uniqueId`. See comments about + // UUIDv7 for more. + NSString *uniqueId = [[NSUUID sequential] UUIDString]; self = [super initWithUniqueId:uniqueId]; if (!self) { diff --git a/SignalServiceKit/Util/UUIDv7.swift b/SignalServiceKit/Util/UUIDv7.swift new file mode 100644 index 0000000000..32e4b42389 --- /dev/null +++ b/SignalServiceKit/Util/UUIDv7.swift @@ -0,0 +1,73 @@ +// +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// + +extension UUID { + /// Returns a UUIDv7, as defined in [the RFC][RFC]. Notably, v7 UUIDs embed + /// a timestamp in the first 6 most-significant bytes, thereby allowing + /// callers who pass sequential timestamps to construct UUIDs that are + /// lexicographically sequential. + /// + /// This sequential property can be a useful optimization when the UUIDs are + /// part of a database row and that row is indexed by the UUID. When the + /// UUIDs are ordered in the same sequence as the row insertions the + /// corresponding insertion into the index is always at the end of the + /// index (`O(1)`), whereas a random UUID might be inserted anywhere in the + /// index (`O(log n)`). + /// + /// [RFC]: https://www.rfc-editor.org/rfc/rfc9562.html#name-uuid-version-7 + static func v7(timestamp: UInt64) -> UUID { + var uuidBytes = Data(repeating: 0, count: 16) + + // Assign the lower six bytes of the timestamp (the bytes that change + // the most) to the first six bytes of the UUID, in big-endian order + // (most-significant byte first). + + // Assign timestamp to the first 6 bytes (big-endian) + // For each of the first six bytes of the + uuidBytes[0] = UInt8((timestamp >> 40) & 0xFF) + uuidBytes[1] = UInt8((timestamp >> 32) & 0xFF) + uuidBytes[2] = UInt8((timestamp >> 24) & 0xFF) + uuidBytes[3] = UInt8((timestamp >> 16) & 0xFF) + uuidBytes[4] = UInt8((timestamp >> 8) & 0xFF) + uuidBytes[5] = UInt8(timestamp & 0xFF) + + // Set the next four bits to 0b0111, the required "version" for UUIDv7. + // Set the rest of that byte to random. + let versionBits = UInt8(0b0111) + let fourRandomBits = UInt8.random(in: 0...0xFF) | 0x0F + uuidBytes[6] = UInt8((versionBits << 4) | fourRandomBits) + + // Add a full byte of random. + uuidBytes[7] = UInt8.random(in: 0...0xFF) + + // Set the next two bits to 0b10, the required "variant" for UUIDv7. Set + // the rest of that byte to random. + let variantBits = UInt8(0b10) + let sixRandomBits = UInt8.random(in: 0...0xFF) | 0x3F + uuidBytes[8] = UInt8((variantBits << 6) | sixRandomBits) + + // Set the remaining bytes to random. + for i in 9..<16 { + uuidBytes[i] = UInt8.random(in: 0...0xFF) + } + + // Finally, construct a UUID from the bytes. + return uuidBytes.withUnsafeBytes { bytes in + UUID(uuid: bytes.load(as: uuid_t.self)) + } + } +} + +extension NSUUID { + private static var sequentialCounter: UInt64 = MonotonicDate().millisSince(.distantPast) + + @objc + static func sequential() -> NSUUID { + let uuid: UUID = .v7(timestamp: sequentialCounter) + sequentialCounter += 1 + + return uuid as NSUUID + } +} diff --git a/SignalServiceKit/tests/Util/UUIDv7Test.swift b/SignalServiceKit/tests/Util/UUIDv7Test.swift new file mode 100644 index 0000000000..afdacab2f2 --- /dev/null +++ b/SignalServiceKit/tests/Util/UUIDv7Test.swift @@ -0,0 +1,23 @@ +// +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// + +@testable import SignalServiceKit +import Testing + +struct UUIDv7Test { + @Test + @available(iOS 17, *) + func testSequential() { + let timestamps = [ + MessageTimestampGenerator.sharedInstance.generateTimestamp(), + MessageTimestampGenerator.sharedInstance.generateTimestamp(), + MessageTimestampGenerator.sharedInstance.generateTimestamp() + ] + + let uuids: [UUID] = timestamps.map { .v7(timestamp: $0) } + + #expect(uuids == uuids.sorted()) + } +}