From 0605cc0a9caa6dbc5a44e9411bf5936eb0fe5fcd Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Fri, 6 Mar 2026 14:56:15 -0500 Subject: [PATCH] Update delete column migration to use a single insert. --- .../migration/V302_AddDeletedByColumn.kt | 281 ++++++++---------- 1 file changed, 131 insertions(+), 150 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V302_AddDeletedByColumn.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V302_AddDeletedByColumn.kt index c4cc008328..270a376cb2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V302_AddDeletedByColumn.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V302_AddDeletedByColumn.kt @@ -9,13 +9,13 @@ import org.signal.core.util.requireNonNullString import org.thoughtcrime.securesms.database.SQLiteDatabase /** - * Adds column to messages to track who has deleted a given message. Because of an - * OOM crash, we rebuild the table manually in batches instead of using the drop column syntax. + * Adds column to messages to track who has deleted a given message. We rebuild the table + * manually instead of using ALTER TABLE to drop the column, which previously caused OOM crashes. */ @Suppress("ClassName") object V302_AddDeletedByColumn : SignalDatabaseMigration { - private val TAG = Log.tag(V302_AddDeletedByColumn::class.java) + private val TAG = Log.tag(V302_AddDeletedByColumn::class) override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { if (SqlUtil.columnExists(db, "message", "deleted_by")) { @@ -33,80 +33,140 @@ object V302_AddDeletedByColumn : SignalDatabaseMigration { } stopwatch.split("drop-dependents") - db.execSQL("ALTER TABLE message ADD COLUMN deleted_by INTEGER DEFAULT NULL REFERENCES recipient (_id) ON DELETE CASCADE") - stopwatch.split("add-column") - - db.execSQL("UPDATE message SET deleted_by = from_recipient_id WHERE remote_deleted > 0") - stopwatch.split("update-data") - db.execSQL( """ - CREATE TABLE message_tmp ( - _id INTEGER PRIMARY KEY AUTOINCREMENT, - date_sent INTEGER NOT NULL, - date_received INTEGER NOT NULL, - date_server INTEGER DEFAULT -1, - thread_id INTEGER NOT NULL REFERENCES thread (_id) ON DELETE CASCADE, - from_recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE, - from_device_id INTEGER, - to_recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE, - type INTEGER NOT NULL, - body TEXT, - read INTEGER DEFAULT 0, - ct_l TEXT, - exp INTEGER, - m_type INTEGER, - m_size INTEGER, - st INTEGER, - tr_id TEXT, - subscription_id INTEGER DEFAULT -1, - receipt_timestamp INTEGER DEFAULT -1, - has_delivery_receipt INTEGER DEFAULT 0, - has_read_receipt INTEGER DEFAULT 0, - viewed INTEGER DEFAULT 0, - mismatched_identities TEXT DEFAULT NULL, - network_failures TEXT DEFAULT NULL, - expires_in INTEGER DEFAULT 0, - expire_started INTEGER DEFAULT 0, - notified INTEGER DEFAULT 0, - quote_id INTEGER DEFAULT 0, - quote_author INTEGER DEFAULT 0, - quote_body TEXT DEFAULT NULL, - quote_missing INTEGER DEFAULT 0, - quote_mentions BLOB DEFAULT NULL, - quote_type INTEGER DEFAULT 0, - shared_contacts TEXT DEFAULT NULL, - unidentified INTEGER DEFAULT 0, - link_previews TEXT DEFAULT NULL, - view_once INTEGER DEFAULT 0, - reactions_unread INTEGER DEFAULT 0, - reactions_last_seen INTEGER DEFAULT -1, - mentions_self INTEGER DEFAULT 0, - notified_timestamp INTEGER DEFAULT 0, - server_guid TEXT DEFAULT NULL, - message_ranges BLOB DEFAULT NULL, - story_type INTEGER DEFAULT 0, - parent_story_id INTEGER DEFAULT 0, - export_state BLOB DEFAULT NULL, - exported INTEGER DEFAULT 0, - scheduled_date INTEGER DEFAULT -1, - latest_revision_id INTEGER DEFAULT NULL REFERENCES message (_id) ON DELETE CASCADE, - original_message_id INTEGER DEFAULT NULL REFERENCES message (_id) ON DELETE CASCADE, - revision_number INTEGER DEFAULT 0, - message_extras BLOB DEFAULT NULL, - expire_timer_version INTEGER DEFAULT 1 NOT NULL, - votes_unread INTEGER DEFAULT 0, - votes_last_seen INTEGER DEFAULT 0, - pinned_until INTEGER DEFAULT 0, - pinning_message_id INTEGER DEFAULT 0, - pinned_at INTEGER DEFAULT 0, - deleted_by INTEGER DEFAULT NULL REFERENCES recipient (_id) ON DELETE CASCADE - ) + CREATE TABLE message_tmp ( + _id INTEGER PRIMARY KEY AUTOINCREMENT, + date_sent INTEGER NOT NULL, + date_received INTEGER NOT NULL, + date_server INTEGER DEFAULT -1, + thread_id INTEGER NOT NULL REFERENCES thread (_id) ON DELETE CASCADE, + from_recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE, + from_device_id INTEGER, + to_recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE, + type INTEGER NOT NULL, + body TEXT, + read INTEGER DEFAULT 0, + ct_l TEXT, + exp INTEGER, + m_type INTEGER, + m_size INTEGER, + st INTEGER, + tr_id TEXT, + subscription_id INTEGER DEFAULT -1, + receipt_timestamp INTEGER DEFAULT -1, + has_delivery_receipt INTEGER DEFAULT 0, + has_read_receipt INTEGER DEFAULT 0, + viewed INTEGER DEFAULT 0, + mismatched_identities TEXT DEFAULT NULL, + network_failures TEXT DEFAULT NULL, + expires_in INTEGER DEFAULT 0, + expire_started INTEGER DEFAULT 0, + notified INTEGER DEFAULT 0, + quote_id INTEGER DEFAULT 0, + quote_author INTEGER DEFAULT 0, + quote_body TEXT DEFAULT NULL, + quote_missing INTEGER DEFAULT 0, + quote_mentions BLOB DEFAULT NULL, + quote_type INTEGER DEFAULT 0, + shared_contacts TEXT DEFAULT NULL, + unidentified INTEGER DEFAULT 0, + link_previews TEXT DEFAULT NULL, + view_once INTEGER DEFAULT 0, + reactions_unread INTEGER DEFAULT 0, + reactions_last_seen INTEGER DEFAULT -1, + mentions_self INTEGER DEFAULT 0, + notified_timestamp INTEGER DEFAULT 0, + server_guid TEXT DEFAULT NULL, + message_ranges BLOB DEFAULT NULL, + story_type INTEGER DEFAULT 0, + parent_story_id INTEGER DEFAULT 0, + export_state BLOB DEFAULT NULL, + exported INTEGER DEFAULT 0, + scheduled_date INTEGER DEFAULT -1, + latest_revision_id INTEGER DEFAULT NULL REFERENCES message (_id) ON DELETE CASCADE, + original_message_id INTEGER DEFAULT NULL REFERENCES message (_id) ON DELETE CASCADE, + revision_number INTEGER DEFAULT 0, + message_extras BLOB DEFAULT NULL, + expire_timer_version INTEGER DEFAULT 1 NOT NULL, + votes_unread INTEGER DEFAULT 0, + votes_last_seen INTEGER DEFAULT 0, + pinned_until INTEGER DEFAULT 0, + pinning_message_id INTEGER DEFAULT 0, + pinned_at INTEGER DEFAULT 0, + deleted_by INTEGER DEFAULT NULL REFERENCES recipient (_id) ON DELETE CASCADE + ) """ ) stopwatch.split("create-table") - copyMessages(db) + db.execSQL( + """ + INSERT INTO message_tmp + SELECT + _id, + date_sent, + date_received, + date_server, + thread_id, + from_recipient_id, + from_device_id, + to_recipient_id, + type, + body, + read, + ct_l, + exp, + m_type, + m_size, + st, + tr_id, + subscription_id, + receipt_timestamp, + has_delivery_receipt, + has_read_receipt, + viewed, + mismatched_identities, + network_failures, + expires_in, + expire_started, + notified, + quote_id, + quote_author, + quote_body, + quote_missing, + quote_mentions, + quote_type, + shared_contacts, + unidentified, + link_previews, + view_once, + reactions_unread, + reactions_last_seen, + mentions_self, + notified_timestamp, + server_guid, + message_ranges, + story_type, + parent_story_id, + export_state, + exported, + scheduled_date, + latest_revision_id, + original_message_id, + revision_number, + message_extras, + expire_timer_version, + votes_unread, + votes_last_seen, + pinned_until, + pinning_message_id, + pinned_at, + CASE WHEN remote_deleted > 0 THEN from_recipient_id ELSE NULL END AS deleted_by + FROM + message + """ + ) stopwatch.split("copy-data") db.execSQL("DROP TABLE message") @@ -135,85 +195,6 @@ object V302_AddDeletedByColumn : SignalDatabaseMigration { stopwatch.stop(TAG) } - private fun copyMessages(db: SQLiteDatabase) { - val batchSize = 50_000L - - val maxId = SqlUtil.getNextAutoIncrementId(db, "message") - - for (i in 1..maxId step batchSize) { - db.execSQL( - """ - INSERT INTO message_tmp - SELECT - _id, - date_sent, - date_received, - date_server, - thread_id, - from_recipient_id, - from_device_id, - to_recipient_id, - type, - body, - read, - ct_l, - exp, - m_type, - m_size, - st, - tr_id, - subscription_id, - receipt_timestamp, - has_delivery_receipt, - has_read_receipt, - viewed, - mismatched_identities, - network_failures, - expires_in, - expire_started, - notified, - quote_id, - quote_author, - quote_body, - quote_missing, - quote_mentions, - quote_type, - shared_contacts, - unidentified, - link_previews, - view_once, - reactions_unread, - reactions_last_seen, - mentions_self, - notified_timestamp, - server_guid, - message_ranges, - story_type, - parent_story_id, - export_state, - exported, - scheduled_date, - latest_revision_id, - original_message_id, - revision_number, - message_extras, - expire_timer_version, - votes_unread, - votes_last_seen, - pinned_until, - pinning_message_id, - pinned_at, - deleted_by - FROM - message - WHERE - _id >= $i AND - _id < ${i + batchSize} - """ - ) - } - } - private fun getAllDependentItems(db: SQLiteDatabase, tableName: String): List { return db.rawQuery("SELECT type, name, sql FROM sqlite_schema WHERE tbl_name='$tableName' AND type != 'table'").readToList { cursor -> SqlItem(