diff --git a/SignalServiceKit/Storage/Database/GRDBSchemaMigrator.swift b/SignalServiceKit/Storage/Database/GRDBSchemaMigrator.swift index d919291416..46d9a8655b 100644 --- a/SignalServiceKit/Storage/Database/GRDBSchemaMigrator.swift +++ b/SignalServiceKit/Storage/Database/GRDBSchemaMigrator.swift @@ -333,6 +333,7 @@ public class GRDBSchemaMigrator { case addMimeTypeToMessageAttachmentReference case purgeMyStoryDeletedAtTimestamp case addRecoverablePlaceholderExpirationIndex + case backfillRecoverablePlaceholderErrorType // NOTE: Every time we add a migration id, consider // incrementing grdbSchemaVersionLatest. @@ -5214,6 +5215,11 @@ public class GRDBSchemaMigrator { return .success(()) } + migrator.registerMigration(.backfillRecoverablePlaceholderErrorType) { tx in + try Self.backfillRecoverablePlaceholderErrorType(tx: tx) + return .success(()) + } + // MARK: - Schema Migration Insertion Point } @@ -5573,6 +5579,15 @@ public class GRDBSchemaMigrator { // MARK: - Migrations + static func backfillRecoverablePlaceholderErrorType(tx: DBWriteTransaction) throws { + try tx.database.execute(sql: """ + UPDATE model_TSInteraction + INDEXED BY index_interactions_on_recoverable_placeholder_expiration + SET errorType = \(TSErrorMessageType.decryptionFailure.rawValue) + WHERE recordType = \(SDSRecordType.recoverableDecryptionPlaceholder.rawValue) + """) + } + static func addMimeTypeToMessageAttachmentReference(tx: DBWriteTransaction) throws { try tx.database.alter(table: "MessageAttachmentReference") { table in // In practice, this column will be NOT NULL. However, we can't diff --git a/SignalServiceKit/Storage/Database/Records/InteractionFinder.swift b/SignalServiceKit/Storage/Database/Records/InteractionFinder.swift index c125cf0ad8..b71a21b300 100644 --- a/SignalServiceKit/Storage/Database/Records/InteractionFinder.swift +++ b/SignalServiceKit/Storage/Database/Records/InteractionFinder.swift @@ -383,17 +383,27 @@ public class InteractionFinder: NSObject { ORDER BY \(interactionColumn: .receivedAtTimestamp) """ - let cursor = TSInteraction.grdbFetchCursor( - sql: sql, - transaction: tx, - ) + var cursor = FailIfThrowsRecordCursor { + try InteractionRecord.fetchCursor( + tx.database, + sql: sql, + ) + } + + while let interactionRecord = cursor.next() { + let interaction: TSInteraction + do { + interaction = try TSInteraction.fromRecord(interactionRecord) + } catch { + owsFailDebug("Failed to deserialize TSInteraction! \(error)") + continue + } + + guard let placeholder = interaction as? OWSRecoverableDecryptionPlaceholder else { + owsFailDebug("TSInteraction was not an OWSRecoverableDecryptionPlaceholder?") + continue + } - while - // Silently skip malformed TSInteraction rows, lest we become stuck - // on a malformed row forever. - let interaction = try? cursor.next(), - let placeholder = interaction as? OWSRecoverableDecryptionPlaceholder - { return placeholder } diff --git a/SignalServiceKit/tests/Storage/Database/GRDBSchemaMigratorTest.swift b/SignalServiceKit/tests/Storage/Database/GRDBSchemaMigratorTest.swift index 88a83f5f93..3673217a62 100644 --- a/SignalServiceKit/tests/Storage/Database/GRDBSchemaMigratorTest.swift +++ b/SignalServiceKit/tests/Storage/Database/GRDBSchemaMigratorTest.swift @@ -1642,4 +1642,74 @@ struct GRDBSchemaMigratorTest { false, // regular mp4 ]) } + + @Test + func testBackfillRecoverablePlaceholderErrorType() throws { + let placeholderRecordType = SDSRecordType.recoverableDecryptionPlaceholder.rawValue + let errorMessageRecordType = SDSRecordType.errorMessage.rawValue + let decryptionFailure = TSErrorMessageType.decryptionFailure.rawValue + let nonBlockingIdentityChange = TSErrorMessageType.nonBlockingIdentityChange.rawValue + + let databaseQueue = DatabaseQueue() + try databaseQueue.write { db in + try db.execute(sql: """ + CREATE TABLE "model_TSInteraction" ( + id INTEGER PRIMARY KEY, + recordType INTEGER NOT NULL, + uniqueId TEXT NOT NULL UNIQUE, + receivedAtTimestamp INTEGER NOT NULL, + errorType INTEGER + ); + """) + try db.execute(sql: """ + CREATE INDEX "index_interactions_on_recoverable_placeholder_expiration" + ON "model_TSInteraction"(receivedAtTimestamp) + WHERE recordType = \(placeholderRecordType); + """) + try db.execute( + sql: """ + INSERT INTO "model_TSInteraction" (id, recordType, uniqueId, receivedAtTimestamp, errorType) + VALUES (?, ?, ?, ?, ?), (?, ?, ?, ?, ?), (?, ?, ?, ?, ?), (?, ?, ?, ?, ?) + """, + arguments: [ + 1, + placeholderRecordType, + "A", + 1000, + nil, + 2, + placeholderRecordType, + "B", + 2000, + nonBlockingIdentityChange, + 3, + errorMessageRecordType, + "C", + 3000, + nil, + 4, + errorMessageRecordType, + "D", + 4000, + nonBlockingIdentityChange, + ], + ) + + let tx = DBWriteTransaction(database: db) + defer { tx.finalizeTransaction() } + try GRDBSchemaMigrator.backfillRecoverablePlaceholderErrorType(tx: tx) + } + + let errorTypes = try databaseQueue.read { db in + try Int32?.fetchAll(db, sql: """ + SELECT errorType FROM model_TSInteraction ORDER BY id + """) + } + #expect(errorTypes == [ + decryptionFailure, // placeholder, was NULL -> backfilled + decryptionFailure, // placeholder, errorType corrected + nil, // non-placeholder, untouched + nonBlockingIdentityChange, // non-placeholder, untouched + ]) + } }