From b2f450d84966428408150c8d42bf7a31c42591e4 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Fri, 22 May 2026 16:48:12 -0400 Subject: [PATCH] Add index to make marking unread faster. --- .../securesms/database/MessageTable.kt | 30 +++++++++++++------ .../helpers/SignalDatabaseMigrations.kt | 6 ++-- ...AddMessageThreadDateReceivedUnreadIndex.kt | 16 ++++++++++ .../migrations/ApplicationMigrations.java | 7 ++++- 4 files changed, 47 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V317_AddMessageThreadDateReceivedUnreadIndex.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt index 4cc13ac782..51e04af0d4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt @@ -310,6 +310,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat """ private const val INDEX_THREAD_STORY_SCHEDULED_DATE_LATEST_REVISION_ID = "message_thread_story_parent_story_scheduled_date_latest_revision_id_index" + private const val INDEX_THREAD_DATE_RECEIVED_UNREAD = "message_thread_date_received_unread_index" private const val INDEX_DATE_SENT_FROM_TO_THREAD = "message_date_sent_from_to_thread_index" private const val INDEX_THREAD_COUNT = "message_thread_count_index" private const val INDEX_THREAD_UNREAD_COUNT = "message_thread_unread_count_index" @@ -339,6 +340,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat "CREATE INDEX IF NOT EXISTS $INDEX_THREAD_COUNT ON $TABLE_NAME ($THREAD_ID) WHERE $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL AND $COLLAPSED_STATE != ${CollapsedState.COLLAPSED.id}", // This index is created specifically for getting the number of unread messages in a thread and therefore needs to be kept in sync with that query "CREATE INDEX IF NOT EXISTS $INDEX_THREAD_UNREAD_COUNT ON $TABLE_NAME ($THREAD_ID) WHERE $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1 AND $ORIGINAL_MESSAGE_ID IS NULL AND $READ = 0", + // Partial index for marking messages read in a thread (see setMessagesReadSince). Only contains unread/unseen rows. + "CREATE INDEX IF NOT EXISTS $INDEX_THREAD_DATE_RECEIVED_UNREAD ON $TABLE_NAME ($THREAD_ID, $DATE_RECEIVED) WHERE $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND ($READ = 0 OR $REACTIONS_UNREAD = 1 OR $VOTES_UNREAD = 1)", "CREATE INDEX IF NOT EXISTS message_votes_unread_index ON $TABLE_NAME ($VOTES_UNREAD)", "CREATE INDEX IF NOT EXISTS message_pinned_until_index ON $TABLE_NAME ($PINNED_UNTIL)", "CREATE INDEX IF NOT EXISTS message_pinned_at_index ON $TABLE_NAME ($PINNED_AT)", @@ -2523,22 +2526,26 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat } fun setMessagesReadSince(threadId: Long, sinceTimestamp: Long): List { + // The standalone "($READ = 0 OR $REACTIONS_UNREAD = 1 OR $VOTES_UNREAD = 1)" term below is logically redundant -- it's implied by the + // larger read/reactions/votes clause that follows. We need it to satisfy the query planner so we can use INDEX_THREAD_DATE_RECEIVED_UNREAD. + // The index needs to appear exactly in the query. var query = """ - $THREAD_ID = ? AND - $STORY_TYPE = 0 AND - $PARENT_STORY_ID <= 0 AND + $THREAD_ID = ? AND + $STORY_TYPE = 0 AND + $PARENT_STORY_ID <= 0 AND ( $ORIGINAL_MESSAGE_ID IS NULL OR $LATEST_REVISION_ID IS NULL - ) AND + ) AND + ($READ = 0 OR $REACTIONS_UNREAD = 1 OR $VOTES_UNREAD = 1) AND ( - $READ = 0 OR + $READ = 0 OR ( - $REACTIONS_UNREAD = 1 AND + $REACTIONS_UNREAD = 1 AND ($outgoingTypeClause) ) OR ( - $VOTES_UNREAD = 1 AND + $VOTES_UNREAD = 1 AND ($outgoingTypeClause) ) ) @@ -2551,7 +2558,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat args += sinceTimestamp.toString() } - return setMessagesRead(query, args.toTypedArray()) + return setMessagesRead(query, args.toTypedArray(), index = INDEX_THREAD_DATE_RECEIVED_UNREAD) } fun setGroupStoryMessagesReadSince(threadId: Long, groupStoryId: Long, sinceTimestamp: Long): List { @@ -2618,7 +2625,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat private fun setMessagesRead(where: String, arguments: Array?, index: String = INDEX_THREAD_STORY_SCHEDULED_DATE_LATEST_REVISION_ID): List { val releaseChannelId = SignalStore.releaseChannel.releaseChannelRecipientId - return writableDatabase.rawQuery( + val startTime = System.currentTimeMillis() + val results = writableDatabase.rawQuery( """ UPDATE $TABLE_NAME INDEXED BY $index SET $READ = 1, $REACTIONS_UNREAD = 0, $REACTIONS_LAST_SEEN = ${System.currentTimeMillis()}, $VOTES_UNREAD = 0, $VOTES_LAST_SEEN = ${System.currentTimeMillis()} @@ -2645,6 +2653,10 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat } } .filterNotNull() + + Log.d(TAG, "[setMessagesRead] Updated ${results.size} messages in ${System.currentTimeMillis() - startTime} ms using index $index.") + + return results } fun getOldestUnreadMentionDetails(threadId: Long): Pair? { 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 f3f377d920..6302f4632e 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 @@ -169,6 +169,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V313_AddCollapsingU import org.thoughtcrime.securesms.database.helpers.migration.V314_FixMessageRequestAcceptedToRecipient import org.thoughtcrime.securesms.database.helpers.migration.V315_CleanupE164SenderKeyShared import org.thoughtcrime.securesms.database.helpers.migration.V316_AddVerifiedGroupNameHashMigration +import org.thoughtcrime.securesms.database.helpers.migration.V317_AddMessageThreadDateReceivedUnreadIndex import org.thoughtcrime.securesms.database.SQLiteDatabase as SignalSqliteDatabase /** @@ -345,10 +346,11 @@ object SignalDatabaseMigrations { 313 to V313_AddCollapsingUpdateColumns, 314 to V314_FixMessageRequestAcceptedToRecipient, 315 to V315_CleanupE164SenderKeyShared, - 316 to V316_AddVerifiedGroupNameHashMigration + 316 to V316_AddVerifiedGroupNameHashMigration, + 317 to V317_AddMessageThreadDateReceivedUnreadIndex ) - const val DATABASE_VERSION = 316 + const val DATABASE_VERSION = 317 @JvmStatic fun migrate(context: Application, db: SignalSqliteDatabase, oldVersion: Int, newVersion: Int) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V317_AddMessageThreadDateReceivedUnreadIndex.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V317_AddMessageThreadDateReceivedUnreadIndex.kt new file mode 100644 index 0000000000..2ac92a3fd8 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V317_AddMessageThreadDateReceivedUnreadIndex.kt @@ -0,0 +1,16 @@ +package org.thoughtcrime.securesms.database.helpers.migration + +import android.app.Application +import org.thoughtcrime.securesms.database.SQLiteDatabase + +/** + * Adds a partial index on message (thread_id, date_received) containing only unread/unseen rows. This lets the mark-thread-read query in + * MessageTable.setMessagesReadSince seek straight to the relevant rows instead of scanning the whole thread. + */ +@Suppress("ClassName") +object V317_AddMessageThreadDateReceivedUnreadIndex : SignalDatabaseMigration { + + override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + db.execSQL("CREATE INDEX IF NOT EXISTS message_thread_date_received_unread_index ON message (thread_id, date_received) WHERE story_type = 0 AND parent_story_id <= 0 AND (read = 0 OR reactions_unread = 1 OR votes_unread = 1)") + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java index e443946c31..ae7a82d859 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java @@ -200,9 +200,10 @@ public class ApplicationMigrations { static final int COLLAPSED_EVENTS_2 = 156; static final int KEY_TRANSPARENCY = 157; static final int RELEASE_NOTES_CHAT_SYNC = 158; + static final int READ_INDEX_DB_MIGRATION = 159; } - public static final int CURRENT_VERSION = 158; + public static final int CURRENT_VERSION = 159; /** * This *must* be called after the {@link JobManager} has been instantiated, but *before* the call @@ -929,6 +930,10 @@ public class ApplicationMigrations { jobs.put(Version.RELEASE_NOTES_CHAT_SYNC, new AccountRecordMigrationJob()); } + if (lastSeenVersion < Version.READ_INDEX_DB_MIGRATION) { + jobs.put(Version.READ_INDEX_DB_MIGRATION, new DatabaseMigrationJob()); + } + return jobs; }