From d8116c60c1c5b71a996f727131a260c9e043ffc6 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Tue, 4 Feb 2025 11:31:00 -0500 Subject: [PATCH] Add migration to repair missing FTS triggers. --- .../helpers/SignalDatabaseMigrations.kt | 6 +- .../helpers/migration/V265_FixFtsTriggers.kt | 111 ++++++++++++++++++ .../migrations/ApplicationMigrations.java | 7 +- 3 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V265_FixFtsTriggers.kt 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 4863bc9746..5003409133 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 @@ -120,6 +120,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V260_RemapQuoteAuth import org.thoughtcrime.securesms.database.helpers.migration.V261_RemapCallRingers import org.thoughtcrime.securesms.database.helpers.migration.V263_InAppPaymentsSubscriberTableRebuild import org.thoughtcrime.securesms.database.helpers.migration.V264_FixGroupAddMemberUpdate +import org.thoughtcrime.securesms.database.helpers.migration.V265_FixFtsTriggers /** * Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness. @@ -243,10 +244,11 @@ object SignalDatabaseMigrations { 261 to V261_RemapCallRingers, // V263 was originally V262, but a typo in the version mapping caused it not to be run. 263 to V263_InAppPaymentsSubscriberTableRebuild, - 264 to V264_FixGroupAddMemberUpdate + 264 to V264_FixGroupAddMemberUpdate, + 265 to V265_FixFtsTriggers ) - const val DATABASE_VERSION = 264 + const val DATABASE_VERSION = 265 @JvmStatic fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V265_FixFtsTriggers.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V265_FixFtsTriggers.kt new file mode 100644 index 0000000000..6cedfe0b38 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V265_FixFtsTriggers.kt @@ -0,0 +1,111 @@ +package org.thoughtcrime.securesms.database.helpers.migration + +import android.app.Application +import net.zetetic.database.sqlcipher.SQLiteDatabase +import org.signal.core.util.SqlUtil +import org.signal.core.util.Stopwatch +import org.signal.core.util.logging.Log + +/** + * We've seen evidence of some users missing certain triggers. This migration checks for that, and if so, will completely tear down and rebuild the FTS. + */ +@Suppress("ClassName") +object V265_FixFtsTriggers : SignalDatabaseMigration { + + private val TAG = Log.tag(V265_FixFtsTriggers::class) + + private const val FTS_TABLE_NAME = "message_fts" + + private val REQUIRED_TRIGGERS = listOf( + "message_ai", + "message_ad", + "message_au" + ) + + override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + val stopwatch = Stopwatch("migration") + + val hasAllTriggers = REQUIRED_TRIGGERS.all { db.triggerExists(it) } + if (hasAllTriggers) { + Log.d(TAG, "Already have all triggers, no need for corrective action.") + return + } + stopwatch.split("precheck") + + Log.w(TAG, "We're missing some triggers! Tearing everything down and rebuilding it.") + + try { + db.execSQL("DROP TABLE IF EXISTS $FTS_TABLE_NAME") + } catch (e: Throwable) { + Log.w(TAG, "Failed to drop the message_fts table! Trying a different way.") + db.safeDropFtsTable() + } + + db.execSQL("DROP TRIGGER IF EXISTS message_ai") + db.execSQL("DROP TRIGGER IF EXISTS message_ad") + db.execSQL("DROP TRIGGER IF EXISTS message_au") + stopwatch.split("drop") + + db.execSQL("""CREATE VIRTUAL TABLE $FTS_TABLE_NAME USING fts5(body, thread_id UNINDEXED, content=message, content_rowid=_id, tokenize = "unicode61 categories 'L* N* Co Sc So'")""") + + db.execSQL("INSERT INTO message_fts(message_fts) VALUES ('rebuild')") + + db.execSQL( + """ + CREATE TRIGGER message_ai AFTER INSERT ON message BEGIN + INSERT INTO message_fts(rowid, body, thread_id) VALUES (new._id, new.body, new.thread_id); + END; + """ + ) + + db.execSQL( + """ + CREATE TRIGGER message_ad AFTER DELETE ON message BEGIN + INSERT INTO message_fts(message_fts, rowid, body, thread_id) VALUES ('delete', old._id, old.body, old.thread_id); + END; + """ + ) + + db.execSQL( + """ + CREATE TRIGGER message_au AFTER UPDATE ON message BEGIN + INSERT INTO message_fts(message_fts, rowid, body, thread_id) VALUES('delete', old._id, old.body, old.thread_id); + INSERT INTO message_fts(rowid, body, thread_id) VALUES (new._id, new.body, new.thread_id); + END; + """ + ) + + stopwatch.split("rebuild") + stopwatch.stop(TAG) + } + + /** + * Due to issues we've had in the past, the delete sequence here is very particular. It mimics the "safe drop" process in the SQLite source code + * that prevents weird vtable constructor issues when dropping potentially-corrupt tables. https://sqlite.org/src/info/4db9258a78?ln=1549-1592 + */ + private fun SQLiteDatabase.safeDropFtsTable() { + if (SqlUtil.tableExists(this, FTS_TABLE_NAME)) { + val dataExists = SqlUtil.tableExists(this, "${FTS_TABLE_NAME}_data") + val configExists = SqlUtil.tableExists(this, "${FTS_TABLE_NAME}_config") + + if (dataExists) this.execSQL("DELETE FROM ${FTS_TABLE_NAME}_data") + if (configExists) this.execSQL("DELETE FROM ${FTS_TABLE_NAME}_config") + if (dataExists) this.execSQL("INSERT INTO ${FTS_TABLE_NAME}_data VALUES(10, X'0000000000')") + if (configExists) this.execSQL("INSERT INTO ${FTS_TABLE_NAME}_config VALUES('version', 4)") + + this.execSQL("DROP TABLE $FTS_TABLE_NAME") + } + } + + private fun SQLiteDatabase.triggerExists(tableName: String): Boolean { + this.query("SELECT name FROM sqlite_master WHERE type=? AND name=?", arrayOf("trigger", tableName)).use { cursor -> + return cursor.moveToFirst() + } + } + + private fun SQLiteDatabase.tableExists(table: String): Boolean { + this.query("SELECT name FROM sqlite_master WHERE type=? AND name=?", arrayOf("table", table)).use { cursor -> + return cursor.moveToFirst() + } + } +} 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 61a4dbc47f..3cc5a4fcee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java @@ -170,9 +170,10 @@ public class ApplicationMigrations { static final int SSRE2_CAPABILITY = 126; // static final int FIX_INACTIVE_GROUPS = 127; static final int DUPLICATE_E164_FIX = 128; + static final int FTS_TRIGGER_FIX = 129; } - public static final int CURRENT_VERSION = 128; + public static final int CURRENT_VERSION = 129; /** * This *must* be called after the {@link JobManager} has been instantiated, but *before* the call @@ -783,6 +784,10 @@ public class ApplicationMigrations { jobs.put(Version.DUPLICATE_E164_FIX, new DuplicateE164MigrationJob()); } + if (lastSeenVersion < Version.FTS_TRIGGER_FIX) { + jobs.put(Version.FTS_TRIGGER_FIX, new DatabaseMigrationJob()); + } + return jobs; }