Simplify and consolidate "DB Corruption" handling

This commit is contained in:
Sasha Weiss 2025-12-18 17:11:36 -08:00 committed by GitHub
parent 10982f4c69
commit a237b9c114
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
63 changed files with 578 additions and 1217 deletions

View File

@ -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<Void>(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)
}
}
""" % {

View File

@ -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

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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(

View File

@ -11,6 +11,7 @@ class DatabaseRecoveryViewController<SetupResult>: OWSViewController {
private let corruptDatabaseStorage: SDSDatabaseStorage
private let deviceSleepManager: DeviceSleepManagerImpl
private let keychainStorage: any KeychainStorage
private let logger: PrefixedLogger
private let setupSskEnvironment: (SDSDatabaseStorage) -> Task<SetupResult, Never>
private let launchApp: (SetupResult) -> Void
@ -26,6 +27,8 @@ class DatabaseRecoveryViewController<SetupResult>: 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<SetupResult>: 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<SetupResult>: 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<SetupResult>: 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<SetupResult>: 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<Bool> 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<SetupResult>: 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<Bool>(error: error)
}
DatabaseCorruptionState.flagCorruptedDatabaseAsDumpedAndRestored(userDefaults: self.userDefaults)
} else {
progress.completedUnitCount += 1
@ -293,13 +306,14 @@ class DatabaseRecoveryViewController<SetupResult>: 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<SetupResult>: 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<SetupResult>: 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)

View File

@ -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

View File

@ -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";

View File

@ -253,16 +253,11 @@ class BackupArchiveTSMessageContentsArchiver: BackupArchiveProtoStreamWriter {
uniqueInteractionId: BackupArchive.InteractionUniqueId,
context: BackupArchive.RecipientArchivingContext
) -> BackupArchive.ArchiveInteractionResult<ChatItemType> {
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(())
}

View File

@ -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)
}
}

View File

@ -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(

View File

@ -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: -

View File

@ -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")
}
}

View File

@ -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()
}

View File

@ -298,17 +298,14 @@ public extension OWSDisappearingMessagesConfiguration {
@objc
public class OWSDisappearingMessagesConfigurationCursor: NSObject, SDSCursor {
private let transaction: DBReadTransaction
private let cursor: RecordCursor<DisappearingMessagesConfigurationRecord>?
private let cursor: RecordCursor<DisappearingMessagesConfigurationRecord>
init(transaction: DBReadTransaction, cursor: RecordCursor<DisappearingMessagesConfigurationRecord>?) {
init(transaction: DBReadTransaction, cursor: RecordCursor<DisappearingMessagesConfigurationRecord>) {
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<Void>(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)
}
}

View File

@ -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)")
}
}

View File

@ -795,17 +795,14 @@ public extension TSThread {
@objc
public class TSThreadCursor: NSObject, SDSCursor {
private let transaction: DBReadTransaction
private let cursor: RecordCursor<ThreadRecord>?
private let cursor: RecordCursor<ThreadRecord>
init(transaction: DBReadTransaction, cursor: RecordCursor<ThreadRecord>?) {
init(transaction: DBReadTransaction, cursor: RecordCursor<ThreadRecord>) {
self.transaction = transaction
self.cursor = cursor
}
public func next() throws -> TSThread? {
guard let cursor = cursor else {
return nil
}
guard let record = try cursor.next() else {
return nil
}
@ -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<Void>(sql: sql, arguments: arguments, cached: true)
let cursor = try ThreadRecord.fetchCursor(transaction.database, sqlRequest)
return TSThreadCursor(transaction: transaction, cursor: cursor)
} catch {
DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary(
userDefaults: CurrentAppContext().appUserDefaults(),
error: error
)
owsFailDebug("Read failed: \(error)")
return TSThreadCursor(transaction: transaction, cursor: nil)
}
}

View File

@ -330,30 +330,38 @@ class _ThreadMerger_SDSThreadMergerWrapper: _ThreadMerger_SDSThreadMergerShim {
private func mergeInteractions(_ threadPair: MergePair<TSContactThread>, 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<TSContactThread>, 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<TSContactThread>, tx: DBWriteTransaction) {

View File

@ -88,8 +88,12 @@ public func failIfThrows<T>(
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)
}
}
}

View File

@ -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 []
}
}

View File

@ -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
),

View File

@ -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

View File

@ -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],
)
}
}
}

View File

@ -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<ObjCBool>
) -> Void) {
block: @escaping (TSGroupMember, UnsafeMutablePointer<ObjCBool>) -> 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]
)

View File

@ -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)")
}
}
}

View File

@ -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")
}
}

View File

@ -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.")
}
}

View File

@ -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<Data> 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

View File

@ -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
}
}
}

View File

@ -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)

View File

@ -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] {

View File

@ -5387,17 +5387,14 @@ public extension TSInteraction {
@objc
public class TSInteractionCursor: NSObject, SDSCursor {
private let transaction: DBReadTransaction
private let cursor: RecordCursor<InteractionRecord>?
private let cursor: RecordCursor<InteractionRecord>
init(transaction: DBReadTransaction, cursor: RecordCursor<InteractionRecord>?) {
init(transaction: DBReadTransaction, cursor: RecordCursor<InteractionRecord>) {
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<Void>(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)
}
}

View File

@ -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)
}
}

View File

@ -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,
)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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],
)
}
}
}

View File

@ -299,17 +299,14 @@ public extension InstalledSticker {
@objc
public class InstalledStickerCursor: NSObject, SDSCursor {
private let transaction: DBReadTransaction
private let cursor: RecordCursor<InstalledStickerRecord>?
private let cursor: RecordCursor<InstalledStickerRecord>
init(transaction: DBReadTransaction, cursor: RecordCursor<InstalledStickerRecord>?) {
init(transaction: DBReadTransaction, cursor: RecordCursor<InstalledStickerRecord>) {
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<Void>(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)
}
}

View File

@ -338,17 +338,14 @@ public extension StickerPack {
@objc
public class StickerPackCursor: NSObject, SDSCursor {
private let transaction: DBReadTransaction
private let cursor: RecordCursor<StickerPackRecord>?
private let cursor: RecordCursor<StickerPackRecord>
init(transaction: DBReadTransaction, cursor: RecordCursor<StickerPackRecord>?) {
init(transaction: DBReadTransaction, cursor: RecordCursor<StickerPackRecord>) {
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<Void>(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)
}
}

View File

@ -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")
}
}
}

View File

@ -415,17 +415,14 @@ public extension TSPaymentModel {
@objc
public class TSPaymentModelCursor: NSObject, SDSCursor {
private let transaction: DBReadTransaction
private let cursor: RecordCursor<PaymentModelRecord>?
private let cursor: RecordCursor<PaymentModelRecord>
init(transaction: DBReadTransaction, cursor: RecordCursor<PaymentModelRecord>?) {
init(transaction: DBReadTransaction, cursor: RecordCursor<PaymentModelRecord>) {
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<Void>(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)
}
}

View File

@ -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 []
}
}
}

View File

@ -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)")
}
}

View File

@ -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<T>(_ 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)")
}

View File

@ -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)
}
}

View File

@ -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]

View File

@ -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<T>(block: (Database) -> T) -> T {
return databaseStorage.read { block($0.database) }

View File

@ -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]
)

View File

@ -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: -

View File

@ -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")
}
}

View File

@ -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")
}
}

View File

@ -70,7 +70,7 @@ extension SDSCodableModelDatabaseInterfaceImpl {
batchSize: UInt,
block: (Model, UnsafeMutablePointer<ObjCBool>) -> Void
) {
do {
failIfThrows {
var recordCursor: RecordCursor<Model>
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)!")
}
}
}

View File

@ -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 []
}
}

View File

@ -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)")
}
}
}

View File

@ -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)")
}
}
}

View File

@ -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<T, E: Error>(
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.

View File

@ -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)
}

View File

@ -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
}
}
}

View File

@ -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)")
}
}
}

View File

@ -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) {

View File

@ -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]))

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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)
}