Simplify and consolidate "DB Corruption" handling
This commit is contained in:
parent
10982f4c69
commit
a237b9c114
@ -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)
|
||||
}
|
||||
}
|
||||
""" % {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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: -
|
||||
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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],
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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]
|
||||
)
|
||||
|
||||
@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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.")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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] {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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],
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)")
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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) }
|
||||
|
||||
@ -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]
|
||||
)
|
||||
|
||||
@ -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: -
|
||||
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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]))
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user