From a237b9c1148860978d0314809702c0dbd7768628 Mon Sep 17 00:00:00 2001 From: Sasha Weiss Date: Thu, 18 Dec 2025 17:11:36 -0800 Subject: [PATCH] Simplify and consolidate "DB Corruption" handling --- Scripts/sds_codegen/sds_generate.py | 25 +-- Signal/AppLaunch/AppDelegate.swift | 185 +++++++----------- Signal/AppLaunch/SignalApp.swift | 42 ---- .../Components/CVComponentState.swift | 22 +-- .../InternalSettingsViewController.swift | 24 ++- .../DatabaseRecoveryViewController.swift | 55 ++++-- .../ViewControllers/DebugUI/DebugUIMisc.swift | 16 -- .../translations/en.lproj/Localizable.strings | 25 ++- ...ckupArchiveTSMessageContentsArchiver.swift | 23 +-- .../BackupArchiveFullTextSearchIndexer.swift | 56 +----- .../Calls/CallRecord/CallRecordStore.swift | 14 +- .../DeletedCallRecordStore.swift | 13 +- .../Group/GroupCallInteractionFinder.swift | 8 +- .../Contacts/AuthorMergeObserver.swift | 8 +- ...isappearingMessagesConfiguration+SDS.swift | 25 +-- .../Contacts/RecipientDatabaseTable.swift | 34 +--- SignalServiceKit/Contacts/TSThread+SDS.swift | 25 +-- SignalServiceKit/Contacts/ThreadMerger.swift | 38 ++-- .../Debugging/OWSSwiftUtils.swift | 8 +- SignalServiceKit/Devices/OWSDeviceStore.swift | 6 +- SignalServiceKit/Environment/AppSetup.swift | 3 +- SignalServiceKit/Environment/BuildFlags.swift | 4 - .../Groups/GroupMemberStore.swift | 24 ++- SignalServiceKit/Groups/TSGroupMember.swift | 42 ++-- ...nReceiptCredentialRedemptionJobQueue.swift | 8 +- .../Jobs/SendGiftBadgeJobQueue.swift | 8 +- .../Messages/Edit/EditMessageStore.swift | 8 +- .../Messages/GroupMessageProcessor.swift | 49 ++--- .../GroupMessageProcessorJobStore.swift | 30 +-- .../InteractionDeleteManager.swift | 10 +- .../Messages/Interactions/MentionFinder.swift | 6 +- .../Interactions/TSInteraction+SDS.swift | 25 +-- .../Messages/Interactions/TSMessage.swift | 12 +- .../Messages/OWSReceiptManager.swift | 9 +- .../Payments/ArchivedPaymentStore.swift | 36 +--- .../Messages/Reactions/ReactionFinder.swift | 9 +- .../Stickers/InstalledSticker+SDS.swift | 25 +-- .../Messages/Stickers/StickerPack+SDS.swift | 25 +-- .../Messages/Stories/StoryFinder.swift | 8 +- .../Payments/TSPaymentModel+SDS.swift | 25 +-- .../TSPaymentsActivationRequestModel.swift | 17 +- .../Search/FullTextSearchIndexer.swift | 50 +++-- .../Storage/Database/Database+Execute.swift | 58 +++--- .../Database/DatabaseCorruptionState.swift | 102 ++-------- .../Storage/Database/DatabaseRecovery.swift | 138 ++++++------- .../Database/GRDBDatabaseStorageAdapter.swift | 1 - .../Storage/Database/GRDBSchemaMigrator.swift | 2 +- .../Storage/Database/KeyValueStore.swift | 47 +---- .../Database/Records/InteractionFinder.swift | 64 +----- .../Database/Records/ThreadFinder.swift | 19 +- ...ableModelDatabaseInterface+Enumerate.swift | 8 +- ...SCodableModelDatabaseInterface+Fetch.swift | 18 +- ...CodableModelDatabaseInterface+Remove.swift | 9 +- ...DSCodableModelDatabaseInterface+Save.swift | 18 +- .../SDSDatabaseStorage.swift | 101 +++------- .../Storage/Database/SDSModel.swift | 7 +- .../Storage/Database/SDSRecord.swift | 25 +-- .../Donations/DonationReceiptFinder.swift | 16 +- SignalServiceKit/Threads/ThreadStore.swift | 7 +- .../GroupMessageProcessorJobTest.swift | 6 +- .../DatabaseCorruptionStateTest.swift | 34 ++-- .../Database/DatabaseRecoveryTest.swift | 26 +-- .../RecipientPickers/ConversationPicker.swift | 4 +- 63 files changed, 578 insertions(+), 1217 deletions(-) diff --git a/Scripts/sds_codegen/sds_generate.py b/Scripts/sds_codegen/sds_generate.py index 0c67f7c74a..e8c02310fa 100755 --- a/Scripts/sds_codegen/sds_generate.py +++ b/Scripts/sds_codegen/sds_generate.py @@ -2011,17 +2011,14 @@ public extension %(class_name)s { @objc public class %sCursor: NSObject, SDSCursor { private let transaction: DBReadTransaction - private let cursor: RecordCursor<%s>? + private let cursor: RecordCursor<%s> - init(transaction: DBReadTransaction, cursor: RecordCursor<%s>?) { + init(transaction: DBReadTransaction, cursor: RecordCursor<%s>) { self.transaction = transaction self.cursor = cursor } public func next() throws -> %s? { - guard let cursor = cursor else { - return nil - } guard let record = try cursor.next() else { return nil }""" % ( @@ -2076,16 +2073,9 @@ public extension %(class_name)s { @nonobjc class func grdbFetchCursor(transaction: DBReadTransaction) -> %(class_name)sCursor { let database = transaction.database - do { + return failIfThrows { let cursor = try %(record_name)s.fetchCursor(database) return %(class_name)sCursor(transaction: transaction, cursor: cursor) - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFailDebug("Read failed: \\(error)") - return %(class_name)sCursor(transaction: transaction, cursor: nil) } } """ % { @@ -2215,17 +2205,10 @@ public extension %(class_name)s { class func grdbFetchCursor(sql: String, arguments: StatementArguments = StatementArguments(), transaction: DBReadTransaction) -> %(class_name)sCursor { - do { + return failIfThrows { let sqlRequest = SQLRequest(sql: sql, arguments: arguments, cached: true) let cursor = try %(record_name)s.fetchCursor(transaction.database, sqlRequest) return %(class_name)sCursor(transaction: transaction, cursor: cursor) - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFailDebug("Read failed: \\(error)") - return %(class_name)sCursor(transaction: transaction, cursor: nil) } } """ % { diff --git a/Signal/AppLaunch/AppDelegate.swift b/Signal/AppLaunch/AppDelegate.swift index a1262bdfd2..b778d06cc3 100644 --- a/Signal/AppLaunch/AppDelegate.swift +++ b/Signal/AppLaunch/AppDelegate.swift @@ -11,35 +11,6 @@ import SignalUI import UIKit import WebRTC -enum LaunchPreflightError { - case unknownDatabaseVersion - case couldNotRestoreTransferredData - case databaseCorruptedAndMightBeRecoverable - case databaseUnrecoverablyCorrupted - case lastAppLaunchCrashed - case lowStorageSpaceAvailable - case possibleReadCorruptionCrashed - - var supportTag: String { - switch self { - case .unknownDatabaseVersion: - return "LaunchFailure_UnknownDatabaseVersion" - case .couldNotRestoreTransferredData: - return "LaunchFailure_CouldNotRestoreTransferredData" - case .databaseCorruptedAndMightBeRecoverable: - return "LaunchFailure_DatabaseCorruptedAndMightBeRecoverable" - case .databaseUnrecoverablyCorrupted: - return "LaunchFailure_DatabaseUnrecoverablyCorrupted" - case .lastAppLaunchCrashed: - return "LaunchFailure_LastAppLaunchCrashed" - case .lowStorageSpaceAvailable: - return "LaunchFailure_NoDiskSpaceAvailable" - case .possibleReadCorruptionCrashed: - return "LaunchFailure_PossibleReadCorruption" - } - } -} - private func uncaughtExceptionHandler(_ exception: NSException) { if DebugFlags.internalLogging { Logger.error("exception: \(exception)") @@ -228,8 +199,18 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { let viewController = terminalErrorViewController() _ = initializeWindow(mainAppContext: mainAppContext, rootViewController: viewController) - presentDatabaseUnrecoverablyCorruptedError( + presentLaunchFailureActionSheet( from: viewController, + supportTag: "LaunchFailure_DatabaseLoadFailed", + logDumper: .preLaunch(), + title: OWSLocalizedString( + "APP_LAUNCH_FAILURE_COULD_NOT_LOAD_DATABASE", + comment: "Error indicating that the app could not launch because the database could not be loaded." + ), + message: OWSLocalizedString( + "APP_LAUNCH_FAILURE_ALERT_MESSAGE", + comment: "Default message for the 'app launch failed' alert." + ), actions: [ .submitDebugLogsAndCrash, .wipeAppDataAndCrash(keyFetcher: GRDBKeyFetcher(keychainStorage: keychainStorage)), @@ -861,8 +842,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { object: nil ) - checkDatabaseIntegrityIfNecessary(isRegistered: registeredState != nil) - SignalApp.shared.showLaunchInterface( launchInterface, appReadiness: appReadiness, @@ -984,6 +963,29 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { private var shouldKillAppWhenBackgrounded: Bool = false + private enum LaunchPreflightError { + case unknownDatabaseVersion + case couldNotRestoreTransferredData + case databaseCorrupted + case lastAppLaunchCrashed + case lowStorageSpaceAvailable + + var supportTag: String { + switch self { + case .unknownDatabaseVersion: + return "LaunchFailure_UnknownDatabaseVersion" + case .couldNotRestoreTransferredData: + return "LaunchFailure_CouldNotRestoreTransferredData" + case .databaseCorrupted: + return "LaunchFailure_DatabaseCorrupted" + case .lastAppLaunchCrashed: + return "LaunchFailure_LastAppLaunchCrashed" + case .lowStorageSpaceAvailable: + return "LaunchFailure_NoDiskSpaceAvailable" + } + } + } + private func checkIfAllowedToLaunch( mainAppContext: MainAppContext, appVersion: AppVersion, @@ -1007,25 +1009,19 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { let databaseCorruptionState = DatabaseCorruptionState(userDefaults: userDefaults) switch databaseCorruptionState.status { - case .notCorrupted, .readCorrupted: + case .notCorrupted: break case .corrupted, .corruptedButAlreadyDumpedAndRestored: guard !UIDevice.current.isIPad else { // Database recovery theoretically works on iPad, // but we haven't built the UI for it. - return .databaseUnrecoverablyCorrupted + return .lastAppLaunchCrashed } - guard databaseCorruptionState.count <= 5 else { - return .databaseUnrecoverablyCorrupted - } - return .databaseCorruptedAndMightBeRecoverable + return .databaseCorrupted } let launchAttemptFailureThreshold = DebugFlags.betaLogging ? 2 : 3 if userDefaults.integer(forKey: Constants.appLaunchesAttemptedKey) >= launchAttemptFailureThreshold { - if case .readCorrupted = databaseCorruptionState.status { - return .possibleReadCorruptionCrashed - } return .lastAppLaunchCrashed } @@ -1045,23 +1041,21 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { let actions: [LaunchFailureActionSheetAction] switch preflightError { - case .databaseCorruptedAndMightBeRecoverable, .possibleReadCorruptionCrashed: - presentDatabaseRecovery( - from: viewController, - launchContext: launchContext, - window: window + case .databaseCorrupted: + title = OWSLocalizedString( + "APP_LAUNCH_FAILURE_DATABASE_CORRUPTED_TITLE", + comment: "Title for an action sheet explaining that Signal can't launch because the database is corrupted." ) - return - - case .databaseUnrecoverablyCorrupted: - presentDatabaseUnrecoverablyCorruptedError( - from: viewController, - actions: [ - .submitDebugLogsWithDatabaseIntegrityCheckAndCrash(databaseStorage: launchContext.databaseStorage), - .wipeAppDataAndCrash(keyFetcher: GRDBKeyFetcher(keychainStorage: launchContext.keychainStorage)), - ] + message = OWSLocalizedString( + "APP_LAUNCH_FAILURE_DATABASE_CORRUPTED_MESSAGE", + comment: "Message for an action sheet explaining that Signal can't launch because the database is corrupted." ) - return + actions = [ + .presentDatabaseRecovery(window: window, launchContext: launchContext), + .submitDebugLogsAndCrash, + .launchApp(window: window, launchContext: launchContext), + .wipeAppDataAndCrash(keyFetcher: GRDBKeyFetcher(keychainStorage: launchContext.keychainStorage)), + ] case .unknownDatabaseVersion: title = OWSLocalizedString( @@ -1124,8 +1118,8 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { private func presentDatabaseRecovery( from viewController: UIViewController, + window: UIWindow, launchContext: LaunchContext, - window: UIWindow ) { var launchContext = launchContext let recoveryViewController = DatabaseRecoveryViewController<(AppSetup.FinalContinuation, DeviceSleepBlockObject)>( @@ -1164,30 +1158,10 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { viewController.present(recoveryViewController, animated: true) } - private func presentDatabaseUnrecoverablyCorruptedError( - from viewController: UIViewController, - actions: [LaunchFailureActionSheetAction], - ) { - presentLaunchFailureActionSheet( - from: viewController, - supportTag: LaunchPreflightError.databaseUnrecoverablyCorrupted.supportTag, - logDumper: .preLaunch(), - title: OWSLocalizedString( - "APP_LAUNCH_FAILURE_COULD_NOT_LOAD_DATABASE", - comment: "Error indicating that the app could not launch because the database could not be loaded." - ), - message: OWSLocalizedString( - "APP_LAUNCH_FAILURE_ALERT_MESSAGE", - comment: "Default message for the 'app launch failed' alert." - ), - actions: actions - ) - } - private enum LaunchFailureActionSheetAction { case submitDebugLogsAndCrash case submitDebugLogsAndLaunchApp(window: UIWindow, launchContext: LaunchContext) - case submitDebugLogsWithDatabaseIntegrityCheckAndCrash(databaseStorage: SDSDatabaseStorage) + case presentDatabaseRecovery(window: UIWindow, launchContext: LaunchContext) case wipeAppDataAndCrash(keyFetcher: GRDBKeyFetcher) case launchApp(window: UIWindow, launchContext: LaunchContext) } @@ -1227,6 +1201,13 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { func ignoreErrorAndLaunchApp(in window: UIWindow, launchContext: LaunchContext) { // Pretend we didn't fail! self.didAppLaunchFail = false + + // If we're wrong about this, we'll find out pretty quickly when a + // database operation fails. + DatabaseCorruptionState.flagDatabaseAsNotCorrupted( + userDefaults: launchContext.appContext.appUserDefaults(), + ) + let loadingViewController = LoadingViewController() window.rootViewController = loadingViewController self.launchApp( @@ -1252,17 +1233,20 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { } } - case .submitDebugLogsWithDatabaseIntegrityCheckAndCrash(let databaseStorage): - addSubmitDebugLogsAction { [unowned viewController] in - SignalApp.shared.showDatabaseIntegrityCheckUI( - from: viewController, - databaseStorage: databaseStorage, - ) { - DebugLogs.submitLogs(supportTag: supportTag, dumper: logDumper) { - owsFail("Exiting after submitting debug logs") - } + case .presentDatabaseRecovery(let window, let launchContext): + actionSheet.addAction(.init( + title: OWSLocalizedString( + "APP_LAUNCH_FAILURE_DATABASE_RECOVERY_ACTION_TITLE", + comment: "Action in an action sheet offering to attempt recovery of a corrupted database.", + ), + handler: { [self] _ in + presentDatabaseRecovery( + from: viewController, + window: window, + launchContext: launchContext, + ) } - } + )) case .wipeAppDataAndCrash(let keyFetcher): let wipeAppDataActionTitle = OWSLocalizedString( @@ -1300,7 +1284,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { "APP_LAUNCH_FAILURE_CONTINUE", comment: "Button to try launching the app even though the last launch failed" ), - style: .cancel, // Use a cancel-style button to draw attention. handler: { [unowned window] _ in ignoreErrorAndLaunchApp(in: window, launchContext: launchContext) } @@ -1311,6 +1294,8 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { viewController.presentActionSheet(actionSheet) } + // MARK: - + public func userNotificationCenter( _ center: UNUserNotificationCenter, willPresent notification: UNNotification, @@ -1889,28 +1874,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { } return true } - - // MARK: - Database integrity checks - - private func checkDatabaseIntegrityIfNecessary( - isRegistered: Bool - ) { - guard isRegistered, BuildFlags.periodicallyCheckDatabaseIntegrity else { return } - - let appReadiness: AppReadiness = self.appReadiness - DispatchQueue.sharedUtility.async { - switch GRDBDatabaseStorageAdapter.checkIntegrity(databaseStorage: SSKEnvironment.shared.databaseStorageRef) { - case .ok: break - case .notOk: - appReadiness.runNowOrWhenUIDidBecomeReadySync { - OWSActionSheets.showActionSheet( - title: "Database corrupted!", - message: "We have detected database corruption on your device. Please submit debug logs to the iOS team." - ) - } - } - } - } } // MARK: - UNUserNotificationCenterDelegate diff --git a/Signal/AppLaunch/SignalApp.swift b/Signal/AppLaunch/SignalApp.swift index 5f6d50a454..4d40d8260b 100644 --- a/Signal/AppLaunch/SignalApp.swift +++ b/Signal/AppLaunch/SignalApp.swift @@ -397,46 +397,4 @@ public class SignalApp { }) parentVC.present(alert, animated: true) } - - public func showDatabaseIntegrityCheckUI( - from parentVC: UIViewController, - databaseStorage: SDSDatabaseStorage, - completion: @escaping () -> Void = {} - ) { - let alert = UIAlertController( - title: OWSLocalizedString("DATABASE_INTEGRITY_CHECK_TITLE", - comment: "Title for alert before running a database integrity check"), - message: OWSLocalizedString("DATABASE_INTEGRITY_CHECK_MESSAGE", - comment: "Message for alert before running a database integrity check"), - preferredStyle: .alert) - alert.addAction(.init(title: OWSLocalizedString("DATABASE_INTEGRITY_CHECK_ACTION_RUN", - comment: "Button to run the database integrity check"), - style: .default) { _ in - let progressView = UIActivityIndicatorView(style: .large) - progressView.color = .gray - parentVC.view.addSubview(progressView) - progressView.autoCenterInSuperview() - progressView.startAnimating() - - var backgroundTask: OWSBackgroundTask? = OWSBackgroundTask(label: "showDatabaseIntegrityCheckUI") - - DispatchQueue.sharedUserInitiated.async { - GRDBDatabaseStorageAdapter.checkIntegrity(databaseStorage: databaseStorage) - - owsAssertDebug(backgroundTask != nil) - backgroundTask = nil - - DispatchQueue.main.async { - progressView.removeFromSuperview() - completion() - } - } - }) - alert.addAction(.init(title: OWSLocalizedString("DATABASE_INTEGRITY_CHECK_SKIP", - comment: "Button to skip database integrity check step"), - style: .cancel) { _ in - completion() - }) - parentVC.present(alert, animated: true) - } } diff --git a/Signal/ConversationView/Components/CVComponentState.swift b/Signal/ConversationView/Components/CVComponentState.swift index f067468d19..980411de95 100644 --- a/Signal/ConversationView/Components/CVComponentState.swift +++ b/Signal/ConversationView/Components/CVComponentState.swift @@ -1102,19 +1102,15 @@ fileprivate extension CVComponentState.Builder { } if let archivedPaymentMessage = message as? OWSArchivedPaymentMessage { - do { - let archivedPayment = try DependenciesBridge.shared.archivedPaymentStore.fetch( - for: archivedPaymentMessage, - interactionUniqueId: message.uniqueId, - tx: transaction - ) - return buildArchivedPaymentAttachment( - archivedPaymentMessage: archivedPaymentMessage, - archivedPayment: archivedPayment - ) - } catch { - owsFail("\(error.grdbErrorForLogging)") - } + let archivedPayment = DependenciesBridge.shared.archivedPaymentStore.fetch( + for: archivedPaymentMessage, + interactionUniqueId: message.uniqueId, + tx: transaction + ) + return buildArchivedPaymentAttachment( + archivedPaymentMessage: archivedPaymentMessage, + archivedPayment: archivedPayment + ) } if let giftBadge = message.giftBadge { diff --git a/Signal/src/ViewControllers/AppSettings/Internal/InternalSettingsViewController.swift b/Signal/src/ViewControllers/AppSettings/Internal/InternalSettingsViewController.swift index 8c6d738360..d906ffbed6 100644 --- a/Signal/src/ViewControllers/AppSettings/Internal/InternalSettingsViewController.swift +++ b/Signal/src/ViewControllers/AppSettings/Internal/InternalSettingsViewController.swift @@ -95,13 +95,25 @@ class InternalSettingsViewController: OWSTableViewController2 { debugSection.add(.actionItem( withText: "Run Database Integrity Checks", actionBlock: { [weak self] in - guard let self = self else { - return + guard let self else { return } + + ModalActivityIndicatorViewController.present( + fromViewController: self + ) { modal in + let databaseStorage = SSKEnvironment.shared.databaseStorageRef + let integrityCheckResult = GRDBDatabaseStorageAdapter.checkIntegrity( + databaseStorage: databaseStorage, + ) + + modal.dismiss { + switch integrityCheckResult { + case .ok: + self.presentToast(text: "Integrity check: ok! More detail in logs.") + case .notOk: + self.presentToast(text: "Integrity check: not ok! More detail in logs.") + } + } } - SignalApp.shared.showDatabaseIntegrityCheckUI( - from: self, - databaseStorage: SSKEnvironment.shared.databaseStorageRef, - ) } )) debugSection.add(.actionItem( diff --git a/Signal/src/ViewControllers/DatabaseRecoveryViewController.swift b/Signal/src/ViewControllers/DatabaseRecoveryViewController.swift index 5ea247b23f..318e20f4ea 100644 --- a/Signal/src/ViewControllers/DatabaseRecoveryViewController.swift +++ b/Signal/src/ViewControllers/DatabaseRecoveryViewController.swift @@ -11,6 +11,7 @@ class DatabaseRecoveryViewController: OWSViewController { private let corruptDatabaseStorage: SDSDatabaseStorage private let deviceSleepManager: DeviceSleepManagerImpl private let keychainStorage: any KeychainStorage + private let logger: PrefixedLogger private let setupSskEnvironment: (SDSDatabaseStorage) -> Task private let launchApp: (SetupResult) -> Void @@ -26,6 +27,8 @@ class DatabaseRecoveryViewController: OWSViewController { self.corruptDatabaseStorage = corruptDatabaseStorage self.deviceSleepManager = deviceSleepManager self.keychainStorage = keychainStorage + self.logger = PrefixedLogger(prefix: "[DatabaseRecovery]") + self.setupSskEnvironment = setupSskEnvironment self.launchApp = launchApp super.init() @@ -209,7 +212,10 @@ class DatabaseRecoveryViewController: OWSViewController { @objc private func didRequestToSubmitDebugLogs() { self.dismiss(animated: true) { - DebugLogs.submitLogs(supportTag: LaunchPreflightError.databaseCorruptedAndMightBeRecoverable.supportTag, dumper: .preLaunch()) + DebugLogs.submitLogs( + supportTag: "LaunchFailure_DatabaseRecoveryFailed", + dumper: .preLaunch(), + ) } } @@ -225,9 +231,9 @@ class DatabaseRecoveryViewController: OWSViewController { state = .recovering(fractionCompleted: 0) // We might not run all the steps (see comment below). We could use that to adjust the - // progress's unit count but that makes the code more complicated, so we just set it to 4 + // progress's unit count but that makes the code more complicated, so we just set it to 5 // for simplicity. - let progress = Progress(totalUnitCount: 4) + let progress = Progress(totalUnitCount: 5) let progressObserver = progress.observe(\.fractionCompleted, options: [.new]) { [weak self] _, _ in self?.didFractionCompletedChange(fractionCompleted: progress.fractionCompleted) @@ -244,7 +250,7 @@ class DatabaseRecoveryViewController: OWSViewController { // // Otherwise... // - // 1. Try to rebuild the existing database. If that clears corruption, skip steps 2 and 4. + // 1. Try to reindex the existing database. If that clears corruption, skip steps 2 and 4. // 2. Dump and restore. // 3. Set up the environment. // 4. Do a manual recreate. @@ -253,11 +259,16 @@ class DatabaseRecoveryViewController: OWSViewController { switch DatabaseCorruptionState(userDefaults: userDefaults).status { case .notCorrupted: owsFail("Database was not corrupted! Why are we on this screen?") - case .corrupted, .readCorrupted: + case .corrupted: promise = firstly(on: DispatchQueue.sharedUserInitiated) { () -> Promise in + self.logger.info("Integrity check on untouched database...") progress.performAsCurrent(withPendingUnitCount: 1) { - DatabaseRecovery.rebuildExistingDatabase(databaseStorage: self.corruptDatabaseStorage) + _ = DatabaseRecovery.integrityCheck(databaseStorage: self.corruptDatabaseStorage) } + progress.performAsCurrent(withPendingUnitCount: 1) { + DatabaseRecovery.reindex(databaseStorage: self.corruptDatabaseStorage) + } + self.logger.info("Integrity check, again...") let integrity = progress.performAsCurrent(withPendingUnitCount: 1) { return DatabaseRecovery.integrityCheck(databaseStorage: self.corruptDatabaseStorage) } @@ -269,17 +280,19 @@ class DatabaseRecoveryViewController: OWSViewController { } if shouldDumpAndRecreate { - let dumpAndRestore = DatabaseRecovery.DumpAndRestore( + let dumpAndRestoreOperation = DatabaseRecovery.DumpAndRestoreOperation( appReadiness: self.appReadiness, corruptDatabaseStorage: self.corruptDatabaseStorage, keychainStorage: self.keychainStorage ) - progress.addChild(dumpAndRestore.progress, withPendingUnitCount: 1) + progress.addChild(dumpAndRestoreOperation.progress, withPendingUnitCount: 1) + do { - try dumpAndRestore.run() + try dumpAndRestoreOperation.run() } catch { return Promise(error: error) } + DatabaseCorruptionState.flagCorruptedDatabaseAsDumpedAndRestored(userDefaults: self.userDefaults) } else { progress.completedUnitCount += 1 @@ -293,13 +306,14 @@ class DatabaseRecoveryViewController: OWSViewController { databaseFileUrl: self.corruptDatabaseStorage.databaseFileUrl, keychainStorage: self.keychainStorage ) + return Guarantee.wrapAsync { await self.setupSskEnvironment(databaseStorage).value }.map(on: DispatchQueue.sharedUserInitiated) { setupResult in if shouldDumpAndRecreate { - let manualRecreation = DatabaseRecovery.ManualRecreation(databaseStorage: databaseStorage) - progress.addChild(manualRecreation.progress, withPendingUnitCount: 1) - manualRecreation.run() + let recreateFTSIndexOperation = DatabaseRecovery.RecreateFTSIndexOperation(databaseStorage: databaseStorage) + progress.addChild(recreateFTSIndexOperation.progress, withPendingUnitCount: 1) + recreateFTSIndexOperation.run() } else { progress.completedUnitCount += 1 } @@ -310,18 +324,21 @@ class DatabaseRecoveryViewController: OWSViewController { promise = Guarantee.wrapAsync { await self.setupSskEnvironment(self.corruptDatabaseStorage).value }.map(on: DispatchQueue.sharedUserInitiated) { setupResult in - let manualRecreation = DatabaseRecovery.ManualRecreation(databaseStorage: self.corruptDatabaseStorage) - progress.addChild( - manualRecreation.progress, - withPendingUnitCount: progress.remainingUnitCount + let recreateFTSIndexOperation = DatabaseRecovery.RecreateFTSIndexOperation( + databaseStorage: self.corruptDatabaseStorage ) - manualRecreation.run() + progress.addChild( + recreateFTSIndexOperation.progress, + withPendingUnitCount: progress.remainingUnitCount, + ) + recreateFTSIndexOperation.run() + return setupResult } } promise.done(on: DispatchQueue.main) { setupResult in - DatabaseCorruptionState.flagDatabaseAsRecoveredFromCorruption(userDefaults: self.userDefaults) + DatabaseCorruptionState.flagDatabaseAsNotCorrupted(userDefaults: self.userDefaults) self.state = .recoverySucceeded(setupResult) }.ensure { progressObserver.invalidate() @@ -402,7 +419,7 @@ class DatabaseRecoveryViewController: OWSViewController { headlineLabel.text = OWSLocalizedString( "DATABASE_RECOVERY_AWAITING_USER_CONFIRMATION_TITLE", - comment: "In some cases, the user's message history can become corrupted, and a recovery interface is shown. The user has not been hacked and may be confused by this interface, so try to avoid using terms like \"database\" or \"corrupted\"—terms like \"message history\" are better. This is the title on the first screen of this interface, which gives them some information and asks them to continue." + comment: "In some cases, the user's message history can become corrupted, and a recovery interface is shown. This is the title on the first screen of this interface, which gives them some information and asks them to continue." ) stackView.addArrangedSubview(headlineLabel) diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMisc.swift b/Signal/src/ViewControllers/DebugUI/DebugUIMisc.swift index f8c3c166a0..6c03ca087e 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMisc.swift +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMisc.swift @@ -29,9 +29,6 @@ class DebugUIMisc: DebugUIPage { OWSTableItem(title: "Flag database as corrupted", actionBlock: { DebugUIMisc.showFlagDatabaseAsCorruptedUi() }), - OWSTableItem(title: "Flag database as read corrupted", actionBlock: { - DebugUIMisc.showFlagDatabaseAsReadCorruptedUi() - }), OWSTableItem(title: "Test spoiler animations", actionBlock: { let viewController = SpoilerAnimationTestController() @@ -93,19 +90,6 @@ class DebugUIMisc: DebugUIPage { owsFail("Crashing due to (intentional) database corruption") } } - - private static func showFlagDatabaseAsReadCorruptedUi() { - OWSActionSheets.showConfirmationAlert( - title: "Are you sure?", - message: "This will flag your database as possibly corrupted. It will not trigger a recovery on next startup. However, if you select the 'Make next app launch fail', the startup following the crash will funnel into the database recovery flow.", - proceedTitle: "Mark database corrupted on read", - proceedStyle: .destructive - ) { _ in - DatabaseCorruptionState.flagDatabaseAsReadCorrupted( - userDefaults: CurrentAppContext().appUserDefaults() - ) - } - } } #endif diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 070ba25367..72d33d6615 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -217,6 +217,15 @@ /* Error indicating that the app could not launch because the database could not be loaded. */ "APP_LAUNCH_FAILURE_COULD_NOT_LOAD_DATABASE" = "Could Not Load Database"; +/* Message for an action sheet explaining that Signal can't launch because the database is corrupted. */ +"APP_LAUNCH_FAILURE_DATABASE_CORRUPTED_MESSAGE" = "Signal can't launch because the database is corrupted. There has been no impact to the security of your account or messages."; + +/* Title for an action sheet explaining that Signal can't launch because the database is corrupted. */ +"APP_LAUNCH_FAILURE_DATABASE_CORRUPTED_TITLE" = "Database Corrupted"; + +/* Action in an action sheet offering to attempt recovery of a corrupted database. */ +"APP_LAUNCH_FAILURE_DATABASE_RECOVERY_ACTION_TITLE" = "Repair"; + /* Error indicating that the app could not launch without reverting unknown database migrations. */ "APP_LAUNCH_FAILURE_INVALID_DATABASE_VERSION_MESSAGE" = "Please upgrade to the latest version of Signal."; @@ -2461,23 +2470,11 @@ /* Tooltip highlighting the custom chat color controls. */ "CUSTOM_CHAT_COLOR_SETTINGS_TOOLTIP" = "Drag to change the direction of the gradient"; -/* Button to run the database integrity check */ -"DATABASE_INTEGRITY_CHECK_ACTION_RUN" = "Run"; - -/* Message for alert before running a database integrity check */ -"DATABASE_INTEGRITY_CHECK_MESSAGE" = "This will help us solve your issue and may take several minutes to complete. You may close Signal while this check runs, but please return to submit your logs."; - -/* Button to skip database integrity check step */ -"DATABASE_INTEGRITY_CHECK_SKIP" = "Skip"; - -/* Title for alert before running a database integrity check */ -"DATABASE_INTEGRITY_CHECK_TITLE" = "Run database diagnostic?"; - /* In some cases, the user's message history can become corrupted, and a recovery interface is shown. The user has not been hacked and may be confused by this interface, so keep that in mind. This is the description on the first screen of this interface, which gives them some information and asks them to continue. */ "DATABASE_RECOVERY_AWAITING_USER_CONFIRMATION_DESCRIPTION" = "Some of your message history may be corrupted. There has been no impact to the security of your account or messages. Tap “Continue” to recover your data."; -/* In some cases, the user's message history can become corrupted, and a recovery interface is shown. The user has not been hacked and may be confused by this interface, so try to avoid using terms like \"database\" or \"corrupted\"—terms like \"message history\" are better. This is the title on the first screen of this interface, which gives them some information and asks them to continue. */ -"DATABASE_RECOVERY_AWAITING_USER_CONFIRMATION_TITLE" = "Problem Retrieving Data"; +/* In some cases, the user's message history can become corrupted, and a recovery interface is shown. This is the title on the first screen of this interface, which gives them some information and asks them to continue. */ +"DATABASE_RECOVERY_AWAITING_USER_CONFIRMATION_TITLE" = "Repair"; /* On the database recovery screen, if the user's device storage is nearly full, Signal will not be able to recover the database. A warning screen, which can be bypassed if the user wishes, will be shown. This is the text on the button to bypass the warning. */ "DATABASE_RECOVERY_MORE_STORAGE_SPACE_NEEDED_CONTINUE_ANYWAY" = "Continue Anyway"; diff --git a/SignalServiceKit/Backups/Archiving/Archivers/ChatItem/BackupArchiveTSMessageContentsArchiver.swift b/SignalServiceKit/Backups/Archiving/Archivers/ChatItem/BackupArchiveTSMessageContentsArchiver.swift index 16e4381954..22349dc311 100644 --- a/SignalServiceKit/Backups/Archiving/Archivers/ChatItem/BackupArchiveTSMessageContentsArchiver.swift +++ b/SignalServiceKit/Backups/Archiving/Archivers/ChatItem/BackupArchiveTSMessageContentsArchiver.swift @@ -253,16 +253,11 @@ class BackupArchiveTSMessageContentsArchiver: BackupArchiveProtoStreamWriter { uniqueInteractionId: BackupArchive.InteractionUniqueId, context: BackupArchive.RecipientArchivingContext ) -> BackupArchive.ArchiveInteractionResult { - let historyItem: ArchivedPayment? - do { - historyItem = try archivedPaymentStore.fetch( - for: archivedPaymentMessage, - interactionUniqueId: uniqueInteractionId.value, - tx: context.tx - ) - } catch { - return .messageFailure([.archiveFrameError(.paymentInfoFetchFailed(error), uniqueInteractionId)]) - } + let historyItem = archivedPaymentStore.fetch( + for: archivedPaymentMessage, + interactionUniqueId: uniqueInteractionId.value, + tx: context.tx, + ) guard let historyItem else { return .messageFailure([.archiveFrameError(.missingPaymentInformation, uniqueInteractionId)]) } @@ -1354,13 +1349,7 @@ class BackupArchiveTSMessageContentsArchiver: BackupArchiveProtoStreamWriter { direction: direction, interactionUniqueId: message.uniqueId ) - do { - try archivedPaymentStore.insert(archivedPayment, tx: context.tx) - } catch { - return .messageFailure([ - .restoreFrameError(.databaseInsertionFailed(error), chatItemId) - ]) - } + archivedPaymentStore.insert(archivedPayment, tx: context.tx) return .success(()) } diff --git a/SignalServiceKit/Backups/Archiving/BackupArchiveFullTextSearchIndexer.swift b/SignalServiceKit/Backups/Archiving/BackupArchiveFullTextSearchIndexer.swift index e23d0ff385..88091477cd 100644 --- a/SignalServiceKit/Backups/Archiving/BackupArchiveFullTextSearchIndexer.swift +++ b/SignalServiceKit/Backups/Archiving/BackupArchiveFullTextSearchIndexer.swift @@ -3,10 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // -// -// Copyright 2024 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only -// +import GRDB public protocol BackupArchiveFullTextSearchIndexer { @@ -28,7 +25,6 @@ public class BackupArchiveFullTextSearchIndexerImpl: BackupArchiveFullTextSearch private let appReadiness: AppReadiness private let dateProvider: DateProviderMonotonic private let db: any DB - private let fullTextSearchIndexer: Shims.FullTextSearchIndexer private let interactionStore: InteractionStore private let kvStore: KeyValueStore private let logger: PrefixedLogger @@ -39,14 +35,12 @@ public class BackupArchiveFullTextSearchIndexerImpl: BackupArchiveFullTextSearch appReadiness: AppReadiness, dateProvider: @escaping DateProviderMonotonic, db: any DB, - fullTextSearchIndexer: Shims.FullTextSearchIndexer, interactionStore: InteractionStore, searchableNameIndexer: SearchableNameIndexer ) { self.appReadiness = appReadiness self.dateProvider = dateProvider self.db = db - self.fullTextSearchIndexer = fullTextSearchIndexer self.interactionStore = interactionStore self.kvStore = KeyValueStore(collection: "BackupFullTextSearchIndexerImpl") self.logger = PrefixedLogger(prefix: "[Backups]") @@ -141,7 +135,7 @@ public class BackupArchiveFullTextSearchIndexerImpl: BackupArchiveFullTextSearch finalizeBatch(tx: tx) return true } - try self.index(interaction, tx: tx) + index(interaction, tx: tx) maxInteractionRowIdSoFar = interaction.sqliteRowId processedCount += 1 } @@ -156,25 +150,20 @@ public class BackupArchiveFullTextSearchIndexerImpl: BackupArchiveFullTextSearch } } - private func index(_ interaction: TSInteraction, tx: DBWriteTransaction) throws { + private func index(_ interaction: TSInteraction, tx: DBWriteTransaction) { guard let message = interaction as? TSMessage else { return } - do { - try self.fullTextSearchIndexer.insert(message, tx: tx) - } catch let insertError { - do { - try self.fullTextSearchIndexer.update(message, tx: tx) - } catch { - throw insertError - } - } + + FullTextSearchIndexer.insert(message, tx: tx) if let bodyRanges = message.bodyRanges { let uniqueMentionedAcis = Set(bodyRanges.mentions.values) for mentionedAci in uniqueMentionedAcis { let mention = TSMention(uniqueMessageId: message.uniqueId, uniqueThreadId: message.uniqueThreadId, aci: mentionedAci) - try mention.save(tx.database) + failIfThrows { + try mention.save(tx.database) + } } } } @@ -217,32 +206,3 @@ public class BackupArchiveFullTextSearchIndexerImpl: BackupArchiveFullTextSearch static let batchDurationMs: UInt64 = 90 } } - -// MARK: - Shims - -extension BackupArchiveFullTextSearchIndexerImpl { - public enum Shims { - public typealias FullTextSearchIndexer = _BackupArchiveFullTextSearchIndexerImpl_FullTextSearchIndexerShim - } - public enum Wrappers { - public typealias FullTextSearchIndexer = _BackupArchiveFullTextSearchIndexerImpl_FullTextSearchIndexerWrapper - } -} - -public protocol _BackupArchiveFullTextSearchIndexerImpl_FullTextSearchIndexerShim { - func insert(_ message: TSMessage, tx: DBWriteTransaction) throws - func update(_ message: TSMessage, tx: DBWriteTransaction) throws -} - -public class _BackupArchiveFullTextSearchIndexerImpl_FullTextSearchIndexerWrapper: BackupArchiveFullTextSearchIndexerImpl.Shims.FullTextSearchIndexer { - - public init() {} - - public func insert(_ message: TSMessage, tx: DBWriteTransaction) throws { - try FullTextSearchIndexer.insert(message, tx: tx) - } - - public func update(_ message: TSMessage, tx: DBWriteTransaction) throws { - try FullTextSearchIndexer.update(message, tx: tx) - } -} diff --git a/SignalServiceKit/Calls/CallRecord/CallRecordStore.swift b/SignalServiceKit/Calls/CallRecord/CallRecordStore.swift index a5df1fc564..ee26af2b66 100644 --- a/SignalServiceKit/Calls/CallRecord/CallRecordStore.swift +++ b/SignalServiceKit/Calls/CallRecord/CallRecordStore.swift @@ -269,14 +269,18 @@ class CallRecordStoreImpl: CallRecordStore { intoThreadRowId intoRowId: Int64, tx: DBWriteTransaction ) { - tx.database.executeHandlingErrors( - sql: """ + failIfThrows { + let sql = """ UPDATE "\(CallRecord.databaseTableName)" SET "\(CallRecord.CodingKeys.threadRowId.rawValue)" = ? WHERE "\(CallRecord.CodingKeys.threadRowId.rawValue)" = ? - """, - arguments: [ intoRowId, fromRowId ] - ) + """ + + try tx.database.execute( + sql: sql, + arguments: [intoRowId, fromRowId], + ) + } } func fetch( diff --git a/SignalServiceKit/Calls/DeletedCallRecord/DeletedCallRecordStore.swift b/SignalServiceKit/Calls/DeletedCallRecord/DeletedCallRecordStore.swift index 4b635ceee6..6087691e6f 100644 --- a/SignalServiceKit/Calls/DeletedCallRecord/DeletedCallRecordStore.swift +++ b/SignalServiceKit/Calls/DeletedCallRecord/DeletedCallRecordStore.swift @@ -137,14 +137,17 @@ class DeletedCallRecordStoreImpl: DeletedCallRecordStore { intoThreadRowId intoRowId: Int64, tx: DBWriteTransaction ) { - tx.database.executeHandlingErrors( - sql: """ + failIfThrows { + let sql = """ UPDATE "\(DeletedCallRecord.databaseTableName)" SET "\(DeletedCallRecord.CodingKeys.threadRowId.rawValue)" = ? WHERE "\(DeletedCallRecord.CodingKeys.threadRowId.rawValue)" = ? - """, - arguments: [ intoRowId, fromRowId ] - ) + """ + try tx.database.execute( + sql: sql, + arguments: [intoRowId, fromRowId], + ) + } } // MARK: - diff --git a/SignalServiceKit/Calls/Group/GroupCallInteractionFinder.swift b/SignalServiceKit/Calls/Group/GroupCallInteractionFinder.swift index 055b79b1e2..24abe610e0 100644 --- a/SignalServiceKit/Calls/Group/GroupCallInteractionFinder.swift +++ b/SignalServiceKit/Calls/Group/GroupCallInteractionFinder.swift @@ -31,18 +31,12 @@ public final class GroupCallInteractionFinder { """ let arguments: StatementArguments = [thread.uniqueId, eraId] - do { + return failIfThrows { return try Bool.fetchOne( transaction.database, sql: sql, arguments: arguments ) ?? false - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFail("Failed to find group call") } } diff --git a/SignalServiceKit/Contacts/AuthorMergeObserver.swift b/SignalServiceKit/Contacts/AuthorMergeObserver.swift index 873b20b006..b305a1297f 100644 --- a/SignalServiceKit/Contacts/AuthorMergeObserver.swift +++ b/SignalServiceKit/Contacts/AuthorMergeObserver.swift @@ -58,8 +58,12 @@ class AuthorMergeObserver: RecipientMergeObserver { WHERE "\(table.phoneNumberColumn)" = ? AND "\(table.aciColumn)" IS NULL """ - let arguments: StatementArguments = [aciString, phoneNumber] - tx.database.executeHandlingErrors(sql: sql, arguments: arguments) + failIfThrows { + try tx.database.execute( + sql: sql, + arguments: [aciString, phoneNumber], + ) + } } SSKEnvironment.shared.modelReadCachesRef.evacuateAllCaches() } diff --git a/SignalServiceKit/Contacts/OWSDisappearingMessagesConfiguration+SDS.swift b/SignalServiceKit/Contacts/OWSDisappearingMessagesConfiguration+SDS.swift index 8815b12310..b7b257f660 100644 --- a/SignalServiceKit/Contacts/OWSDisappearingMessagesConfiguration+SDS.swift +++ b/SignalServiceKit/Contacts/OWSDisappearingMessagesConfiguration+SDS.swift @@ -298,17 +298,14 @@ public extension OWSDisappearingMessagesConfiguration { @objc public class OWSDisappearingMessagesConfigurationCursor: NSObject, SDSCursor { private let transaction: DBReadTransaction - private let cursor: RecordCursor? + private let cursor: RecordCursor - init(transaction: DBReadTransaction, cursor: RecordCursor?) { + init(transaction: DBReadTransaction, cursor: RecordCursor) { self.transaction = transaction self.cursor = cursor } public func next() throws -> OWSDisappearingMessagesConfiguration? { - guard let cursor = cursor else { - return nil - } guard let record = try cursor.next() else { return nil } @@ -334,16 +331,9 @@ public extension OWSDisappearingMessagesConfiguration { @nonobjc class func grdbFetchCursor(transaction: DBReadTransaction) -> OWSDisappearingMessagesConfigurationCursor { let database = transaction.database - do { + return failIfThrows { let cursor = try DisappearingMessagesConfigurationRecord.fetchCursor(database) return OWSDisappearingMessagesConfigurationCursor(transaction: transaction, cursor: cursor) - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFailDebug("Read failed: \(error)") - return OWSDisappearingMessagesConfigurationCursor(transaction: transaction, cursor: nil) } } @@ -420,17 +410,10 @@ public extension OWSDisappearingMessagesConfiguration { class func grdbFetchCursor(sql: String, arguments: StatementArguments = StatementArguments(), transaction: DBReadTransaction) -> OWSDisappearingMessagesConfigurationCursor { - do { + return failIfThrows { let sqlRequest = SQLRequest(sql: sql, arguments: arguments, cached: true) let cursor = try DisappearingMessagesConfigurationRecord.fetchCursor(transaction.database, sqlRequest) return OWSDisappearingMessagesConfigurationCursor(transaction: transaction, cursor: cursor) - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFailDebug("Read failed: \(error)") - return OWSDisappearingMessagesConfigurationCursor(transaction: transaction, cursor: nil) } } diff --git a/SignalServiceKit/Contacts/RecipientDatabaseTable.swift b/SignalServiceKit/Contacts/RecipientDatabaseTable.swift index 1cc7aa577a..f135fd8ce1 100644 --- a/SignalServiceKit/Contacts/RecipientDatabaseTable.swift +++ b/SignalServiceKit/Contacts/RecipientDatabaseTable.swift @@ -74,25 +74,15 @@ public struct RecipientDatabaseTable { } public func fetchRecipient(rowId: Int64, tx: DBReadTransaction) -> SignalRecipient? { - do { + return failIfThrows { return try SignalRecipient.fetchOne(tx.database, key: rowId) - } catch { - let grdbError = error.grdbErrorForLogging - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary(error: grdbError) - owsFailDebug("\(grdbError)") - return nil } } public func fetchRecipient(uniqueId: String, tx: DBReadTransaction) -> SignalRecipient? { let sql = "SELECT * FROM \(SignalRecipient.databaseTableName) WHERE \(signalRecipientColumn: .uniqueId) = ?" - do { + return failIfThrows { return try SignalRecipient.fetchOne(tx.database, sql: sql, arguments: [uniqueId]) - } catch { - let grdbError = error.grdbErrorForLogging - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary(error: grdbError) - owsFailDebug("\(grdbError)") - return nil } } @@ -104,30 +94,20 @@ public struct RecipientDatabaseTable { } }() let sql = "SELECT * FROM \(SignalRecipient.databaseTableName) WHERE \(signalRecipientColumn: serviceIdColumn) = ?" - do { + return failIfThrows { return try SignalRecipient.fetchOne(tx.database, sql: sql, arguments: [serviceId.serviceIdUppercaseString]) - } catch { - let grdbError = error.grdbErrorForLogging - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary(error: grdbError) - owsFailDebug("\(grdbError)") - return nil } } public func fetchRecipient(phoneNumber: String, transaction tx: DBReadTransaction) -> SignalRecipient? { let sql = "SELECT * FROM \(SignalRecipient.databaseTableName) WHERE \(signalRecipientColumn: .phoneNumber) = ?" - do { + return failIfThrows { return try SignalRecipient.fetchOne(tx.database, sql: sql, arguments: [phoneNumber]) - } catch { - let grdbError = error.grdbErrorForLogging - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary(error: grdbError) - owsFailDebug("\(grdbError)") - return nil } } public func enumerateAll(tx: DBReadTransaction, block: (SignalRecipient) -> Void) { - do { + failIfThrows { let cursor = try SignalRecipient.fetchCursor(tx.database) var hasMore = true while hasMore { @@ -139,10 +119,6 @@ public struct RecipientDatabaseTable { block(recipient) } } - } catch { - let grdbError = error.grdbErrorForLogging - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary(error: grdbError) - owsFailDebug("\(grdbError)") } } diff --git a/SignalServiceKit/Contacts/TSThread+SDS.swift b/SignalServiceKit/Contacts/TSThread+SDS.swift index bc814f4f5f..3c30042fa7 100644 --- a/SignalServiceKit/Contacts/TSThread+SDS.swift +++ b/SignalServiceKit/Contacts/TSThread+SDS.swift @@ -795,17 +795,14 @@ public extension TSThread { @objc public class TSThreadCursor: NSObject, SDSCursor { private let transaction: DBReadTransaction - private let cursor: RecordCursor? + private let cursor: RecordCursor - init(transaction: DBReadTransaction, cursor: RecordCursor?) { + init(transaction: DBReadTransaction, cursor: RecordCursor) { 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 } @@ -833,16 +830,9 @@ public extension TSThread { @nonobjc class func grdbFetchCursor(transaction: DBReadTransaction) -> TSThreadCursor { let database = transaction.database - do { + return failIfThrows { 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) } } @@ -933,17 +923,10 @@ public extension TSThread { class func grdbFetchCursor(sql: String, arguments: StatementArguments = StatementArguments(), transaction: DBReadTransaction) -> TSThreadCursor { - do { + return failIfThrows { let sqlRequest = SQLRequest(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) } } diff --git a/SignalServiceKit/Contacts/ThreadMerger.swift b/SignalServiceKit/Contacts/ThreadMerger.swift index da8ec345d3..e2eb113959 100644 --- a/SignalServiceKit/Contacts/ThreadMerger.swift +++ b/SignalServiceKit/Contacts/ThreadMerger.swift @@ -330,30 +330,38 @@ class _ThreadMerger_SDSThreadMergerWrapper: _ThreadMerger_SDSThreadMergerShim { private func mergeInteractions(_ threadPair: MergePair, tx: DBWriteTransaction) { let uniqueIds = threadPair.map { $0.uniqueId } - tx.database.executeHandlingErrors( - sql: """ + failIfThrows { + let sql = """ UPDATE "\(InteractionRecord.databaseTableName)" SET "\(interactionColumn: .threadUniqueId)" = ? WHERE "\(interactionColumn: .threadUniqueId)" = ? - """, - arguments: [uniqueIds.intoValue, uniqueIds.fromValue] - ) + """ + try tx.database.execute( + sql: sql, + arguments: [uniqueIds.intoValue, uniqueIds.fromValue], + ) + } } private func mergeReceiptsPendingMessageRequest(_ threadPair: MergePair, tx: DBWriteTransaction) { let threadRowIds = threadPair.map { $0.sqliteRowId! } - tx.database.executeHandlingErrors( - sql: """ + failIfThrows { + let viewedReceiptSQL = """ UPDATE "\(PendingViewedReceiptRecord.databaseTableName)" SET "threadId" = ? WHERE "threadId" = ? - """, - arguments: [threadRowIds.intoValue, threadRowIds.fromValue] - ) - tx.database.executeHandlingErrors( - sql: """ + """ + try tx.database.execute( + sql: viewedReceiptSQL, + arguments: [threadRowIds.intoValue, threadRowIds.fromValue], + ) + + let readReceiptSQL = """ UPDATE "\(PendingReadReceiptRecord.databaseTableName)" SET "threadId" = ? WHERE "threadId" = ? - """, - arguments: [threadRowIds.intoValue, threadRowIds.fromValue] - ) + """ + try tx.database.execute( + sql: readReceiptSQL, + arguments: [threadRowIds.intoValue, threadRowIds.fromValue] + ) + } } private func mergeMessageSendLogPayloads(_ threadPair: MergePair, tx: DBWriteTransaction) { diff --git a/SignalServiceKit/Debugging/OWSSwiftUtils.swift b/SignalServiceKit/Debugging/OWSSwiftUtils.swift index 0ccb291be4..5c66108080 100644 --- a/SignalServiceKit/Debugging/OWSSwiftUtils.swift +++ b/SignalServiceKit/Debugging/OWSSwiftUtils.swift @@ -88,8 +88,12 @@ public func failIfThrows( do { return try block() } catch { - DatabaseCorruptionState.flagDatabaseCorruptionIfNecessary(userDefaults: CurrentAppContext().appUserDefaults(), error: error) - owsFail("Couldn't write: \(error)", file: file, function: function, line: line) + if let error = error as? DatabaseError, error.resultCode == .SQLITE_CORRUPT { + DatabaseCorruptionState.flagDatabaseAsCorrupted(userDefaults: CurrentAppContext().appUserDefaults()) + owsFail("Failing due to database corruption. Extended result code: \(error.extendedResultCode)", file: file, function: function, line: line) + } else { + owsFail("Failing for unexpected throw: \(error.grdbErrorForLogging)", file: file, function: function, line: line) + } } } diff --git a/SignalServiceKit/Devices/OWSDeviceStore.swift b/SignalServiceKit/Devices/OWSDeviceStore.swift index f280b291c9..083b09dce3 100644 --- a/SignalServiceKit/Devices/OWSDeviceStore.swift +++ b/SignalServiceKit/Devices/OWSDeviceStore.swift @@ -26,12 +26,8 @@ public struct OWSDeviceStore { // MARK: - public func fetchAll(tx: DBReadTransaction) -> [OWSDevice] { - do { + return failIfThrows { return try OWSDevice.fetchAll(tx.database) - } catch { - owsFailDebug("Failed to fetch devices! \(error)") - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary(error: error) - return [] } } diff --git a/SignalServiceKit/Environment/AppSetup.swift b/SignalServiceKit/Environment/AppSetup.swift index b39056cdec..df231a825a 100644 --- a/SignalServiceKit/Environment/AppSetup.swift +++ b/SignalServiceKit/Environment/AppSetup.swift @@ -299,7 +299,7 @@ extension AppSetup.GlobalsContinuation { ) let ows2FAManager = OWS2FAManager() let paymentsHelper = testDependencies.paymentsHelper ?? PaymentsHelperImpl() - let archivedPaymentStore = ArchivedPaymentStoreImpl() + let archivedPaymentStore = ArchivedPaymentStore() let pniProtocolStore = SignalProtocolStore.build( dateProvider: dateProvider, identity: .pni, @@ -1481,7 +1481,6 @@ extension AppSetup.GlobalsContinuation { appReadiness: appReadiness, dateProvider: dateProviderMonotonic, db: db, - fullTextSearchIndexer: BackupArchiveFullTextSearchIndexerImpl.Wrappers.FullTextSearchIndexer(), interactionStore: interactionStore, searchableNameIndexer: searchableNameIndexer ), diff --git a/SignalServiceKit/Environment/BuildFlags.swift b/SignalServiceKit/Environment/BuildFlags.swift index 00a98f4f00..24661f10fb 100644 --- a/SignalServiceKit/Environment/BuildFlags.swift +++ b/SignalServiceKit/Environment/BuildFlags.swift @@ -35,10 +35,6 @@ public enum BuildFlags { public static let shouldUseTestIntervals = build <= .beta - /// If we ever need to internally detect database corruption again in the - /// future, we can re-enable this. - public static let periodicallyCheckDatabaseIntegrity: Bool = false - public enum Backups { public static let showMegaphones = build <= .internal public static let showOptimizeMedia = build <= .dev diff --git a/SignalServiceKit/Groups/GroupMemberStore.swift b/SignalServiceKit/Groups/GroupMemberStore.swift index 0bd43c2bd9..b6263f5a59 100644 --- a/SignalServiceKit/Groups/GroupMemberStore.swift +++ b/SignalServiceKit/Groups/GroupMemberStore.swift @@ -40,7 +40,13 @@ class GroupMemberStoreImpl: GroupMemberStore { FROM \(TSGroupMember.databaseTableName) WHERE \(TSGroupMember.columnName(.serviceId)) = ? """ - return tx.database.strictRead { try String.fetchAll($0, sql: sql, arguments: [serviceId.serviceIdUppercaseString]) } + return failIfThrows { + try String.fetchAll( + tx.database, + sql: sql, + arguments: [serviceId.serviceIdUppercaseString], + ) + } } func groupThreadIds(withFullMember phoneNumber: E164, tx: DBReadTransaction) -> [String] { @@ -53,7 +59,13 @@ class GroupMemberStoreImpl: GroupMemberStore { FROM \(TSGroupMember.databaseTableName) WHERE \(TSGroupMember.columnName(.phoneNumber)) = ? """ - return tx.database.strictRead { try String.fetchAll($0, sql: sql, arguments: [phoneNumber.stringValue]) } + return failIfThrows { + return try String.fetchAll( + tx.database, + sql: sql, + arguments: [phoneNumber.stringValue], + ) + } } func sortedFullGroupMembers(in groupThreadId: String, tx: DBReadTransaction) -> [TSGroupMember] { @@ -66,7 +78,13 @@ class GroupMemberStoreImpl: GroupMemberStore { WHERE \(TSGroupMember.columnName(.groupThreadId)) = ? ORDER BY \(TSGroupMember.columnName(.lastInteractionTimestamp)) DESC """ - return tx.database.strictRead { try TSGroupMember.fetchAll($0, sql: sql, arguments: [groupThreadId]) } + return failIfThrows { + try TSGroupMember.fetchAll( + tx.database, + sql: sql, + arguments: [groupThreadId], + ) + } } } diff --git a/SignalServiceKit/Groups/TSGroupMember.swift b/SignalServiceKit/Groups/TSGroupMember.swift index ed45a831ce..9bba14f0ac 100644 --- a/SignalServiceKit/Groups/TSGroupMember.swift +++ b/SignalServiceKit/Groups/TSGroupMember.swift @@ -121,19 +121,12 @@ public final class TSGroupMember: NSObject, SDSCodableModel, Decodable { LIMIT 1 """ - do { + return failIfThrows { return try fetchOne( transaction.database, sql: sql, arguments: [address.serviceIdUppercaseString, address.phoneNumber, groupThreadId] ) - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFailDebug("Failed to fetch group member \(error)") - return nil } } @@ -151,8 +144,8 @@ public final class TSGroupMember: NSObject, SDSCodableModel, Decodable { public class func enumerateGroupMembers( for address: SignalServiceAddress, transaction: DBReadTransaction, - block: @escaping (TSGroupMember, UnsafeMutablePointer - ) -> Void) { + block: @escaping (TSGroupMember, UnsafeMutablePointer) -> Void, + ) { let sql = """ SELECT * FROM \(databaseTableName) WHERE (\(columnName(.serviceId)) = ? OR \(columnName(.serviceId)) IS NULL) @@ -160,7 +153,7 @@ public final class TSGroupMember: NSObject, SDSCodableModel, Decodable { AND NOT (\(columnName(.serviceId)) IS NULL AND \(columnName(.phoneNumber)) IS NULL) """ - do { + failIfThrows { let cursor = try fetchCursor( transaction.database, sql: sql, @@ -171,12 +164,6 @@ public final class TSGroupMember: NSObject, SDSCodableModel, Decodable { block(member, &stop) if stop.boolValue { break } } - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFail("Failed to enumerate group membership.") } } } @@ -197,9 +184,9 @@ public extension TSGroupThread { ORDER BY \(TSGroupMember.columnName(.lastInteractionTimestamp)) DESC """ - var groupThreads = [TSGroupThread]() + return failIfThrows { + var groupThreads = [TSGroupThread]() - do { let cursor = try String.fetchCursor( transaction.database, sql: sql, @@ -217,15 +204,9 @@ public extension TSGroupThread { groupThreads.append(groupThread) } - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFail("Failed to find group thread") - } - return groupThreads + return groupThreads + } } class func enumerateGroupThreads( @@ -261,7 +242,7 @@ public extension TSGroupThread { class func groupThreadIds( with address: SignalServiceAddress, - transaction: DBReadTransaction + transaction tx: DBReadTransaction ) -> [String] { let sql = """ SELECT \(TSGroupMember.columnName(.groupThreadId)) @@ -271,10 +252,9 @@ public extension TSGroupThread { AND NOT (\(TSGroupMember.columnName(.serviceId)) IS NULL AND \(TSGroupMember.columnName(.phoneNumber)) IS NULL) ORDER BY \(TSGroupMember.columnName(.lastInteractionTimestamp)) DESC """ - - return transaction.database.strictRead { database in + return failIfThrows { try String.fetchAll( - database, + tx.database, sql: sql, arguments: [address.serviceIdUppercaseString, address.phoneNumber] ) diff --git a/SignalServiceKit/Jobs/DonationReceiptCredentialRedemptionJobQueue.swift b/SignalServiceKit/Jobs/DonationReceiptCredentialRedemptionJobQueue.swift index 1a8b48226b..fd1366c77a 100644 --- a/SignalServiceKit/Jobs/DonationReceiptCredentialRedemptionJobQueue.swift +++ b/SignalServiceKit/Jobs/DonationReceiptCredentialRedemptionJobQueue.swift @@ -210,14 +210,8 @@ struct DonationReceiptCredentialRedemptionJobFinder { subscriberID ] - do { + return failIfThrows { return try Bool.fetchOne(tx.database, sql: sql, arguments: arguments) ?? false - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFail("Unable to find job: \(error.grdbErrorForLogging)") } } } diff --git a/SignalServiceKit/Jobs/SendGiftBadgeJobQueue.swift b/SignalServiceKit/Jobs/SendGiftBadgeJobQueue.swift index cb2082a96f..ae312a71d2 100644 --- a/SignalServiceKit/Jobs/SendGiftBadgeJobQueue.swift +++ b/SignalServiceKit/Jobs/SendGiftBadgeJobQueue.swift @@ -113,14 +113,8 @@ private func jobExists(threadId: String, transaction: DBReadTransaction) -> Bool SendGiftBadgeJobRecord.Status.permanentlyFailed.rawValue, SendGiftBadgeJobRecord.Status.obsolete.rawValue ] - do { + return failIfThrows { return try Bool.fetchOne(transaction.database, sql: sql, arguments: arguments) ?? false - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFail("Unable to find job") } } diff --git a/SignalServiceKit/Messages/Edit/EditMessageStore.swift b/SignalServiceKit/Messages/Edit/EditMessageStore.swift index 9dc325943a..1241362bb3 100644 --- a/SignalServiceKit/Messages/Edit/EditMessageStore.swift +++ b/SignalServiceKit/Messages/Edit/EditMessageStore.swift @@ -111,18 +111,12 @@ public struct EditMessageStore { let arguments: StatementArguments = [message.grdbId] - do { + return failIfThrows { return try Int.fetchOne( tx.database, sql: sql, arguments: arguments ) ?? 0 - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFail("Missing instance.") } } diff --git a/SignalServiceKit/Messages/GroupMessageProcessor.swift b/SignalServiceKit/Messages/GroupMessageProcessor.swift index 0ae645a94c..b3c6b81903 100644 --- a/SignalServiceKit/Messages/GroupMessageProcessor.swift +++ b/SignalServiceKit/Messages/GroupMessageProcessor.swift @@ -320,31 +320,18 @@ internal class SpecificGroupMessageProcessor { } private func nextJob(tx: DBReadTransaction) -> GroupMessageProcessorJob? { - do { - return try self.finder.nextJob(forGroupId: self.groupId, tx: tx) - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary(error: error) - owsFailDebug("Couldn't process group messages: \(error)") - return nil - } + return finder.nextJob(forGroupId: self.groupId, tx: tx) } private func newestJobId() -> Int64? { - let databaseStorage = SSKEnvironment.shared.databaseStorageRef - return databaseStorage.read { tx in - do { - return try self.finder.newestJobId(tx: tx) - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary(error: error) - return nil - } + let db = DependenciesBridge.shared.db + return db.read { tx in + finder.newestJobId(tx: tx) } } private func didCompleteJob(_ job: GroupMessageProcessorJob, tx: DBWriteTransaction) { - failIfThrows { - try self.finder.removeJob(withRowId: job.id, tx: tx) - } + finder.removeJob(withRowId: job.id, tx: tx) } private func performLocalProcessingSync( @@ -570,16 +557,10 @@ public class GroupMessageProcessorManager { } // Obtain the list of groups that currently need processing. - let databaseStorage = SSKEnvironment.shared.databaseStorageRef - let groupIds = Set(databaseStorage.read { tx -> [Data] in - do { - return try self.finder.allEnqueuedGroupIds(tx: tx) - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary(error: error) - owsFailDebug("Can't process group messages: \(error)") - return [] - } - }) + let db = DependenciesBridge.shared.db + let groupIds = db.read { tx -> Set in + return Set(finder.allEnqueuedGroupIds(tx: tx)) + } if !groupIds.isEmpty { Logger.info("(Re-)starting \(groupIds.count) group message processor(s) with pending messages.") @@ -704,14 +685,10 @@ public class GroupMessageProcessorManager { // 1. We don't have any other messages queued for this thread // 2. The message can be processed without updates - let existsJob: Bool - do { - existsJob = try GroupMessageProcessorJobStore().existsJob(forGroupId: groupContextInfo.groupId.serialize(), tx: tx) - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary(error: error) - owsFailDebug("Couldn't check for existing group jobs: \(error)") - existsJob = true // fall back to assuming it's true - } + let existsJob: Bool = GroupMessageProcessorJobStore().existsJob( + forGroupId: groupContextInfo.groupId.serialize(), + tx: tx, + ) if existsJob { Logger.info("Cannot immediately process GV2 message because there are messages queued.") return false diff --git a/SignalServiceKit/Messages/GroupMessageProcessorJobStore.swift b/SignalServiceKit/Messages/GroupMessageProcessorJobStore.swift index b160eac466..5b8bfdd0ba 100644 --- a/SignalServiceKit/Messages/GroupMessageProcessorJobStore.swift +++ b/SignalServiceKit/Messages/GroupMessageProcessorJobStore.swift @@ -7,20 +7,18 @@ import Foundation import GRDB struct GroupMessageProcessorJobStore { - func allEnqueuedGroupIds(tx: DBReadTransaction) throws -> [Data] { + func allEnqueuedGroupIds(tx: DBReadTransaction) -> [Data] { let sql = """ SELECT DISTINCT \(GroupMessageProcessorJob.CodingKeys.groupId.rawValue) FROM \(GroupMessageProcessorJob.databaseTableName) """ - do { + return failIfThrows { return try (Data?).fetchAll(tx.database, sql: sql).compacted() - } catch { - throw error.grdbErrorForLogging } } - func nextJob(forGroupId groupId: Data, tx: DBReadTransaction) throws -> GroupMessageProcessorJob? { - do { + func nextJob(forGroupId groupId: Data, tx: DBReadTransaction) -> GroupMessageProcessorJob? { + return failIfThrows { let sql = """ SELECT * FROM \(GroupMessageProcessorJob.databaseTableName) @@ -28,41 +26,33 @@ struct GroupMessageProcessorJobStore { ORDER BY \(GroupMessageProcessorJob.CodingKeys.id.rawValue) """ return try GroupMessageProcessorJob.fetchOne(tx.database, sql: sql, arguments: [groupId]) - } catch { - throw error.grdbErrorForLogging } } - func newestJobId(tx: DBReadTransaction) throws -> Int64? { - do { + func newestJobId(tx: DBReadTransaction) -> Int64? { + return failIfThrows { let sql = """ SELECT \(GroupMessageProcessorJob.CodingKeys.id.rawValue) FROM \(GroupMessageProcessorJob.databaseTableName) ORDER BY \(GroupMessageProcessorJob.CodingKeys.id.rawValue) DESC """ return try Int64.fetchOne(tx.database, sql: sql) - } catch { - throw error.grdbErrorForLogging } } - public func existsJob(forGroupId groupId: Data, tx: DBReadTransaction) throws -> Bool { + public func existsJob(forGroupId groupId: Data, tx: DBReadTransaction) -> Bool { let sql = """ SELECT 1 FROM \(GroupMessageProcessorJob.databaseTableName) WHERE \(GroupMessageProcessorJob.CodingKeys.groupId.rawValue) = ? """ - do { + return failIfThrows { return try Bool.fetchOne(tx.database, sql: sql, arguments: [groupId]) ?? false - } catch { - throw error.grdbErrorForLogging } } - public func removeJob(withRowId rowId: Int64, tx: DBWriteTransaction) throws { - do { + public func removeJob(withRowId rowId: Int64, tx: DBWriteTransaction) { + failIfThrows { _ = try GroupMessageProcessorJob.deleteOne(tx.database, key: rowId) - } catch { - throw error.grdbErrorForLogging } } } diff --git a/SignalServiceKit/Messages/Interactions/InteractionDeleteManager.swift b/SignalServiceKit/Messages/Interactions/InteractionDeleteManager.swift index 3fd0d7333e..da4c81ceef 100644 --- a/SignalServiceKit/Messages/Interactions/InteractionDeleteManager.swift +++ b/SignalServiceKit/Messages/Interactions/InteractionDeleteManager.swift @@ -248,7 +248,9 @@ final class InteractionDeleteManagerImpl: InteractionDeleteManager { tx: tx ) - tx.database.executeAndCacheStatementHandlingErrors( + // Worth using a cached statement here, since we may be deleting a large + // number of interactions at once here. + tx.database.executeWithCachedStatement( sql: "DELETE FROM model_TSInteraction WHERE uniqueId = ?", arguments: [interaction.uniqueId] ) @@ -311,11 +313,7 @@ final class InteractionDeleteManagerImpl: InteractionDeleteManager { interactionReadCache.didRemove(interaction: interaction, transaction: tx) if let message = interaction as? TSMessage { - do { - try FullTextSearchIndexer.delete(message, tx: tx) - } catch { - owsFailBeta("Error: \(error)") - } + FullTextSearchIndexer.delete(message, tx: tx) message.removeAllAttachments(tx: tx) message.removeAllReactions(transaction: tx) diff --git a/SignalServiceKit/Messages/Interactions/MentionFinder.swift b/SignalServiceKit/Messages/Interactions/MentionFinder.swift index 9cf177e79e..3847e306e0 100644 --- a/SignalServiceKit/Messages/Interactions/MentionFinder.swift +++ b/SignalServiceKit/Messages/Interactions/MentionFinder.swift @@ -83,12 +83,14 @@ public class MentionFinder { return messages } - public class func deleteAllMentions(for message: TSMessage, transaction: DBWriteTransaction) { + public class func deleteAllMentions(for message: TSMessage, transaction tx: DBWriteTransaction) { let sql = """ DELETE FROM \(TSMention.databaseTableName) WHERE \(TSMention.columnName(.uniqueMessageId)) = ? """ - transaction.database.executeHandlingErrors(sql: sql, arguments: [message.uniqueId]) + failIfThrows { + try tx.database.execute(sql: sql, arguments: [message.uniqueId]) + } } public class func mentionedAcis(for message: TSMessage, tx: DBReadTransaction) -> [Aci] { diff --git a/SignalServiceKit/Messages/Interactions/TSInteraction+SDS.swift b/SignalServiceKit/Messages/Interactions/TSInteraction+SDS.swift index 47cdd5126b..a8919fb55f 100644 --- a/SignalServiceKit/Messages/Interactions/TSInteraction+SDS.swift +++ b/SignalServiceKit/Messages/Interactions/TSInteraction+SDS.swift @@ -5387,17 +5387,14 @@ public extension TSInteraction { @objc public class TSInteractionCursor: NSObject, SDSCursor { private let transaction: DBReadTransaction - private let cursor: RecordCursor? + private let cursor: RecordCursor - init(transaction: DBReadTransaction, cursor: RecordCursor?) { + init(transaction: DBReadTransaction, cursor: RecordCursor) { self.transaction = transaction self.cursor = cursor } public func next() throws -> TSInteraction? { - guard let cursor = cursor else { - return nil - } guard let record = try cursor.next() else { return nil } @@ -5425,16 +5422,9 @@ public extension TSInteraction { @nonobjc class func grdbFetchCursor(transaction: DBReadTransaction) -> TSInteractionCursor { let database = transaction.database - do { + return failIfThrows { let cursor = try InteractionRecord.fetchCursor(database) return TSInteractionCursor(transaction: transaction, cursor: cursor) - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFailDebug("Read failed: \(error)") - return TSInteractionCursor(transaction: transaction, cursor: nil) } } @@ -5525,17 +5515,10 @@ public extension TSInteraction { class func grdbFetchCursor(sql: String, arguments: StatementArguments = StatementArguments(), transaction: DBReadTransaction) -> TSInteractionCursor { - do { + return failIfThrows { let sqlRequest = SQLRequest(sql: sql, arguments: arguments, cached: true) let cursor = try InteractionRecord.fetchCursor(transaction.database, sqlRequest) return TSInteractionCursor(transaction: transaction, cursor: cursor) - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFailDebug("Read failed: \(error)") - return TSInteractionCursor(transaction: transaction, cursor: nil) } } diff --git a/SignalServiceKit/Messages/Interactions/TSMessage.swift b/SignalServiceKit/Messages/Interactions/TSMessage.swift index d4239f5e5c..834b076bba 100644 --- a/SignalServiceKit/Messages/Interactions/TSMessage.swift +++ b/SignalServiceKit/Messages/Interactions/TSMessage.swift @@ -742,20 +742,12 @@ public extension TSMessage { @objc internal func _anyDidInsert(tx: DBWriteTransaction) { - do { - try FullTextSearchIndexer.insert(self, tx: tx) - } catch { - owsFail("Error: \(error)") - } + FullTextSearchIndexer.insert(self, tx: tx) } @objc internal func _anyDidUpdate(tx: DBWriteTransaction) { - do { - try FullTextSearchIndexer.update(self, tx: tx) - } catch { - owsFail("Error: \(error)") - } + FullTextSearchIndexer.update(self, tx: tx) } } diff --git a/SignalServiceKit/Messages/OWSReceiptManager.swift b/SignalServiceKit/Messages/OWSReceiptManager.swift index 3416bc1318..73286e095d 100644 --- a/SignalServiceKit/Messages/OWSReceiptManager.swift +++ b/SignalServiceKit/Messages/OWSReceiptManager.swift @@ -845,7 +845,7 @@ extension OWSReceiptManager { static func markAllCallInteractionsAsReadLocally( beforeSQLId sqlId: NSNumber?, /* Clears everything if nil */ thread: TSThread, - transaction: DBWriteTransaction + transaction tx: DBWriteTransaction ) { var sql = """ UPDATE \(InteractionRecord.databaseTableName) @@ -860,7 +860,12 @@ extension OWSReceiptManager { sql += " AND \(interactionColumn: .id) <= ?" arguments += [sqlId] } - transaction.database.executeHandlingErrors(sql: sql, arguments: arguments) + failIfThrows { + try tx.database.execute( + sql: sql, + arguments: arguments, + ) + } } } diff --git a/SignalServiceKit/Messages/Payments/ArchivedPaymentStore.swift b/SignalServiceKit/Messages/Payments/ArchivedPaymentStore.swift index be3afb2395..9aec6a99f9 100644 --- a/SignalServiceKit/Messages/Payments/ArchivedPaymentStore.swift +++ b/SignalServiceKit/Messages/Payments/ArchivedPaymentStore.swift @@ -6,22 +6,12 @@ import Foundation import GRDB -public protocol ArchivedPaymentStore { - func insert(_ archivedPayment: ArchivedPayment, tx: DBWriteTransaction) throws - func fetch( - for archivedPaymentMessage: OWSArchivedPaymentMessage, - interactionUniqueId: String, - tx: DBReadTransaction - ) throws -> ArchivedPayment? - func enumerateAll(tx: DBReadTransaction, block: @escaping (ArchivedPayment, _ stop: inout Bool) -> Void) -} - -public struct ArchivedPaymentStoreImpl: ArchivedPaymentStore { +public struct ArchivedPaymentStore { public func enumerateAll( tx: DBReadTransaction, block: @escaping (ArchivedPayment, _ stop: inout Bool) -> Void ) { - do { + failIfThrows { let cursor = try ArchivedPayment.fetchCursor(tx.database) var stop = false while let archivedPayment = try cursor.next() { @@ -30,12 +20,6 @@ public struct ArchivedPaymentStoreImpl: ArchivedPaymentStore { break } } - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFail("Missing instance.") } } @@ -43,21 +27,17 @@ public struct ArchivedPaymentStoreImpl: ArchivedPaymentStore { for archivedPaymentMessage: OWSArchivedPaymentMessage, interactionUniqueId: String, tx: DBReadTransaction - ) throws -> ArchivedPayment? { - do { + ) -> ArchivedPayment? { + failIfThrows { return try ArchivedPayment .filter(Column(ArchivedPayment.CodingKeys.interactionUniqueId) == interactionUniqueId) .fetchOne(tx.database) - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - throw error } } - public func insert(_ archivedPayment: ArchivedPayment, tx: DBWriteTransaction) throws { - try archivedPayment.insert(tx.database) + public func insert(_ archivedPayment: ArchivedPayment, tx: DBWriteTransaction) { + failIfThrows { + try archivedPayment.insert(tx.database) + } } } diff --git a/SignalServiceKit/Messages/Reactions/ReactionFinder.swift b/SignalServiceKit/Messages/Reactions/ReactionFinder.swift index aafb9a3c46..bda2386d06 100644 --- a/SignalServiceKit/Messages/Reactions/ReactionFinder.swift +++ b/SignalServiceKit/Messages/Reactions/ReactionFinder.swift @@ -124,11 +124,16 @@ public class ReactionFinder { } /// Delete all reaction records associated with this message - public func deleteAllReactions(transaction: DBWriteTransaction) { + public func deleteAllReactions(transaction tx: DBWriteTransaction) { let sql = """ DELETE FROM \(OWSReaction.databaseTableName) WHERE \(OWSReaction.columnName(.uniqueMessageId)) = ? """ - transaction.database.executeHandlingErrors(sql: sql, arguments: [uniqueMessageId]) + failIfThrows { + try tx.database.execute( + sql: sql, + arguments: [uniqueMessageId], + ) + } } } diff --git a/SignalServiceKit/Messages/Stickers/InstalledSticker+SDS.swift b/SignalServiceKit/Messages/Stickers/InstalledSticker+SDS.swift index d27aaea3cc..7c1759f1c1 100644 --- a/SignalServiceKit/Messages/Stickers/InstalledSticker+SDS.swift +++ b/SignalServiceKit/Messages/Stickers/InstalledSticker+SDS.swift @@ -299,17 +299,14 @@ public extension InstalledSticker { @objc public class InstalledStickerCursor: NSObject, SDSCursor { private let transaction: DBReadTransaction - private let cursor: RecordCursor? + private let cursor: RecordCursor - init(transaction: DBReadTransaction, cursor: RecordCursor?) { + init(transaction: DBReadTransaction, cursor: RecordCursor) { self.transaction = transaction self.cursor = cursor } public func next() throws -> InstalledSticker? { - guard let cursor = cursor else { - return nil - } guard let record = try cursor.next() else { return nil } @@ -337,16 +334,9 @@ public extension InstalledSticker { @nonobjc class func grdbFetchCursor(transaction: DBReadTransaction) -> InstalledStickerCursor { let database = transaction.database - do { + return failIfThrows { let cursor = try InstalledStickerRecord.fetchCursor(database) return InstalledStickerCursor(transaction: transaction, cursor: cursor) - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFailDebug("Read failed: \(error)") - return InstalledStickerCursor(transaction: transaction, cursor: nil) } } @@ -437,17 +427,10 @@ public extension InstalledSticker { class func grdbFetchCursor(sql: String, arguments: StatementArguments = StatementArguments(), transaction: DBReadTransaction) -> InstalledStickerCursor { - do { + return failIfThrows { let sqlRequest = SQLRequest(sql: sql, arguments: arguments, cached: true) let cursor = try InstalledStickerRecord.fetchCursor(transaction.database, sqlRequest) return InstalledStickerCursor(transaction: transaction, cursor: cursor) - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFailDebug("Read failed: \(error)") - return InstalledStickerCursor(transaction: transaction, cursor: nil) } } diff --git a/SignalServiceKit/Messages/Stickers/StickerPack+SDS.swift b/SignalServiceKit/Messages/Stickers/StickerPack+SDS.swift index ce927d9780..ee22ec9df3 100644 --- a/SignalServiceKit/Messages/Stickers/StickerPack+SDS.swift +++ b/SignalServiceKit/Messages/Stickers/StickerPack+SDS.swift @@ -338,17 +338,14 @@ public extension StickerPack { @objc public class StickerPackCursor: NSObject, SDSCursor { private let transaction: DBReadTransaction - private let cursor: RecordCursor? + private let cursor: RecordCursor - init(transaction: DBReadTransaction, cursor: RecordCursor?) { + init(transaction: DBReadTransaction, cursor: RecordCursor) { self.transaction = transaction self.cursor = cursor } public func next() throws -> StickerPack? { - guard let cursor = cursor else { - return nil - } guard let record = try cursor.next() else { return nil } @@ -374,16 +371,9 @@ public extension StickerPack { @nonobjc class func grdbFetchCursor(transaction: DBReadTransaction) -> StickerPackCursor { let database = transaction.database - do { + return failIfThrows { let cursor = try StickerPackRecord.fetchCursor(database) return StickerPackCursor(transaction: transaction, cursor: cursor) - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFailDebug("Read failed: \(error)") - return StickerPackCursor(transaction: transaction, cursor: nil) } } @@ -460,17 +450,10 @@ public extension StickerPack { class func grdbFetchCursor(sql: String, arguments: StatementArguments = StatementArguments(), transaction: DBReadTransaction) -> StickerPackCursor { - do { + return failIfThrows { let sqlRequest = SQLRequest(sql: sql, arguments: arguments, cached: true) let cursor = try StickerPackRecord.fetchCursor(transaction.database, sqlRequest) return StickerPackCursor(transaction: transaction, cursor: cursor) - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFailDebug("Read failed: \(error)") - return StickerPackCursor(transaction: transaction, cursor: nil) } } diff --git a/SignalServiceKit/Messages/Stories/StoryFinder.swift b/SignalServiceKit/Messages/Stories/StoryFinder.swift index 3c20e45ddb..a992d79492 100644 --- a/SignalServiceKit/Messages/Stories/StoryFinder.swift +++ b/SignalServiceKit/Messages/Stories/StoryFinder.swift @@ -342,14 +342,8 @@ public class StoryFinder { ) ) """ - do { + return failIfThrows { return try Bool.fetchOne(transaction.database, sql: sql) ?? false - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFail("Fetch failed") } } } diff --git a/SignalServiceKit/Payments/TSPaymentModel+SDS.swift b/SignalServiceKit/Payments/TSPaymentModel+SDS.swift index 8d34788c51..281b01119f 100644 --- a/SignalServiceKit/Payments/TSPaymentModel+SDS.swift +++ b/SignalServiceKit/Payments/TSPaymentModel+SDS.swift @@ -415,17 +415,14 @@ public extension TSPaymentModel { @objc public class TSPaymentModelCursor: NSObject, SDSCursor { private let transaction: DBReadTransaction - private let cursor: RecordCursor? + private let cursor: RecordCursor - init(transaction: DBReadTransaction, cursor: RecordCursor?) { + init(transaction: DBReadTransaction, cursor: RecordCursor) { self.transaction = transaction self.cursor = cursor } public func next() throws -> TSPaymentModel? { - guard let cursor = cursor else { - return nil - } guard let record = try cursor.next() else { return nil } @@ -451,16 +448,9 @@ public extension TSPaymentModel { @nonobjc class func grdbFetchCursor(transaction: DBReadTransaction) -> TSPaymentModelCursor { let database = transaction.database - do { + return failIfThrows { let cursor = try PaymentModelRecord.fetchCursor(database) return TSPaymentModelCursor(transaction: transaction, cursor: cursor) - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFailDebug("Read failed: \(error)") - return TSPaymentModelCursor(transaction: transaction, cursor: nil) } } @@ -537,17 +527,10 @@ public extension TSPaymentModel { class func grdbFetchCursor(sql: String, arguments: StatementArguments = StatementArguments(), transaction: DBReadTransaction) -> TSPaymentModelCursor { - do { + return failIfThrows { let sqlRequest = SQLRequest(sql: sql, arguments: arguments, cached: true) let cursor = try PaymentModelRecord.fetchCursor(transaction.database, sqlRequest) return TSPaymentModelCursor(transaction: transaction, cursor: cursor) - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFailDebug("Read failed: \(error)") - return TSPaymentModelCursor(transaction: transaction, cursor: nil) } } diff --git a/SignalServiceKit/Payments/TSPaymentsActivationRequestModel.swift b/SignalServiceKit/Payments/TSPaymentsActivationRequestModel.swift index 34547f0c32..a1cbfe121c 100644 --- a/SignalServiceKit/Payments/TSPaymentsActivationRequestModel.swift +++ b/SignalServiceKit/Payments/TSPaymentsActivationRequestModel.swift @@ -65,19 +65,13 @@ public struct TSPaymentsActivationRequestModel: Codable, FetchableRecord, Persis let arguments: StatementArguments = [ threadUniqueId ] - do { + failIfThrows { let exists = try Bool.fetchOne(transaction.database, sql: sql, arguments: arguments) ?? false if exists { return } let model = Self.init(threadUniqueId: threadUniqueId, senderAci: senderAci) try model.insert(transaction.database) - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFailDebug("Unable to create payment model") } } @@ -86,18 +80,11 @@ public struct TSPaymentsActivationRequestModel: Codable, FetchableRecord, Persis ) -> [TSThread] { // This could be a SQL join, but the table is really small // so its fine to do an in-memory join. - do { + failIfThrows { return try TSPaymentsActivationRequestModel.fetchAll(transaction.database) .compactMap { model in return TSThread.anyFetch(uniqueId: model.threadUniqueId, transaction: transaction) } - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFailDebug("Unable to find payment models") - return [] } } } diff --git a/SignalServiceKit/Search/FullTextSearchIndexer.swift b/SignalServiceKit/Search/FullTextSearchIndexer.swift index ea0169129b..d3dd874b8b 100644 --- a/SignalServiceKit/Search/FullTextSearchIndexer.swift +++ b/SignalServiceKit/Search/FullTextSearchIndexer.swift @@ -3,7 +3,6 @@ // SPDX-License-Identifier: AGPL-3.0-only // -import Foundation import GRDB public enum FullTextSearchIndexer { @@ -160,11 +159,14 @@ extension FullTextSearchIndexer { return normalizeText(bodyText) } - public static func insert(_ message: TSMessage, tx: DBWriteTransaction) throws { + public static func insert( + _ message: TSMessage, + tx: DBWriteTransaction, + ) { guard let ftsContent = indexableContent(for: message, tx: tx) else { return } - try executeUpdate( + executeUpdate( sql: """ INSERT INTO \(contentTableName) (\(collectionColumn), \(uniqueIdColumn), \(ftsContentColumn)) @@ -176,13 +178,19 @@ extension FullTextSearchIndexer { ) } - public static func update(_ message: TSMessage, tx: DBWriteTransaction) throws { - try delete(message, tx: tx) - try insert(message, tx: tx) + public static func update( + _ message: TSMessage, + tx: DBWriteTransaction, + ) { + delete(message, tx: tx) + insert(message, tx: tx) } - public static func delete(_ message: TSMessage, tx: DBWriteTransaction) throws { - try executeUpdate( + public static func delete( + _ message: TSMessage, + tx: DBWriteTransaction, + ) { + executeUpdate( sql: """ DELETE FROM \(contentTableName) WHERE \(uniqueIdColumn) == ? @@ -196,19 +204,23 @@ extension FullTextSearchIndexer { private static func executeUpdate( sql: String, arguments: StatementArguments, - tx: DBWriteTransaction - ) throws { - let database = tx.database + tx: DBWriteTransaction, + ) { do { - let statement = try database.cachedStatement(sql: sql) - try statement.setArguments(arguments) - try statement.execute() - } catch { - DatabaseCorruptionState.flagDatabaseCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error + // Worth using a cached statement here, as we may be indexing many + // things at once. + try tx.database.executeWithCachedStatementThrows( + sql: sql, + arguments: arguments, ) - throw error + } catch { + // We intentionally don't use failIfThrows here because we know the + // FTS index relatively frequently reports corruption errors; for + // these specifically swallow them rather than flagging the entire + // database as corrupted. + // + // TODO: Implement "rebuild the FTS index" in response to it becoming corrupted. + owsFailDebug("Failed to perform FTS index operation! \(error.grdbErrorForLogging)") } } diff --git a/SignalServiceKit/Storage/Database/Database+Execute.swift b/SignalServiceKit/Storage/Database/Database+Execute.swift index 69fdc6ff0c..36bbfad1e8 100644 --- a/SignalServiceKit/Storage/Database/Database+Execute.swift +++ b/SignalServiceKit/Storage/Database/Database+Execute.swift @@ -6,46 +6,36 @@ public import GRDB extension Database { - /// Execute some SQL. - public func executeHandlingErrors(sql: String, arguments: StatementArguments = .init()) { - do { - let statement = try makeStatement(sql: sql) - try statement.setArguments(arguments) - try statement.execute() - } catch { - handleFatalDatabaseError(error) + /// Execute some SQL using/creating a cached statement. + /// + /// Caching statements has significant performance benefits for queries that + /// are performed repeatedly. + /// + /// - Important Crashes on database errors. + /// - SeeAlso ``executeWithCachedStatementThrows(sql:arguments:)`` + public func executeWithCachedStatement( + sql: String, + arguments: StatementArguments, + ) { + failIfThrows { + try executeWithCachedStatementThrows( + sql: sql, + arguments: arguments, + ) } } - /// Execute some SQL and cache the statement. - /// - /// Caching the statement has significant performance benefits over ``execute`` for queries - /// that are performed repeatedly. - public func executeAndCacheStatementHandlingErrors(sql: String, arguments: StatementArguments = .init()) { + /// Like ``executeWithCachedStatement(sql:arguments:)``, but throws instead + /// of crashes on database errors. + public func executeWithCachedStatementThrows( + sql: String, + arguments: StatementArguments, + ) throws(GRDB.DatabaseError) { do { let statement = try cachedStatement(sql: sql) - try statement.setArguments(arguments) - try statement.execute() + try statement.execute(arguments: arguments) } catch { - handleFatalDatabaseError(error) - } - } - - public func strictRead(_ criticalSection: (_ database: GRDB.Database) throws -> T) -> T { - do { - return try criticalSection(self) - } catch { - handleFatalDatabaseError(error) + throw error.forceCastToDatabaseError() } } } - -// MARK: - - -private func handleFatalDatabaseError(_ error: Error) -> Never { - DatabaseCorruptionState.flagDatabaseCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFail("Error: \(error)") -} diff --git a/SignalServiceKit/Storage/Database/DatabaseCorruptionState.swift b/SignalServiceKit/Storage/Database/DatabaseCorruptionState.swift index 070b014749..a0b0fd3cdc 100644 --- a/SignalServiceKit/Storage/Database/DatabaseCorruptionState.swift +++ b/SignalServiceKit/Storage/Database/DatabaseCorruptionState.swift @@ -6,138 +6,72 @@ import Foundation import GRDB -public final class DatabaseCorruptionState: Codable, Equatable, CustomStringConvertible { - public enum DatabaseCorruptionStatus: Int, Codable, CustomStringConvertible { +public struct DatabaseCorruptionState: Equatable { + public enum DatabaseCorruptionStatus: Int, Codable { // We used to store these as booleans, so the value is important. case notCorrupted = 0 case corrupted = 1 case corruptedButAlreadyDumpedAndRestored = 2 - case readCorrupted = 3 + // This case was deprecated, but is left here such that we don't + // inadvertently reuse this rawValue and resurrect it. + // case readCorrupted = 3 public var isCorrupted: Bool { switch self { - case .notCorrupted, .readCorrupted: return false + case .notCorrupted: return false case .corrupted, .corruptedButAlreadyDumpedAndRestored: return true } } - - public var description: String { - switch self { - case .notCorrupted: - return "not corrupted" - case .readCorrupted: - return "read corrupted" - case .corrupted: - return "corrupted" - case .corruptedButAlreadyDumpedAndRestored: - return "corrupted (but already dumped and restored)" - } - } } public let status: DatabaseCorruptionStatus - public let count: UInt - init(status: DatabaseCorruptionStatus, count: UInt) { + init(status: DatabaseCorruptionStatus) { self.status = status - self.count = count - } - - public static func == (lhs: DatabaseCorruptionState, rhs: DatabaseCorruptionState) -> Bool { - (lhs.status == rhs.status) && (lhs.count == rhs.count) - } - - public var description: String { - "Database is \(status). Corruption count: \(count)" } // MARK: - Reading and writing from `UserDefaults` // The value of this key doesn't match the name because that's what we used to store. static var databaseCorruptionStatusKey: String { "hasGrdbDatabaseCorruption" } - static var databaseCorruptionCountKey: String { "databaseCorruptionCount" } - public convenience init(userDefaults: UserDefaults) { + public init(userDefaults: UserDefaults) { let rawStatus = userDefaults.integer(forKey: Self.databaseCorruptionStatusKey) - let rawCount = userDefaults.integer(forKey: Self.databaseCorruptionCountKey) - let status = DatabaseCorruptionStatus(rawValue: rawStatus) ?? .notCorrupted - let count: UInt = status.isCorrupted ? max(UInt(rawCount), 1) : UInt(rawCount) - - self.init(status: status, count: count) + self.init(status: status) } private func save(to userDefaults: UserDefaults) { userDefaults.set(status.rawValue, forKey: Self.databaseCorruptionStatusKey) - userDefaults.set(count, forKey: Self.databaseCorruptionCountKey) - } - - /// If the error is a `SQLITE_CORRUPT` error, set the "has database corruption" flag, log, and crash. - /// We do this so we can attempt to perform diagnostics/recovery on relaunch. - public static func flagDatabaseCorruptionIfNecessary( - userDefaults: UserDefaults, - error: Error, - file: String = #fileID, - function: String = #function, - line: Int = #line - ) { - if let error = error as? DatabaseError, error.resultCode == .SQLITE_CORRUPT { - flagDatabaseAsCorrupted(userDefaults: userDefaults) - owsFail("Crashing due to database corruption. Extended result code: \(error.extendedResultCode)", file: file, function: function, line: line) - } - } - - /// If the error is a `SQLITE_CORRUPT` error, set the "has database read corruption" flag, log, but don't crash. - /// We do this so we can attempt to perform diagnostics/recovery if this read error is coupled with a crash.. - public static func flagDatabaseReadCorruptionIfNecessary(userDefaults: UserDefaults = CurrentAppContext().appUserDefaults(), error: Error) { - if let error = error as? DatabaseError, error.resultCode == .SQLITE_CORRUPT { - flagDatabaseAsReadCorrupted(userDefaults: userDefaults) - } } public static func flagDatabaseAsCorrupted(userDefaults: UserDefaults) { - let oldState = DatabaseCorruptionState(userDefaults: userDefaults) - switch oldState.status { - case .notCorrupted, .readCorrupted: - Self(status: .corrupted, count: oldState.count + 1).save(to: userDefaults) - case .corrupted, .corruptedButAlreadyDumpedAndRestored: - return - } - } - - public static func flagDatabaseAsReadCorrupted(userDefaults: UserDefaults) { let oldState = DatabaseCorruptionState(userDefaults: userDefaults) switch oldState.status { case .notCorrupted: - Self(status: .readCorrupted, count: oldState.count).save(to: userDefaults) - case .corrupted, .readCorrupted, .corruptedButAlreadyDumpedAndRestored: - return + Self(status: .corrupted).save(to: userDefaults) + case .corrupted, .corruptedButAlreadyDumpedAndRestored: + break } } public static func flagCorruptedDatabaseAsDumpedAndRestored(userDefaults: UserDefaults) { let oldState = DatabaseCorruptionState(userDefaults: userDefaults) switch oldState.status { - case .corrupted, .readCorrupted: - DatabaseCorruptionState(status: .corruptedButAlreadyDumpedAndRestored, count: oldState.count).save(to: userDefaults) + case .corrupted: + DatabaseCorruptionState(status: .corruptedButAlreadyDumpedAndRestored).save(to: userDefaults) case .notCorrupted, .corruptedButAlreadyDumpedAndRestored: owsFailDebug("Flagging database as partially recovered, but it was not in the right state previously") } } - public static func flagDatabaseAsRecoveredFromCorruption(userDefaults: UserDefaults) { + public static func flagDatabaseAsNotCorrupted(userDefaults: UserDefaults) { let oldState = DatabaseCorruptionState(userDefaults: userDefaults) switch oldState.status { case .notCorrupted: - owsFailDebug("Flagging database as recovered from corruption, but it wasn't marked corrupted") - case .corrupted, .readCorrupted, .corruptedButAlreadyDumpedAndRestored: - Self(status: .notCorrupted, count: 0).save(to: userDefaults) + break + case .corrupted, .corruptedButAlreadyDumpedAndRestored: + Self(status: .notCorrupted).save(to: userDefaults) } } - - @objc(stringForLoggingWith:) - public static func objcStringForLogging(userDefaults: UserDefaults) -> String { - let state = DatabaseCorruptionState(userDefaults: userDefaults) - return String(describing: state) - } } diff --git a/SignalServiceKit/Storage/Database/DatabaseRecovery.swift b/SignalServiceKit/Storage/Database/DatabaseRecovery.swift index 33b24979a1..c4b7e5a1d2 100644 --- a/SignalServiceKit/Storage/Database/DatabaseRecovery.swift +++ b/SignalServiceKit/Storage/Database/DatabaseRecovery.swift @@ -4,7 +4,7 @@ // import Foundation -public import GRDB +import GRDB public enum DatabaseRecoveryError: Error { case ranOutOfDiskSpace @@ -33,17 +33,14 @@ public enum DatabaseRecoveryError: Error { /// restoring full-text search indexes, without the app being mostly set up. /// /// It's up to the caller to coordinate these steps, and decide which are necessary. -public enum DatabaseRecovery {} +public enum DatabaseRecovery { -// MARK: - Rebuild + private static let logger = PrefixedLogger(prefix: "[DatabaseRecovery]") -public extension DatabaseRecovery { - /// Rebuild the existing database in-place. - /// - /// This just runs `REINDEX` for now. We might be able to do other things in the future like - /// rebuilding the FTS index. - static func rebuildExistingDatabase(databaseStorage: SDSDatabaseStorage) { - Logger.info("Attempting to reindex the database...") + // MARK: - Reindex + + public static func reindex(databaseStorage: SDSDatabaseStorage) { + logger.info("Attempting to reindex the database...") do { // We use the `performWrite` method directly instead of the usual // `write` methods because we explicitly do NOT want to owsFail if @@ -55,26 +52,24 @@ public extension DatabaseRecovery { ) { tx in do { try SqliteUtil.reindex(db: tx.database) - Logger.info("Reindexed database") + logger.info("Reindexed database") return .commit } catch { - Logger.warn("Failed to reindex database") + logger.warn("Failed to reindex database") return .rollback } } } catch { - Logger.warn("Failed to write to database") + logger.warn("Failed to write to database") } } -} -// MARK: - Dump and restore + // MARK: - Dump and restore -public extension DatabaseRecovery { /// Dump and restore tables. /// /// Remember: this isn't everything you need to do to recover a database! See earlier docs. - class DumpAndRestore { + public class DumpAndRestoreOperation { private let appReadiness: AppReadiness private let corruptDatabaseStorage: SDSDatabaseStorage private let keychainStorage: any KeychainStorage @@ -82,8 +77,8 @@ public extension DatabaseRecovery { private let unitCountForCheckpoint: Int64 = 1 private let unitCountForOldDatabaseMigration: Int64 = 1 private let unitCountForNewDatabaseCreation: Int64 = 1 - private let unitCountForBestEffortCopy = Int64(DumpAndRestore.tablesToCopyWithBestEffort.count) - private let unitCountForFlawlessCopy = Int64(DumpAndRestore.tablesThatMustBeCopiedFlawlessly.count) + private let unitCountForBestEffortCopy = Int64(DumpAndRestoreOperation.tablesToCopyWithBestEffort.count) + private let unitCountForFlawlessCopy = Int64(DumpAndRestoreOperation.tablesThatMustBeCopiedFlawlessly.count) private let unitCountForNewDatabasePromotion: Int64 = 3 public let progress: Progress @@ -121,7 +116,7 @@ public extension DatabaseRecovery { Self.logTablesExplicitlySkipped() - Logger.info("Attempting database dump and restore") + logger.info("Attempting database dump and restore") let oldDatabaseStorage = self.corruptDatabaseStorage @@ -182,32 +177,32 @@ public extension DatabaseRecovery { ) } - Logger.info("Dump and restore complete") + logger.info("Dump and restore complete") } // MARK: Checkpoint old database to clear its WAL/SHM files (step 1) private static func attemptToCheckpoint(oldDatabaseStorage: SDSDatabaseStorage) { - Logger.info("Attempting to checkpoint the old database...") + logger.info("Attempting to checkpoint the old database...") do { try checkpoint(databaseStorage: oldDatabaseStorage) - Logger.info("Checkpointed old database.") + logger.info("Checkpointed old database.") } catch { - Logger.warn("Failed to checkpoint old database with error: \(error). Continuing on") + logger.warn("Failed to checkpoint old database with error: \(error). Continuing on") } } // MARK: Creating new database (step 2) private static func temporaryDatabaseFileUrl() -> URL { - Logger.info("Creating temporary database file...") + logger.info("Creating temporary database file...") let result = OWSFileSystem.temporaryFileUrl() - Logger.info("Created at \(result)") + logger.info("Created at \(result)") return result } private static func deleteTemporaryDatabase(databaseFileUrl: URL) { - Logger.info("Attempting to delete temporary database files...") + logger.info("Attempting to delete temporary database files...") let urls: [URL] = [ databaseFileUrl, GRDBDatabaseStorageAdapter.walFileUrl(for: databaseFileUrl), @@ -216,9 +211,9 @@ public extension DatabaseRecovery { for url in urls { do { try OWSFileSystem.deleteFileIfExists(url: url) - Logger.info("Deleted temporary database file") + logger.info("Deleted temporary database file") } catch { - Logger.warn("Failed to delete temporary database file") + logger.warn("Failed to delete temporary database file") } } } @@ -238,7 +233,7 @@ public extension DatabaseRecovery { } private static func runMigrationsOn(databaseStorage: SDSDatabaseStorage, databaseIs mode: MigrationsMode) throws { - Logger.info("Running migrations on \(mode) database...") + logger.info("Running migrations on \(mode) database...") do { let didPerformIncrementalMigrations = try GRDBSchemaMigrator.migrateDatabase( databaseStorage: databaseStorage, @@ -250,9 +245,9 @@ public extension DatabaseRecovery { } }() ) - Logger.info("Ran migrations on \(mode) database. \(didPerformIncrementalMigrations ? "Performed" : "Did not perform") incremental migrations") + logger.info("Ran migrations on \(mode) database. \(didPerformIncrementalMigrations ? "Performed" : "Did not perform") incremental migrations") } catch { - Logger.warn("Failed to run migrations on \(mode) database. Error: \(error)") + logger.warn("Failed to run migrations on \(mode) database. Error: \(error)") throw error } } @@ -333,7 +328,7 @@ public extension DatabaseRecovery { oldDatabaseStorage: SDSDatabaseStorage, newDatabaseStorage: SDSDatabaseStorage ) throws { - Logger.info("Attempting to copy \(tableName) (best effort)...") + logger.info("Attempting to copy \(tableName) (best effort)...") let result = copyTable( tableName: tableName, from: oldDatabaseStorage, @@ -341,17 +336,17 @@ public extension DatabaseRecovery { ) switch result { case let .totalFailure(error): - Logger.warn("Completely unable to copy \(tableName)") + logger.warn("Completely unable to copy \(tableName)") if error.isSqliteFullError { throw DatabaseRecoveryError.ranOutOfDiskSpace } case let .copiedSomeButHadTrouble(error, rowsCopied): - Logger.warn("Finished copying \(tableName). Copied \(rowsCopied) row(s), but there was an error") + logger.warn("Finished copying \(tableName). Copied \(rowsCopied) row(s), but there was an error") if error.isSqliteFullError { throw DatabaseRecoveryError.ranOutOfDiskSpace } case let .wentFlawlessly(rowsCopied): - Logger.info("Finished copying \(tableName). Copied \(rowsCopied) row(s)") + logger.info("Finished copying \(tableName). Copied \(rowsCopied) row(s)") } } @@ -403,7 +398,7 @@ public extension DatabaseRecovery { oldDatabaseStorage: SDSDatabaseStorage, newDatabaseStorage: SDSDatabaseStorage ) -> TableCopyResult { - Logger.info("Attempting to copy \(tableName) (with no mistakes)...") + logger.info("Attempting to copy \(tableName) (with no mistakes)...") let result = copyTable( tableName: tableName, from: oldDatabaseStorage, @@ -411,11 +406,11 @@ public extension DatabaseRecovery { ) switch result { case .totalFailure: - Logger.warn("Completely unable to copy \(tableName)") + logger.warn("Completely unable to copy \(tableName)") case let .copiedSomeButHadTrouble(_, rowsCopied): - Logger.warn("Failed copying \(tableName) flawlessly. Copied \(rowsCopied) row(s)") + logger.warn("Failed copying \(tableName) flawlessly. Copied \(rowsCopied) row(s)") case let .wentFlawlessly(rowsCopied: rowsCopied): - Logger.info("Finished copying \(tableName). Copied \(rowsCopied) row(s)") + logger.info("Finished copying \(tableName). Copied \(rowsCopied) row(s)") } return result } @@ -432,31 +427,31 @@ public extension DatabaseRecovery { try checkpointAndClose(databaseStorage: oldDatabaseStorage, logLabel: "old") try checkpointAndClose(databaseStorage: newDatabaseStorage, logLabel: "new") - Logger.info("Replacing old database with the new one...") + logger.info("Replacing old database with the new one...") _ = try FileManager.default.replaceItemAt( oldDatabaseStorage.databaseFileUrl, withItemAt: newDatabaseStorage.databaseFileUrl ) - Logger.info("Out with the old database, in with the new!") + logger.info("Out with the old database, in with the new!") } private static func checkpointAndClose( databaseStorage: SDSDatabaseStorage, logLabel: String ) throws { - Logger.info("Checkpointing \(logLabel) database...") + logger.info("Checkpointing \(logLabel) database...") try checkpoint(databaseStorage: databaseStorage) - Logger.info("Checkpointed \(logLabel) database. Closing...") + logger.info("Checkpointed \(logLabel) database. Closing...") try databaseStorage.grdbStorage.pool.close() - Logger.info("Cleaning up WAL and SHM files...") + logger.info("Cleaning up WAL and SHM files...") OWSFileSystem.deleteFileIfExists(databaseStorage.grdbStorage.databaseWALFilePath) OWSFileSystem.deleteFileIfExists(databaseStorage.grdbStorage.databaseSHMFilePath) - Logger.info("\(logLabel.capitalized) database closed.") + logger.info("\(logLabel.capitalized) database closed.") } // MARK: Tables that are explicitly skipped @@ -490,7 +485,7 @@ public extension DatabaseRecovery { /// /// This is a little weird, but helps us be clear: we don't copy all tables. private static func logTablesExplicitlySkipped() { - Logger.info("Explicitly skipping tables: \(tablesExplicitlySkipped.joined(separator: ", "))") + logger.info("Explicitly skipping tables: \(tablesExplicitlySkipped.joined(separator: ", "))") } // MARK: Checkpointing tables @@ -532,7 +527,7 @@ public extension DatabaseRecovery { columnNames = try getColumnNames(db: fromDb, tableName: tableName) cursor = try Row.fetchCursor(fromDb, sql: "SELECT * FROM \(tableName)") } catch { - Logger.warn("Could not create cursor for table \(tableName) with error: \(error)") + logger.warn("Could not create cursor for table \(tableName) with error: \(error)") return .totalFailure(error: error) } @@ -545,7 +540,7 @@ public extension DatabaseRecovery { do { insertStatement = try toDb.makeStatement(sql: insertSql) } catch { - Logger.warn("Could not create prepared insert statement. \(error)") + logger.warn("Could not create prepared insert statement. \(error)") return .totalFailure(error: error) } @@ -563,7 +558,7 @@ public extension DatabaseRecovery { } } } catch { - Logger.warn("Error while iterating: \(error)") + logger.warn("Error while iterating: \(error)") latestError = error } @@ -575,7 +570,7 @@ public extension DatabaseRecovery { } } } catch { - Logger.warn("Error when reading: \(error)") + logger.warn("Error when reading: \(error)") return .totalFailure(error: error) } } @@ -617,13 +612,11 @@ public extension DatabaseRecovery { return "INSERT INTO \(tableName) (\(columnNamesSql)) VALUES (\(valuesSql))" } } -} -// MARK: - Manual recreation + // MARK: - Manual recreation -public extension DatabaseRecovery { /// Manually recreate various tables, such as the full-text search indexes. - class ManualRecreation { + public class RecreateFTSIndexOperation { private let databaseStorage: SDSDatabaseStorage private let unitCountForFullTextSearch: Int64 = 2 @@ -646,7 +639,7 @@ public extension DatabaseRecovery { } private func attemptToRecreateFullTextSearch() { - Logger.info("Starting to re-index full text search...") + logger.info("Starting to re-index full text search...") databaseStorage.write { tx in let searchableNameIndexer = DependenciesBridge.shared.searchableNameIndexer @@ -658,22 +651,16 @@ public extension DatabaseRecovery { guard let message = interaction as? TSMessage else { return } - do { - try FullTextSearchIndexer.insert(message, tx: tx) - } catch { - owsFailDebug("Failed to insert message into FTS: \(error)") - } + FullTextSearchIndexer.insert(message, tx: tx) } } - Logger.info("Finished re-indexing full text search") + logger.info("Finished re-indexing full text search") } } -} -// MARK: - Utilities + // MARK: - Utilities -extension DatabaseRecovery { private struct PreparedOperation { public let progress: Progress private let fn: (Progress) throws -> Void @@ -689,28 +676,29 @@ extension DatabaseRecovery { } public static func integrityCheck(databaseStorage: SDSDatabaseStorage) -> SqliteUtil.IntegrityCheckResult { - Logger.info("Running integrity check on database...") - let result = databaseStorage.write { transaction in - let db = transaction.database - return SqliteUtil.quickCheck(db: db) - } + logger.info("Running integrity check on database...") + let result = GRDBDatabaseStorageAdapter.checkIntegrity(databaseStorage: databaseStorage) switch result { - case .ok: Logger.info("Integrity check succeeded!") - case .notOk: Logger.warn("Integrity check failed") + case .ok: logger.info("Integrity check succeeded!") + case .notOk: logger.warn("Integrity check failed") } return result } } -extension Error { +// MARK: - + +private extension Error { var isSqliteFullError: Bool { guard let self = self as? DatabaseError else { return false } return self.resultCode == .SQLITE_FULL } } -extension Row { - public var asDictionary: [String: DatabaseValue] { +// MARK: - + +private extension Row { + var asDictionary: [String: DatabaseValue] { var result = [String: DatabaseValue]() for rowIndex in stride(from: startIndex, to: endIndex, by: 1) { let (columnName, databaseValue) = self[rowIndex] diff --git a/SignalServiceKit/Storage/Database/GRDBDatabaseStorageAdapter.swift b/SignalServiceKit/Storage/Database/GRDBDatabaseStorageAdapter.swift index 6d80f98e09..bce876da64 100644 --- a/SignalServiceKit/Storage/Database/GRDBDatabaseStorageAdapter.swift +++ b/SignalServiceKit/Storage/Database/GRDBDatabaseStorageAdapter.swift @@ -873,7 +873,6 @@ extension GRDBDatabaseStorageAdapter { } /// Run database integrity checks and log their results. - @discardableResult public static func checkIntegrity(databaseStorage: SDSDatabaseStorage) -> SqliteUtil.IntegrityCheckResult { func read(block: (Database) -> T) -> T { return databaseStorage.read { block($0.database) } diff --git a/SignalServiceKit/Storage/Database/GRDBSchemaMigrator.swift b/SignalServiceKit/Storage/Database/GRDBSchemaMigrator.swift index 1991227b5f..091fcd8ea4 100644 --- a/SignalServiceKit/Storage/Database/GRDBSchemaMigrator.swift +++ b/SignalServiceKit/Storage/Database/GRDBSchemaMigrator.swift @@ -4503,7 +4503,7 @@ public class GRDBSchemaMigrator { DELETE FROM \(InteractionRecord.databaseTableName) WHERE \(interactionColumn: .recordType) = ? """ - transaction.database.executeHandlingErrors( + try transaction.database.execute( sql: sql, arguments: [SDSRecordType.invalidIdentityKeySendingErrorMessage.rawValue] ) diff --git a/SignalServiceKit/Storage/Database/KeyValueStore.swift b/SignalServiceKit/Storage/Database/KeyValueStore.swift index 9ecf503631..0d04aaf07a 100644 --- a/SignalServiceKit/Storage/Database/KeyValueStore.swift +++ b/SignalServiceKit/Storage/Database/KeyValueStore.swift @@ -86,28 +86,11 @@ public struct KeyValueStore { // MARK: - Data public func getData(_ key: String, transaction: DBReadTransaction) -> Data? { - do { - return try NewKeyValueStore(collection: collection).fetchValueOrThrow(Data.self, forKey: key, tx: transaction) - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFailDebug("error: \(error)") - return nil - } + return NewKeyValueStore(collection: collection).fetchValue(Data.self, forKey: key, tx: transaction) } - public func setData(_ data: Data?, key: String, transaction: DBWriteTransaction) { - do { - try NewKeyValueStore(collection: collection).writeValueOrThrow(data, forKey: key, tx: transaction) - } catch { - DatabaseCorruptionState.flagDatabaseCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFailDebug("Error: \(error)") - } + public func setData(_ data: Data?, key: String, transaction tx: DBWriteTransaction) { + NewKeyValueStore(collection: collection).writeValue(data, forKey: key, tx: tx) } // MARK: - Int @@ -221,16 +204,8 @@ public struct KeyValueStore { } } - public func removeAll(transaction: DBWriteTransaction) { - do { - try NewKeyValueStore(collection: collection).removeAllOrThrow(tx: transaction) - } catch { - DatabaseCorruptionState.flagDatabaseCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFail("Error: \(error)") - } + public func removeAll(transaction tx: DBWriteTransaction) { + NewKeyValueStore(collection: collection).removeAll(tx: tx) } public func allDataValues(transaction: DBReadTransaction) -> [Data] { @@ -253,16 +228,8 @@ public struct KeyValueStore { return dataValue } - public func allKeys(transaction: DBReadTransaction) -> [String] { - do { - return try NewKeyValueStore(collection: collection).fetchKeysOrThrow(tx: transaction) - } catch { - DatabaseCorruptionState.flagDatabaseCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFail("Error: \(error)") - } + public func allKeys(transaction tx: DBReadTransaction) -> [String] { + return NewKeyValueStore(collection: collection).fetchKeys(tx: tx) } // MARK: - diff --git a/SignalServiceKit/Storage/Database/Records/InteractionFinder.swift b/SignalServiceKit/Storage/Database/Records/InteractionFinder.swift index d5911d99a8..194e5f6019 100644 --- a/SignalServiceKit/Storage/Database/Records/InteractionFinder.swift +++ b/SignalServiceKit/Storage/Database/Records/InteractionFinder.swift @@ -69,18 +69,12 @@ public class InteractionFinder: NSObject { sourceAci.serviceIdUppercaseString, SignalServiceAddress(sourceAci).phoneNumber ] - do { + return failIfThrows { return try Bool.fetchOne( transaction.database, sql: sql, arguments: arguments ) ?? false - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFail("Failed to find incoming message") } } @@ -858,18 +852,12 @@ public class InteractionFinder: NSObject { threadUniqueId, SDSRecordType.outgoingMessage.rawValue ] - do { + return failIfThrows { return try Bool.fetchOne( transaction.database, sql: sql, arguments: arguments ) ?? false - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFail("Failed to find outgoing message") } } @@ -885,18 +873,12 @@ public class InteractionFinder: NSObject { """ let arguments: StatementArguments = [threadUniqueId] - do { + return failIfThrows { return try Bool.fetchOne( transaction.database, sql: sql, arguments: arguments ) ?? false - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFail("Failed to find info message") } } @@ -942,18 +924,12 @@ public class InteractionFinder: NSObject { """ let arguments: StatementArguments = [threadUniqueId] - do { + return failIfThrows { return try Bool.fetchOne( transaction.database, sql: sql, arguments: arguments ) ?? false - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFail("Failed to find info message") } } @@ -1013,18 +989,12 @@ public class InteractionFinder: NSObject { """ let arguments: StatementArguments = [threadUniqueId] - do { + return failIfThrows { return try Bool.fetchOne( transaction.database, sql: sql, arguments: arguments ) ?? false - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFail("Failed to determine interaction") } } @@ -1060,18 +1030,12 @@ public class InteractionFinder: NSObject { """ let arguments: StatementArguments = [threadUniqueId] - do { + return failIfThrows { return try Bool.fetchOne( transaction.database, sql: sql, arguments: arguments ) ?? false - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFail("Failed to find Incoming message") } } @@ -1088,18 +1052,12 @@ public class InteractionFinder: NSObject { SDSRecordType.outgoingMessage.rawValue ] - do { + return failIfThrows { return try UInt.fetchOne( transaction.database, sql: sql, arguments: arguments ) ?? 0 - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFail("Failed to determine message count") } } @@ -1118,18 +1076,12 @@ public class InteractionFinder: NSObject { limit ] - do { + return failIfThrows { return try UInt.fetchOne( transaction.database, sql: sql, arguments: arguments ) ?? 0 - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFail("Failed to determine message count") } } diff --git a/SignalServiceKit/Storage/Database/Records/ThreadFinder.swift b/SignalServiceKit/Storage/Database/Records/ThreadFinder.swift index 854edcdc42..24d668a0c1 100644 --- a/SignalServiceKit/Storage/Database/Records/ThreadFinder.swift +++ b/SignalServiceKit/Storage/Database/Records/ThreadFinder.swift @@ -157,7 +157,7 @@ public class ThreadFinder { isArchived: Bool, transaction: DBReadTransaction, block: (TSThread) -> Void - ) throws { + ) { let sql = """ SELECT * FROM \(ThreadRecord.databaseTableName) @@ -166,20 +166,13 @@ public class ThreadFinder { ORDER BY \(threadColumn: .lastInteractionRowId) DESC """ - do { + failIfThrows { try ThreadRecord.fetchCursor( transaction.database, sql: sql ).forEach { threadRecord in block(try TSThread.fromRecord(threadRecord)) } - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - // rethrow the error after marking database - throw error } } @@ -333,18 +326,12 @@ public class ThreadFinder { ) """ let arguments: StatementArguments = [SDSRecordType.groupThread.rawValue] - do { + return failIfThrows { return try Bool.fetchOne( transaction.database, sql: sql, arguments: arguments ) ?? false - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFail("Failed to find group thread") } } diff --git a/SignalServiceKit/Storage/Database/SDSCodableModel/SDSCodableModelDatabaseInterface+Enumerate.swift b/SignalServiceKit/Storage/Database/SDSCodableModel/SDSCodableModelDatabaseInterface+Enumerate.swift index f665fff691..8f059820c5 100644 --- a/SignalServiceKit/Storage/Database/SDSCodableModel/SDSCodableModelDatabaseInterface+Enumerate.swift +++ b/SignalServiceKit/Storage/Database/SDSCodableModel/SDSCodableModelDatabaseInterface+Enumerate.swift @@ -70,7 +70,7 @@ extension SDSCodableModelDatabaseInterfaceImpl { batchSize: UInt, block: (Model, UnsafeMutablePointer) -> Void ) { - do { + failIfThrows { var recordCursor: RecordCursor if let sql = sql, let arguments = arguments { recordCursor = try Model.fetchCursor( @@ -90,12 +90,6 @@ extension SDSCodableModelDatabaseInterfaceImpl { value.anyDidEnumerateOne(transaction: transaction) block(value, stop) } - } catch let error { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFailDebug("Failed to fetch models: \(error)!") } } } diff --git a/SignalServiceKit/Storage/Database/SDSCodableModel/SDSCodableModelDatabaseInterface+Fetch.swift b/SignalServiceKit/Storage/Database/SDSCodableModel/SDSCodableModelDatabaseInterface+Fetch.swift index 5d9856e9ef..98d3059d95 100644 --- a/SignalServiceKit/Storage/Database/SDSCodableModel/SDSCodableModelDatabaseInterface+Fetch.swift +++ b/SignalServiceKit/Storage/Database/SDSCodableModel/SDSCodableModelDatabaseInterface+Fetch.swift @@ -44,7 +44,7 @@ extension SDSCodableModelDatabaseInterfaceImpl { arguments: StatementArguments, transaction: DBReadTransaction ) -> Model? { - do { + return failIfThrows { let model = try modelType.fetchOne( transaction.database, sql: sql, @@ -52,13 +52,6 @@ extension SDSCodableModelDatabaseInterfaceImpl { ) model?.anyDidFetchOne(transaction: transaction) return model - } catch let error { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFailDebug("Failed to fetch model \(modelType): \(error.grdbErrorForLogging)") - return nil } } @@ -67,7 +60,7 @@ extension SDSCodableModelDatabaseInterfaceImpl { modelType: Model.Type, transaction: DBReadTransaction ) -> [Model] { - do { + return failIfThrows { let sql: String = """ SELECT * FROM \(modelType.databaseTableName) """ @@ -76,13 +69,6 @@ extension SDSCodableModelDatabaseInterfaceImpl { transaction.database, sql: sql ) - } catch let error { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFailDebug("Failed to fetch \(modelType) models: \(error.grdbErrorForLogging)") - return [] } } diff --git a/SignalServiceKit/Storage/Database/SDSCodableModel/SDSCodableModelDatabaseInterface+Remove.swift b/SignalServiceKit/Storage/Database/SDSCodableModel/SDSCodableModelDatabaseInterface+Remove.swift index 576846876d..200bf3cfd3 100644 --- a/SignalServiceKit/Storage/Database/SDSCodableModel/SDSCodableModelDatabaseInterface+Remove.swift +++ b/SignalServiceKit/Storage/Database/SDSCodableModel/SDSCodableModelDatabaseInterface+Remove.swift @@ -26,7 +26,7 @@ extension SDSCodableModelDatabaseInterfaceImpl { _ model: Model, transaction: DBWriteTransaction ) { - do { + failIfThrows { let sql: String = """ DELETE FROM \(Model.databaseTableName.quotedDatabaseIdentifier) WHERE uniqueId = ? @@ -35,13 +35,6 @@ extension SDSCodableModelDatabaseInterfaceImpl { let statement = try transaction.database.cachedStatement(sql: sql) try statement.setArguments([model.uniqueId]) try statement.execute() - } catch let error { - DatabaseCorruptionState.flagDatabaseCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - - owsFail("Delete failed: \(error.grdbErrorForLogging)") } } } diff --git a/SignalServiceKit/Storage/Database/SDSCodableModel/SDSCodableModelDatabaseInterface+Save.swift b/SignalServiceKit/Storage/Database/SDSCodableModel/SDSCodableModelDatabaseInterface+Save.swift index 8e64510a8a..3d68eae475 100644 --- a/SignalServiceKit/Storage/Database/SDSCodableModel/SDSCodableModelDatabaseInterface+Save.swift +++ b/SignalServiceKit/Storage/Database/SDSCodableModel/SDSCodableModelDatabaseInterface+Save.swift @@ -195,18 +195,11 @@ private extension SDSCodableModelDatabaseInterface { existingGrdbRowId: SDSCodableModel.RowId, transaction: DBWriteTransaction ) { - do { + failIfThrows { var recordCopy = model recordCopy.id = existingGrdbRowId try recordCopy.update(transaction.database) - } catch let error { - DatabaseCorruptionState.flagDatabaseCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - - owsFail("Update failed: \(error.grdbErrorForLogging)") } } @@ -214,15 +207,8 @@ private extension SDSCodableModelDatabaseInterface { model: Model, transaction: DBWriteTransaction ) { - do { + failIfThrows { try model.insert(transaction.database) - } catch let error { - DatabaseCorruptionState.flagDatabaseCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - - owsFail("Insert failed: \(error.grdbErrorForLogging)") } } } diff --git a/SignalServiceKit/Storage/Database/SDSDatabaseStorage/SDSDatabaseStorage.swift b/SignalServiceKit/Storage/Database/SDSDatabaseStorage/SDSDatabaseStorage.swift index 3ff8db9c9f..6292162e61 100644 --- a/SignalServiceKit/Storage/Database/SDSDatabaseStorage/SDSDatabaseStorage.swift +++ b/SignalServiceKit/Storage/Database/SDSDatabaseStorage/SDSDatabaseStorage.swift @@ -67,22 +67,14 @@ public class SDSDatabaseStorage: NSObject, DB { } func runGrdbSchemaMigrations() { - let didPerformIncrementalMigrations: Bool - do { - didPerformIncrementalMigrations = try GRDBSchemaMigrator.migrateDatabase(databaseStorage: self, runDataMigrations: false) - } catch { - DatabaseCorruptionState.flagDatabaseCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error + failIfThrows { + let didPerformIncrementalMigrations = try GRDBSchemaMigrator.migrateDatabase( + databaseStorage: self, + runDataMigrations: false, ) - owsFail("Database migration failed. Error: \(error.grdbErrorForLogging)") - } - if didPerformIncrementalMigrations { - do { + if didPerformIncrementalMigrations { try reopenGRDBStorage() - } catch { - owsFail("Unable to reopen storage \(error.grdbErrorForLogging)") } } } @@ -91,14 +83,11 @@ public class SDSDatabaseStorage: NSObject, DB { /// should be impossible to execute this method when there are any /// outstanding schema migrations. func runGrdbDataMigrations() { - do { - try GRDBSchemaMigrator.migrateDatabase(databaseStorage: self, runDataMigrations: true) - } catch { - DatabaseCorruptionState.flagDatabaseCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error + failIfThrows { + _ = try GRDBSchemaMigrator.migrateDatabase( + databaseStorage: self, + runDataMigrations: true ) - owsFail("Database migration failed. Error: \(error.grdbErrorForLogging)") } } @@ -153,11 +142,7 @@ public class SDSDatabaseStorage: NSObject, DB { _databaseChangeObserver.didTouch(interaction: interaction, transaction: tx) } if shouldReindex, let message = interaction as? TSMessage { - do { - try FullTextSearchIndexer.update(message, tx: tx) - } catch { - owsFail("Error: \(error)") - } + FullTextSearchIndexer.update(message, tx: tx) } } @@ -249,23 +234,6 @@ public class SDSDatabaseStorage: NSObject, DB { try grdbStorage.read { try block($0) } } - public func read( - file: String, - function: String, - line: Int, - block: (DBReadTransaction) -> Void - ) { - do { - try readThrows(file: file, function: function, line: line, block: block) - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFail("error: \(error.grdbErrorForLogging)") - } - } - @objc(readWithBlock:) public func readObjC(block: (DBReadTransaction) -> Void) { read(file: "objc", function: "block", line: 0, block: block) @@ -276,23 +244,16 @@ public class SDSDatabaseStorage: NSObject, DB { function: String, line: Int, block: (DBReadTransaction) throws(E) -> T - ) throws(E) -> T { - return try _read(file: file, function: function, line: line, block: block) - } - - private func _read( - file: String, - function: String, - line: Int, - block: (DBReadTransaction) throws(E) -> T, ) throws(E) -> T { var value: T! var thrown: E? - read(file: file, function: function, line: line) { tx in - do throws(E) { - value = try block(tx) - } catch { - thrown = error + failIfThrows { + try readThrows(file: file, function: function, line: line) { tx in + do throws(E) { + value = try block(tx) + } catch { + thrown = error + } } } if let thrown { @@ -377,7 +338,7 @@ public class SDSDatabaseStorage: NSObject, DB { ) throws(E) -> T { var value: T! var thrown: E? - do { + failIfThrows { try performWriteWithTxCompletion( file: file, function: function, @@ -392,8 +353,6 @@ public class SDSDatabaseStorage: NSObject, DB { return completionIfThrows } } - } catch { - owsFail("error: \(error.grdbErrorForLogging)") } if let thrown { throw thrown @@ -504,21 +463,15 @@ public class SDSDatabaseStorage: NSObject, DB { line: Int, block: (DBWriteTransaction) -> Void ) { - do { - try performWriteWithTxCompletion( - file: file, - function: function, - line: line, - isAwaitableWrite: false, - block: { - block($0) - // The block can't throw; always commit. - return .commit - } - ) - } catch { - owsFail("error: \(error.grdbErrorForLogging)") - } + _writeWithTxCompletionIfThrows( + file: file, + function: function, + line: line, + isAwaitableWrite: false, + // The block can't throw: always commit. + completionIfThrows: .commit, + block: block, + ) } /// NOTE: Do NOT call these methods directly. See SDSDatabaseStorage+Objc.h. diff --git a/SignalServiceKit/Storage/Database/SDSModel.swift b/SignalServiceKit/Storage/Database/SDSModel.swift index 892cc9f920..f789d2658b 100644 --- a/SignalServiceKit/Storage/Database/SDSModel.swift +++ b/SignalServiceKit/Storage/Database/SDSModel.swift @@ -60,7 +60,12 @@ public extension SDSModel { FROM \(sdsTableName) WHERE uniqueId == ? """ - tx.database.executeAndCacheStatementHandlingErrors(sql: sql, arguments: [uniqueId]) + failIfThrows { + try tx.database.execute( + sql: sql, + arguments: [uniqueId], + ) + } anyDidRemove(with: tx) } diff --git a/SignalServiceKit/Storage/Database/SDSRecord.swift b/SignalServiceKit/Storage/Database/SDSRecord.swift index db9b6745e1..3f9f38e2d1 100644 --- a/SignalServiceKit/Storage/Database/SDSRecord.swift +++ b/SignalServiceKit/Storage/Database/SDSRecord.swift @@ -53,28 +53,16 @@ public extension SDSRecord { } private func sdsUpdate(grdbId: Int64, transaction: DBWriteTransaction) { - do { + failIfThrows { var recordCopy = self recordCopy.id = grdbId try recordCopy.update(transaction.database) - } catch { - DatabaseCorruptionState.flagDatabaseCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFail("Update failed: \(error.grdbErrorForLogging)") } } private func sdsInsert(transaction: DBWriteTransaction) { - do { + failIfThrows { try self.insert(transaction.database) - } catch { - DatabaseCorruptionState.flagDatabaseCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFail("Insert failed: \(error.grdbErrorForLogging)") } } } @@ -98,20 +86,13 @@ extension BaseModel { uniqueIdColumnName: String, uniqueIdColumnValue: String, transaction: DBReadTransaction) -> Int64? { - do { + return failIfThrows { let tableName = tableMetadata.tableName let sql = "SELECT id FROM \(tableName.quotedDatabaseIdentifier) WHERE \(uniqueIdColumnName.quotedDatabaseIdentifier)=?" guard let value = try Int64.fetchOne(transaction.database, sql: sql, arguments: [uniqueIdColumnValue]) else { return nil } return value - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFailDebug("Could not find grdb id: \(error)") - return nil } } } diff --git a/SignalServiceKit/Subscriptions/Donations/DonationReceiptFinder.swift b/SignalServiceKit/Subscriptions/Donations/DonationReceiptFinder.swift index 8fb136db7b..e3a84fb963 100644 --- a/SignalServiceKit/Subscriptions/Donations/DonationReceiptFinder.swift +++ b/SignalServiceKit/Subscriptions/Donations/DonationReceiptFinder.swift @@ -15,14 +15,8 @@ public class DonationReceiptFinder { LIMIT 1 ) """ - do { + return failIfThrows { return try Bool.fetchOne(transaction.database, sql: sql) ?? false - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFail("Failed to find donation receipt") } } @@ -32,14 +26,8 @@ public class DonationReceiptFinder { FROM \(DonationReceipt.databaseTableName) ORDER BY \(DonationReceipt.columnName(.timestamp)) DESC """ - do { + return failIfThrows { return try DonationReceipt.fetchAll(transaction.database, sql: sql) - } catch { - DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary( - userDefaults: CurrentAppContext().appUserDefaults(), - error: error - ) - owsFail("Failed to fetch donation receipts \(error)") } } } diff --git a/SignalServiceKit/Threads/ThreadStore.swift b/SignalServiceKit/Threads/ThreadStore.swift index 13a06c9d15..f4ebc51476 100644 --- a/SignalServiceKit/Threads/ThreadStore.swift +++ b/SignalServiceKit/Threads/ThreadStore.swift @@ -205,7 +205,12 @@ public class ThreadStoreImpl: ThreadStore { public func removeThread(_ thread: TSThread, tx: DBWriteTransaction) { let sql = "DELETE FROM \(thread.sdsTableName) WHERE uniqueId = ?" - tx.database.executeAndCacheStatementHandlingErrors(sql: sql, arguments: [thread.uniqueId]) + failIfThrows { + try tx.database.execute( + sql: sql, + arguments: [thread.uniqueId], + ) + } } public func updateThread(_ thread: TSThread, tx: DBWriteTransaction) { diff --git a/SignalServiceKit/tests/Messages/GroupMessageProcessorJobTest.swift b/SignalServiceKit/tests/Messages/GroupMessageProcessorJobTest.swift index 54dd60724e..c4e2dc2db8 100644 --- a/SignalServiceKit/tests/Messages/GroupMessageProcessorJobTest.swift +++ b/SignalServiceKit/tests/Messages/GroupMessageProcessorJobTest.swift @@ -10,10 +10,10 @@ import Testing struct GroupMessageProcessorJobTest { @Test - func testDeserialize() throws { + func testDeserialize() { let groupId = Data(repeating: 2, count: 32) let db = InMemoryDB() - try db.write { tx in + try! db.write { tx in try tx.database.execute(sql: """ INSERT INTO "model_IncomingGroupsV2MessageJob" ( "id", @@ -38,7 +38,7 @@ struct GroupMessageProcessorJobTest { ) """) } - let job = try db.read { tx in try GroupMessageProcessorJobStore().nextJob(forGroupId: groupId, tx: tx)! } + let job = db.read { tx in GroupMessageProcessorJobStore().nextJob(forGroupId: groupId, tx: tx)! } #expect(job.id == 42) #expect(job.groupId == groupId) #expect(job.envelopeData == Data([0x12, 0x34])) diff --git a/SignalServiceKit/tests/Storage/Database/DatabaseCorruptionStateTest.swift b/SignalServiceKit/tests/Storage/Database/DatabaseCorruptionStateTest.swift index 70514ad36e..8f4ea71927 100644 --- a/SignalServiceKit/tests/Storage/Database/DatabaseCorruptionStateTest.swift +++ b/SignalServiceKit/tests/Storage/Database/DatabaseCorruptionStateTest.swift @@ -14,53 +14,44 @@ class DatabaseCorruptionStateTest: XCTestCase { } func expected( _ status: DatabaseCorruptionState.DatabaseCorruptionStatus, - count: UInt ) -> DatabaseCorruptionState { - DatabaseCorruptionState(status: status, count: count) + DatabaseCorruptionState(status: status) } // Initial state - XCTAssertEqual(fetch(), expected(.notCorrupted, count: 0)) - - // After flagging as read corrupted - DatabaseCorruptionState.flagDatabaseAsReadCorrupted(userDefaults: defaults) - XCTAssertEqual(fetch(), expected(.readCorrupted, count: 0)) + XCTAssertEqual(fetch(), expected(.notCorrupted)) // After flagging as corrupted DatabaseCorruptionState.flagDatabaseAsCorrupted(userDefaults: defaults) - XCTAssertEqual(fetch(), expected(.corrupted, count: 1)) + XCTAssertEqual(fetch(), expected(.corrupted)) // After partial recovery DatabaseCorruptionState.flagCorruptedDatabaseAsDumpedAndRestored(userDefaults: defaults) - XCTAssertEqual(fetch(), expected(.corruptedButAlreadyDumpedAndRestored, count: 1)) + XCTAssertEqual(fetch(), expected(.corruptedButAlreadyDumpedAndRestored)) // After full recovery - DatabaseCorruptionState.flagDatabaseAsRecoveredFromCorruption(userDefaults: defaults) - XCTAssertEqual(fetch(), expected(.notCorrupted, count: 0)) + DatabaseCorruptionState.flagDatabaseAsNotCorrupted(userDefaults: defaults) + XCTAssertEqual(fetch(), expected(.notCorrupted)) // After another corruption DatabaseCorruptionState.flagDatabaseAsCorrupted(userDefaults: defaults) - XCTAssertEqual(fetch(), expected(.corrupted, count: 1)) - - // Read corruption shouldn't change state after a corruption - DatabaseCorruptionState.flagDatabaseAsReadCorrupted(userDefaults: defaults) - XCTAssertEqual(fetch(), expected(.corrupted, count: 1)) + XCTAssertEqual(fetch(), expected(.corrupted)) } - func testLegacyFalseValueWithoutCount() throws { + func testLegacyFalseValue() throws { let defaults = TestUtils.userDefaults() defaults.set(false, forKey: DatabaseCorruptionState.databaseCorruptionStatusKey) - let expected = DatabaseCorruptionState(status: .notCorrupted, count: 0) + let expected = DatabaseCorruptionState(status: .notCorrupted) let actual = DatabaseCorruptionState(userDefaults: defaults) XCTAssertEqual(actual, expected) } - func testLegacyTrueValueWithoutCount() throws { + func testLegacyTrueValue() throws { let defaults = TestUtils.userDefaults() defaults.set(true, forKey: DatabaseCorruptionState.databaseCorruptionStatusKey) - let expected = DatabaseCorruptionState(status: .corrupted, count: 1) + let expected = DatabaseCorruptionState(status: .corrupted) let actual = DatabaseCorruptionState(userDefaults: defaults) XCTAssertEqual(actual, expected) } @@ -68,9 +59,8 @@ class DatabaseCorruptionStateTest: XCTestCase { func testInvalidData() throws { let defaults = TestUtils.userDefaults() defaults.set("garbage", forKey: DatabaseCorruptionState.databaseCorruptionStatusKey) - defaults.set("garbage", forKey: DatabaseCorruptionState.databaseCorruptionCountKey) - let expected = DatabaseCorruptionState(status: .notCorrupted, count: 0) + let expected = DatabaseCorruptionState(status: .notCorrupted) let actual = DatabaseCorruptionState(userDefaults: defaults) XCTAssertEqual(actual, expected) } diff --git a/SignalServiceKit/tests/Storage/Database/DatabaseRecoveryTest.swift b/SignalServiceKit/tests/Storage/Database/DatabaseRecoveryTest.swift index 56f4103601..d09751ac0d 100644 --- a/SignalServiceKit/tests/Storage/Database/DatabaseRecoveryTest.swift +++ b/SignalServiceKit/tests/Storage/Database/DatabaseRecoveryTest.swift @@ -33,14 +33,14 @@ final class DatabaseRecoveryTest: SSKBaseTest { ) } - // MARK: - Rebuild existing database + // MARK: - Reindex existing database - func testRebuildExistingDatabase() throws { + func testReindexExistingDatabase() throws { let databaseStorage = try newDatabase(keychainStorage: keychainStorage) try GRDBSchemaMigrator.migrateDatabase(databaseStorage: databaseStorage) try XCTUnwrap(databaseStorage.grdbStorage.pool.close()) - DatabaseRecovery.rebuildExistingDatabase(databaseStorage: try cloneDatabaseStorage(databaseStorage)) + DatabaseRecovery.reindex(databaseStorage: try cloneDatabaseStorage(databaseStorage)) // As a smoke test, ensure that the database is still empty. let finishedDatabaseStorage = try cloneDatabaseStorage(databaseStorage) @@ -60,7 +60,7 @@ final class DatabaseRecoveryTest: SSKBaseTest { // MARK: - Dump and restore func testDumpedTables() throws { - let allTableNames = DatabaseRecovery.DumpAndRestore.allTableNames + let allTableNames = DatabaseRecovery.DumpAndRestoreOperation.allTableNames let allTableNamesSet = Set(allTableNames) let hasDuplicates = allTableNames.count != allTableNamesSet.count @@ -103,7 +103,7 @@ final class DatabaseRecoveryTest: SSKBaseTest { try GRDBSchemaMigrator.migrateDatabase(databaseStorage: databaseStorage) try XCTUnwrap(databaseStorage.grdbStorage.pool.close()) - let dump = DatabaseRecovery.DumpAndRestore( + let dump = DatabaseRecovery.DumpAndRestoreOperation( appReadiness: AppReadinessMock(), corruptDatabaseStorage: try cloneDatabaseStorage(databaseStorage), keychainStorage: keychainStorage @@ -183,7 +183,7 @@ final class DatabaseRecoveryTest: SSKBaseTest { try XCTUnwrap(databaseStorage.grdbStorage.pool.close()) - let dump = DatabaseRecovery.DumpAndRestore( + let dump = DatabaseRecovery.DumpAndRestoreOperation( appReadiness: AppReadinessMock(), corruptDatabaseStorage: try cloneDatabaseStorage(databaseStorage), keychainStorage: keychainStorage @@ -262,7 +262,7 @@ final class DatabaseRecoveryTest: SSKBaseTest { } try XCTUnwrap(databaseStorage.grdbStorage.pool.close()) - let dump = DatabaseRecovery.DumpAndRestore( + let dump = DatabaseRecovery.DumpAndRestoreOperation( appReadiness: AppReadinessMock(), corruptDatabaseStorage: try cloneDatabaseStorage(databaseStorage), keychainStorage: keychainStorage @@ -283,7 +283,7 @@ final class DatabaseRecoveryTest: SSKBaseTest { } try XCTUnwrap(databaseStorage.grdbStorage.pool.close()) - let dump = DatabaseRecovery.DumpAndRestore( + let dump = DatabaseRecovery.DumpAndRestoreOperation( appReadiness: AppReadinessMock(), corruptDatabaseStorage: try cloneDatabaseStorage(databaseStorage), keychainStorage: keychainStorage @@ -332,7 +332,7 @@ final class DatabaseRecoveryTest: SSKBaseTest { try XCTUnwrap(databaseStorage.grdbStorage.pool.close()) - let dump = DatabaseRecovery.DumpAndRestore( + let dump = DatabaseRecovery.DumpAndRestoreOperation( appReadiness: AppReadinessMock(), corruptDatabaseStorage: try cloneDatabaseStorage(databaseStorage), keychainStorage: keychainStorage @@ -345,8 +345,8 @@ final class DatabaseRecoveryTest: SSKBaseTest { keychainStorage: keychainStorage ) - let manualRecreation = DatabaseRecovery.ManualRecreation(databaseStorage: finishedDatabaseStorage) - manualRecreation.run() + let recreateFTSIndex = DatabaseRecovery.RecreateFTSIndexOperation(databaseStorage: finishedDatabaseStorage) + recreateFTSIndex.run() finishedDatabaseStorage.read { transaction in func searchMessages(for searchText: String) -> [TSMessage] { @@ -420,8 +420,8 @@ final class DatabaseRecoveryTest: SSKBaseTest { // MARK: - Test-only extensions -extension DatabaseRecovery.DumpAndRestore { - fileprivate static var allTableNames: [String] { +private extension DatabaseRecovery.DumpAndRestoreOperation { + static var allTableNames: [String] { tablesToCopyWithBestEffort + tablesThatMustBeCopiedFlawlessly + tablesExplicitlySkipped } } diff --git a/SignalUI/RecipientPickers/ConversationPicker.swift b/SignalUI/RecipientPickers/ConversationPicker.swift index fb9e77ab7f..2df6998aa3 100644 --- a/SignalUI/RecipientPickers/ConversationPicker.swift +++ b/SignalUI/RecipientPickers/ConversationPicker.swift @@ -385,11 +385,11 @@ open class ConversationPickerViewController: OWSTableViewController2 { } } - try! ThreadFinder().enumerateVisibleThreads(isArchived: false, transaction: transaction) { thread in + ThreadFinder().enumerateVisibleThreads(isArchived: false, transaction: transaction) { thread in addThread(thread) } - try! ThreadFinder().enumerateVisibleThreads(isArchived: true, transaction: transaction) { thread in + ThreadFinder().enumerateVisibleThreads(isArchived: true, transaction: transaction) { thread in addThread(thread) }