From df86d1b4ba87e1ec87fbd40f1417fe7a7f608520 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Wed, 11 Jan 2023 13:11:19 -0500 Subject: [PATCH] Re-run migration to add foreign key to thread table. Unfortunately the table schema wasn't fixed for new installs, so we need to run it again. --- .../securesms/database/ThreadTable.kt | 6 +- .../helpers/SignalDatabaseMigrations.kt | 7 +- .../migration/V171_ThreadForeignKeyFix.kt | 180 ++++++++++++++++++ 3 files changed, 189 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V171_ThreadForeignKeyFix.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt index bcd215b00e..c156f13e2b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt @@ -105,7 +105,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa $ID INTEGER PRIMARY KEY AUTOINCREMENT, $DATE INTEGER DEFAULT 0, $MEANINGFUL_MESSAGES INTEGER DEFAULT 0, - $RECIPIENT_ID INTEGER, + $RECIPIENT_ID INTEGER NOT NULL UNIQUE REFERENCES ${RecipientTable.TABLE_NAME} (${RecipientTable.ID}) ON DELETE CASCADE, $READ INTEGER DEFAULT ${ReadStatus.READ.serialize()}, $TYPE INTEGER DEFAULT 0, $ERROR INTEGER DEFAULT 0, @@ -114,14 +114,14 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa $SNIPPET_URI TEXT DEFAULT NULL, $SNIPPET_CONTENT_TYPE TEXT DEFAULT NULL, $SNIPPET_EXTRAS TEXT DEFAULT NULL, + $UNREAD_COUNT INTEGER DEFAULT 0, $ARCHIVED INTEGER DEFAULT 0, $STATUS INTEGER DEFAULT 0, $DELIVERY_RECEIPT_COUNT INTEGER DEFAULT 0, + $READ_RECEIPT_COUNT INTEGER DEFAULT 0, $EXPIRES_IN INTEGER DEFAULT 0, $LAST_SEEN INTEGER DEFAULT 0, $HAS_SENT INTEGER DEFAULT 0, - $READ_RECEIPT_COUNT INTEGER DEFAULT 0, - $UNREAD_COUNT INTEGER DEFAULT 0, $LAST_SCROLLED INTEGER DEFAULT 0, $PINNED INTEGER DEFAULT 0, $UNREAD_SELF_MENTION_COUNT INTEGER DEFAULT 0 diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt index 7b097448b0..cdd22f0c6e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt @@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V167_RecreateReacti import org.thoughtcrime.securesms.database.helpers.migration.V168_SingleMessageTableMigration import org.thoughtcrime.securesms.database.helpers.migration.V169_EmojiSearchIndexRank import org.thoughtcrime.securesms.database.helpers.migration.V170_CallTableMigration +import org.thoughtcrime.securesms.database.helpers.migration.V171_ThreadForeignKeyFix /** * Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness. @@ -34,7 +35,7 @@ object SignalDatabaseMigrations { val TAG: String = Log.tag(SignalDatabaseMigrations.javaClass) - const val DATABASE_VERSION = 170 + const val DATABASE_VERSION = 171 @JvmStatic fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { @@ -125,6 +126,10 @@ object SignalDatabaseMigrations { if (oldVersion < 170) { V170_CallTableMigration.migrate(context, db, oldVersion, newVersion) } + + if (oldVersion < 171) { + V171_ThreadForeignKeyFix.migrate(context, db, oldVersion, newVersion) + } } @JvmStatic diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V171_ThreadForeignKeyFix.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V171_ThreadForeignKeyFix.kt new file mode 100644 index 0000000000..a7235359c9 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V171_ThreadForeignKeyFix.kt @@ -0,0 +1,180 @@ +package org.thoughtcrime.securesms.database.helpers.migration + +import android.app.Application +import net.zetetic.database.sqlcipher.SQLiteDatabase +import org.signal.core.util.Stopwatch +import org.signal.core.util.delete +import org.signal.core.util.logging.Log +import org.signal.core.util.readToList +import org.signal.core.util.requireLong +import org.signal.core.util.toSingleLine +import org.signal.core.util.update + +/** + * When we ran [V166_ThreadAndMessageForeignKeys], we forgot to update the actual table definition in [ThreadTable]. + * We could make this conditional, but I'd rather run it on everyone just so it's more predictable. + */ +object V171_ThreadForeignKeyFix : SignalDatabaseMigration { + + private val TAG = Log.tag(V171_ThreadForeignKeyFix::class.java) + + override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + val stopwatch = Stopwatch("migration") + + removeDuplicateThreadEntries(db) + stopwatch.split("thread-dupes") + + updateThreadTableSchema(db) + stopwatch.split("thread-schema") + + stopwatch.stop(TAG) + } + + private fun removeDuplicateThreadEntries(db: SQLiteDatabase) { + db.rawQuery( + """ + SELECT + recipient_id, + COUNT(*) AS thread_count + FROM thread + GROUP BY recipient_id HAVING thread_count > 1 + """.toSingleLine() + ).use { cursor -> + while (cursor.moveToNext()) { + val recipientId = cursor.requireLong("recipient_id") + val count = cursor.requireLong("thread_count") + Log.w(TAG, "There were $count threads for RecipientId::$recipientId. Merging.", true) + + val threads: List = getThreadsByRecipientId(db, cursor.requireLong("recipient_id")) + mergeThreads(db, threads) + } + } + } + + private fun getThreadsByRecipientId(db: SQLiteDatabase, recipientId: Long): List { + return db.rawQuery("SELECT _id, date FROM thread WHERE recipient_id = ?".trimIndent(), recipientId).readToList { cursor -> + ThreadInfo(cursor.requireLong("_id"), cursor.requireLong("date")) + } + } + + private fun mergeThreads(db: SQLiteDatabase, threads: List) { + val primaryThread: ThreadInfo = threads.maxByOrNull { it.date }!! + val secondaryThreads: List = threads.filterNot { it.id == primaryThread.id } + + secondaryThreads.forEach { secondaryThread -> + remapThread(db, primaryThread.id, secondaryThread.id) + } + } + + private fun remapThread(db: SQLiteDatabase, primaryId: Long, secondaryId: Long) { + db.update("drafts") + .values("thread_id" to primaryId) + .where("thread_id = ?", secondaryId) + .run() + + db.update("mention") + .values("thread_id" to primaryId) + .where("thread_id = ?", secondaryId) + .run() + + db.update("mms") + .values("thread_id" to primaryId) + .where("thread_id = ?", secondaryId) + .run() + + db.update("sms") + .values("thread_id" to primaryId) + .where("thread_id = ?", secondaryId) + .run() + + db.update("pending_retry_receipts") + .values("thread_id" to primaryId) + .where("thread_id = ?", secondaryId) + .run() + + // We're dealing with threads that exist, so we don't need to remap old_ids + + val count = db.update("remapped_threads") + .values("new_id" to primaryId) + .where("new_id = ?", secondaryId) + .run() + Log.w(TAG, "Remapped $count remapped_threads new_ids from $secondaryId to $primaryId", true) + + db.delete("thread") + .where("_id = ?", secondaryId) + .run() + } + + private fun updateThreadTableSchema(db: SQLiteDatabase) { + db.execSQL( + """ + CREATE TABLE thread_tmp ( + _id INTEGER PRIMARY KEY AUTOINCREMENT, + date INTEGER DEFAULT 0, + meaningful_messages INTEGER DEFAULT 0, + recipient_id INTEGER NOT NULL UNIQUE REFERENCES recipient (_id) ON DELETE CASCADE, + read INTEGER DEFAULT 1, + type INTEGER DEFAULT 0, + error INTEGER DEFAULT 0, + snippet TEXT, + snippet_type INTEGER DEFAULT 0, + snippet_uri TEXT DEFAULT NULL, + snippet_content_type TEXT DEFAULT NULL, + snippet_extras TEXT DEFAULT NULL, + unread_count INTEGER DEFAULT 0, + archived INTEGER DEFAULT 0, + status INTEGER DEFAULT 0, + delivery_receipt_count INTEGER DEFAULT 0, + read_receipt_count INTEGER DEFAULT 0, + expires_in INTEGER DEFAULT 0, + last_seen INTEGER DEFAULT 0, + has_sent INTEGER DEFAULT 0, + last_scrolled INTEGER DEFAULT 0, + pinned INTEGER DEFAULT 0, + unread_self_mention_count INTEGER DEFAULT 0 + ) + """.trimIndent() + ) + + db.execSQL( + """ + INSERT INTO thread_tmp + SELECT + _id, + date, + meaningful_messages, + recipient_id, + read, + type, + error, + snippet, + snippet_type, + snippet_uri, + snippet_content_type, + snippet_extras, + unread_count, + archived, + status, + delivery_receipt_count, + read_receipt_count, + expires_in, + last_seen, + has_sent, + last_scrolled, + pinned, + unread_self_mention_count + FROM thread + """.trimMargin() + ) + + db.execSQL("DROP TABLE thread") + db.execSQL("ALTER TABLE thread_tmp RENAME TO thread") + + db.execSQL("CREATE INDEX thread_recipient_id_index ON thread (recipient_id)") + db.execSQL("CREATE INDEX archived_count_index ON thread (archived, meaningful_messages)") + db.execSQL("CREATE INDEX thread_pinned_index ON thread (pinned)") + db.execSQL("CREATE INDEX thread_read ON thread (read)") + } + + data class ThreadInfo(val id: Long, val date: Long) +}