Clean up orphaned latest_revision_id references during backup restore.

This commit is contained in:
Greyson Parrelli
2026-07-17 14:36:09 +00:00
parent 39da95f29b
commit 551a25cfc6
2 changed files with 32 additions and 0 deletions
@@ -6490,6 +6490,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
.forEach { entry -> fixDuplicate(writableDatabase, entry.value) }
}
// This section deletes things that would otherwise violate foreign key constraints
writableDatabase.execSQL("DELETE FROM $TABLE_NAME WHERE $LATEST_REVISION_ID IS NOT NULL AND $LATEST_REVISION_ID NOT IN (SELECT $ID FROM $TABLE_NAME)")
writableDatabase.execSQL("DELETE FROM $TABLE_NAME WHERE $ORIGINAL_MESSAGE_ID IS NOT NULL AND $ORIGINAL_MESSAGE_ID NOT IN (SELECT $ID FROM $TABLE_NAME)")
writableDatabase.execSQL("DELETE FROM ${ReactionTable.TABLE_NAME} WHERE ${ReactionTable.MESSAGE_ID} NOT IN (SELECT $ID FROM $TABLE_NAME)")
writableDatabase.execSQL("DELETE FROM ${StorySendTable.TABLE_NAME} WHERE ${StorySendTable.MESSAGE_ID} NOT IN (SELECT $ID FROM $TABLE_NAME)")
@@ -165,6 +165,36 @@ class EditMessageRevisionTest {
assertThat(getLatestRevisionId(edit2Id)).isNull()
}
@Test
fun removeDuplicatesRepairsOrphanedLatestRevisionIdInsteadOfLeavingForeignKeyViolation() {
val originalId = insertOriginalMessage(sentTimeMillis = 1000)
val edit1Id = insertEdit(originalSentTimestamp = 1000, editSentTimeMillis = 1001)
val edit2Id = insertEdit(originalSentTimestamp = 1000, editSentTimeMillis = 1002)
assertThat(getLatestRevisionId(originalId)).isNotNull().isEqualTo(edit2Id)
// Simulate what a backup import can produce: the latest revision is gone (deleted as a duplicate, or never inserted), but the
// earlier revisions still point at it via latest_revision_id. Foreign keys are disabled during import, so this is not caught until
// the final integrity check.
SignalDatabase.writableDatabase.execSQL("PRAGMA foreign_keys=OFF")
SignalDatabase.writableDatabase.execSQL("DELETE FROM ${MessageTable.TABLE_NAME} WHERE ${MessageTable.ID} = ?", arrayOf(edit2Id))
assertThat(getLatestRevisionId(originalId)).isNotNull().isEqualTo(edit2Id)
SignalDatabase.messages.removeDuplicatesPostBackupRestore()
assertEquals("Orphaned latest_revision_id references must be cleaned up", 0, countDanglingLatestRevisionIds())
SignalDatabase.writableDatabase.execSQL("PRAGMA foreign_keys=ON")
}
private fun countDanglingLatestRevisionIds(): Int {
return SignalDatabase.writableDatabase
.query("SELECT COUNT(*) FROM ${MessageTable.TABLE_NAME} WHERE ${MessageTable.LATEST_REVISION_ID} IS NOT NULL AND ${MessageTable.LATEST_REVISION_ID} NOT IN (SELECT ${MessageTable.ID} FROM ${MessageTable.TABLE_NAME})")
.use { cursor ->
cursor.moveToFirst()
cursor.getInt(0)
}
}
private fun insertOriginalMessage(sentTimeMillis: Long): Long {
val message = IncomingMessage(
type = MessageType.NORMAL,