From 2bb9578ef94aa6f11567b995581a15c3ad0d3dfc Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Mon, 23 Mar 2026 13:04:33 -0400 Subject: [PATCH] Use sqlite-jdbc for unit tests to enable FTS5 and JSON1 support. --- app/build.gradle.kts | 1 + .../database/DatabaseConsistencyTest.kt | 541 ------------------ .../database/DatabaseConsistencyTest.kt | 463 ++++++++++++++- .../database/SqliteCapabilityTest.kt | 34 ++ .../securesms/testing/JdbcSqliteDatabase.kt | 427 ++++++++++++++ .../testutil/SignalDatabaseMigrationRule.kt | 27 +- .../securesms/testutil/SignalDatabaseRule.kt | 27 +- gradle/test-libs.versions.toml | 1 + gradle/verification-metadata.xml | 10 + 9 files changed, 939 insertions(+), 592 deletions(-) delete mode 100644 app/src/androidTest/java/org/thoughtcrime/securesms/database/DatabaseConsistencyTest.kt create mode 100644 app/src/test/java/org/thoughtcrime/securesms/database/SqliteCapabilityTest.kt create mode 100644 app/src/test/java/org/thoughtcrime/securesms/testing/JdbcSqliteDatabase.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 79f0f0f67e..3b053b1526 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -750,6 +750,7 @@ dependencies { testImplementation(testFixtures(project(":lib:libsignal-service"))) testImplementation(testLibs.espresso.core) testImplementation(testLibs.kotlinx.coroutines.test) + testImplementation(testLibs.sqlite.jdbc) testImplementation(libs.androidx.compose.ui.test.junit4) "perfImplementation"(libs.androidx.compose.ui.test.manifest) diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/DatabaseConsistencyTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/database/DatabaseConsistencyTest.kt deleted file mode 100644 index 1ae012515f..0000000000 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/DatabaseConsistencyTest.kt +++ /dev/null @@ -1,541 +0,0 @@ -package org.thoughtcrime.securesms.database - -import android.app.Application -import androidx.test.ext.junit.runners.AndroidJUnit4 -import net.zetetic.database.sqlcipher.SQLiteDatabase -import net.zetetic.database.sqlcipher.SQLiteOpenHelper -import org.junit.Assert.assertTrue -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.signal.core.util.ForeignKeyConstraint -import org.signal.core.util.Index -import org.signal.core.util.getForeignKeys -import org.signal.core.util.getIndexes -import org.signal.core.util.readToList -import org.signal.core.util.requireNonNullString -import org.thoughtcrime.securesms.database.helpers.SignalDatabaseMigrations -import org.thoughtcrime.securesms.dependencies.AppDependencies -import org.thoughtcrime.securesms.testing.SignalActivityRule -import org.thoughtcrime.securesms.database.SQLiteDatabase as SignalSQLiteDatabase - -/** - * A test that guarantees that a freshly-created database looks the same as one that went through the upgrade path. - */ -@RunWith(AndroidJUnit4::class) -class DatabaseConsistencyTest { - - @get:Rule - val harness = SignalActivityRule() - - @Test - fun testUpgradeConsistency() { - val currentVersionStatements = SignalDatabase.rawDatabase.getAllCreateStatements() - val testHelper = InMemoryTestHelper(AppDependencies.application).also { - it.onUpgrade(it.writableDatabase, 181, SignalDatabaseMigrations.DATABASE_VERSION) - } - - val upgradedStatements = testHelper.readableDatabase.getAllCreateStatements() - - if (currentVersionStatements != upgradedStatements) { - var message = "\n" - - val currentByName = currentVersionStatements.associateBy { it.name } - val upgradedByName = upgradedStatements.associateBy { it.name } - - if (currentByName.keys != upgradedByName.keys) { - val exclusiveToCurrent = currentByName.keys - upgradedByName.keys - val exclusiveToUpgrade = upgradedByName.keys - currentByName.keys - - message += "SQL entities exclusive to the newly-created database: $exclusiveToCurrent\n" - message += "SQL entities exclusive to the upgraded database: $exclusiveToUpgrade\n\n" - } else { - for (currentEntry in currentByName) { - val upgradedValue: Statement = upgradedByName[currentEntry.key]!! - if (upgradedValue.sql != currentEntry.value.sql) { - message += "Statement differed:\n" - message += "newly-created:\n" - message += "${currentEntry.value.sql}\n\n" - message += "upgraded:\n" - message += "${upgradedValue.sql}\n\n" - } - } - } - - assertTrue(message, false) - } - } - - @Test - fun testForeignKeyIndexCoverage() { - /** We may deem certain indexes non-critical if deletion frequency is low or table size is small. */ - val ignoredColumns: List> = listOf( - StorySendTable.TABLE_NAME to StorySendTable.DISTRIBUTION_ID - ) - - val foreignKeys: List = SignalDatabase.rawDatabase.getForeignKeys() - val indexesByFirstColumn: List = SignalDatabase.rawDatabase.getIndexes() - - val notFound: List> = foreignKeys - .filterNot { ignoredColumns.contains(it.table to it.column) } - .filterNot { foreignKey -> - indexesByFirstColumn.hasPrimaryIndexFor(foreignKey.table, foreignKey.column) - } - .map { it.table to it.column } - - assertTrue("Missing indexes to cover: $notFound", notFound.isEmpty()) - } - - private fun List.hasPrimaryIndexFor(table: String, column: String): Boolean { - return this.any { index -> index.table == table && index.columns[0] == column } - } - - private data class Statement( - val name: String, - val sql: String - ) - - private fun SQLiteDatabase.getAllCreateStatements(): List { - return this.rawQuery("SELECT name, sql FROM sqlite_schema WHERE sql NOT NULL AND name != 'sqlite_sequence'") - .readToList { cursor -> - Statement( - name = cursor.requireNonNullString("name"), - sql = cursor.requireNonNullString("sql").normalizeSql() - ) - } - .filterNot { it.name.startsWith("sqlite_stat") } - .sortedBy { it.name } - } - - private fun String.normalizeSql(): String { - return this - .split("\n") - .map { it.trim() } - .joinToString(separator = " ") - .replace(Regex.fromLiteral(" ,"), ",") - .replace(",([^\\s])".toRegex(), ", $1") - .replace(Regex("\\s+"), " ") - .replace(Regex.fromLiteral("( "), "(") - .replace(Regex.fromLiteral(" )"), ")") - .replace(Regex("CREATE TABLE \"([a-zA-Z_]+)\""), "CREATE TABLE $1") // for some reason SQLite will wrap table names in quotes for upgraded tables. This unwraps them. - } - - private class InMemoryTestHelper(private val application: Application) : SQLiteOpenHelper(application, null, null, 1) { - - override fun onCreate(db: SQLiteDatabase) { - for (statement in SNAPSHOT_V181) { - db.execSQL(statement.sql) - } - } - - override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { - SignalDatabaseMigrations.migrate(application, SignalSQLiteDatabase(db), 181, SignalDatabaseMigrations.DATABASE_VERSION) - } - - /** - * This is the list of statements that existed at version 181. Never change this. - */ - private val SNAPSHOT_V181 = listOf( - Statement( - name = "message", - sql = "CREATE TABLE message (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n date_sent INTEGER NOT NULL,\n date_received INTEGER NOT NULL,\n date_server INTEGER DEFAULT -1,\n thread_id INTEGER NOT NULL REFERENCES thread (_id) ON DELETE CASCADE,\n recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,\n recipient_device_id INTEGER,\n type INTEGER NOT NULL,\n body TEXT,\n read INTEGER DEFAULT 0,\n ct_l TEXT,\n exp INTEGER,\n m_type INTEGER,\n m_size INTEGER,\n st INTEGER,\n tr_id TEXT,\n subscription_id INTEGER DEFAULT -1, \n receipt_timestamp INTEGER DEFAULT -1, \n delivery_receipt_count INTEGER DEFAULT 0, \n read_receipt_count INTEGER DEFAULT 0, \n viewed_receipt_count INTEGER DEFAULT 0,\n mismatched_identities TEXT DEFAULT NULL,\n network_failures TEXT DEFAULT NULL,\n expires_in INTEGER DEFAULT 0,\n expire_started INTEGER DEFAULT 0,\n notified INTEGER DEFAULT 0,\n quote_id INTEGER DEFAULT 0,\n quote_author INTEGER DEFAULT 0,\n quote_body TEXT DEFAULT NULL,\n quote_missing INTEGER DEFAULT 0,\n quote_mentions BLOB DEFAULT NULL,\n quote_type INTEGER DEFAULT 0,\n shared_contacts TEXT DEFAULT NULL,\n unidentified INTEGER DEFAULT 0,\n link_previews TEXT DEFAULT NULL,\n view_once INTEGER DEFAULT 0,\n reactions_unread INTEGER DEFAULT 0,\n reactions_last_seen INTEGER DEFAULT -1,\n remote_deleted INTEGER DEFAULT 0,\n mentions_self INTEGER DEFAULT 0,\n notified_timestamp INTEGER DEFAULT 0,\n server_guid TEXT DEFAULT NULL,\n message_ranges BLOB DEFAULT NULL,\n story_type INTEGER DEFAULT 0,\n parent_story_id INTEGER DEFAULT 0,\n export_state BLOB DEFAULT NULL,\n exported INTEGER DEFAULT 0,\n scheduled_date INTEGER DEFAULT -1\n )" - ), - Statement( - name = "part", - sql = "CREATE TABLE part (_id INTEGER PRIMARY KEY, mid INTEGER, seq INTEGER DEFAULT 0, ct TEXT, name TEXT, chset INTEGER, cd TEXT, fn TEXT, cid TEXT, cl TEXT, ctt_s INTEGER, ctt_t TEXT, encrypted INTEGER, pending_push INTEGER, _data TEXT, data_size INTEGER, file_name TEXT, unique_id INTEGER NOT NULL, digest BLOB, fast_preflight_id TEXT, voice_note INTEGER DEFAULT 0, borderless INTEGER DEFAULT 0, video_gif INTEGER DEFAULT 0, data_random BLOB, quote INTEGER DEFAULT 0, width INTEGER DEFAULT 0, height INTEGER DEFAULT 0, caption TEXT DEFAULT NULL, sticker_pack_id TEXT DEFAULT NULL, sticker_pack_key DEFAULT NULL, sticker_id INTEGER DEFAULT -1, sticker_emoji STRING DEFAULT NULL, data_hash TEXT DEFAULT NULL, blur_hash TEXT DEFAULT NULL, transform_properties TEXT DEFAULT NULL, transfer_file TEXT DEFAULT NULL, display_order INTEGER DEFAULT 0, upload_timestamp INTEGER DEFAULT 0, cdn_number INTEGER DEFAULT 0)" - ), - Statement( - name = "thread", - sql = "CREATE TABLE thread (\n _id INTEGER PRIMARY KEY AUTOINCREMENT, \n date INTEGER DEFAULT 0, \n meaningful_messages INTEGER DEFAULT 0,\n recipient_id INTEGER NOT NULL UNIQUE REFERENCES recipient (_id) ON DELETE CASCADE,\n read INTEGER DEFAULT 1, \n type INTEGER DEFAULT 0, \n error INTEGER DEFAULT 0, \n snippet TEXT, \n snippet_type INTEGER DEFAULT 0, \n snippet_uri TEXT DEFAULT NULL, \n snippet_content_type TEXT DEFAULT NULL, \n snippet_extras TEXT DEFAULT NULL, \n unread_count INTEGER DEFAULT 0, \n archived INTEGER DEFAULT 0, \n status INTEGER DEFAULT 0, \n delivery_receipt_count INTEGER DEFAULT 0, \n read_receipt_count INTEGER DEFAULT 0, \n expires_in INTEGER DEFAULT 0, \n last_seen INTEGER DEFAULT 0, \n has_sent INTEGER DEFAULT 0, \n last_scrolled INTEGER DEFAULT 0, \n pinned INTEGER DEFAULT 0, \n unread_self_mention_count INTEGER DEFAULT 0\n)" - ), - Statement( - name = "identities", - sql = "CREATE TABLE identities (\n _id INTEGER PRIMARY KEY AUTOINCREMENT, \n address INTEGER UNIQUE, \n identity_key TEXT, \n first_use INTEGER DEFAULT 0, \n timestamp INTEGER DEFAULT 0, \n verified INTEGER DEFAULT 0, \n nonblocking_approval INTEGER DEFAULT 0\n )" - ), - Statement( - name = "drafts", - sql = "CREATE TABLE drafts (\n _id INTEGER PRIMARY KEY, \n thread_id INTEGER, \n type TEXT, \n value TEXT\n )" - ), - Statement( - name = "push", - sql = "CREATE TABLE push (_id INTEGER PRIMARY KEY, type INTEGER, source TEXT, source_uuid TEXT, device_id INTEGER, body TEXT, content TEXT, timestamp INTEGER, server_timestamp INTEGER DEFAULT 0, server_delivered_timestamp INTEGER DEFAULT 0, server_guid TEXT DEFAULT NULL)" - ), - Statement( - name = "groups", - sql = "CREATE TABLE groups (\n _id INTEGER PRIMARY KEY, \n group_id TEXT, \n recipient_id INTEGER,\n title TEXT,\n avatar_id INTEGER, \n avatar_key BLOB,\n avatar_content_type TEXT, \n avatar_relay TEXT,\n timestamp INTEGER,\n active INTEGER DEFAULT 1,\n avatar_digest BLOB, \n mms INTEGER DEFAULT 0, \n master_key BLOB, \n revision BLOB, \n decrypted_group BLOB, \n expected_v2_id TEXT DEFAULT NULL, \n former_v1_members TEXT DEFAULT NULL, \n distribution_id TEXT DEFAULT NULL, \n display_as_story INTEGER DEFAULT 0, \n auth_service_id TEXT DEFAULT NULL, \n last_force_update_timestamp INTEGER DEFAULT 0\n )" - ), - Statement( - name = "group_membership", - sql = "CREATE TABLE group_membership ( _id INTEGER PRIMARY KEY, group_id TEXT NOT NULL, recipient_id INTEGER NOT NULL, UNIQUE(group_id, recipient_id) )" - ), - Statement( - name = "recipient", - sql = "CREATE TABLE recipient (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n uuid TEXT UNIQUE DEFAULT NULL,\n username TEXT UNIQUE DEFAULT NULL,\n phone TEXT UNIQUE DEFAULT NULL,\n email TEXT UNIQUE DEFAULT NULL,\n group_id TEXT UNIQUE DEFAULT NULL,\n group_type INTEGER DEFAULT 0,\n blocked INTEGER DEFAULT 0,\n message_ringtone TEXT DEFAULT NULL, \n message_vibrate INTEGER DEFAULT 0, \n call_ringtone TEXT DEFAULT NULL, \n call_vibrate INTEGER DEFAULT 0, \n notification_channel TEXT DEFAULT NULL, \n mute_until INTEGER DEFAULT 0, \n color TEXT DEFAULT NULL, \n seen_invite_reminder INTEGER DEFAULT 0,\n default_subscription_id INTEGER DEFAULT -1,\n message_expiration_time INTEGER DEFAULT 0,\n registered INTEGER DEFAULT 0,\n system_given_name TEXT DEFAULT NULL, \n system_family_name TEXT DEFAULT NULL, \n system_display_name TEXT DEFAULT NULL, \n system_photo_uri TEXT DEFAULT NULL, \n system_phone_label TEXT DEFAULT NULL, \n system_phone_type INTEGER DEFAULT -1, \n system_contact_uri TEXT DEFAULT NULL, \n system_info_pending INTEGER DEFAULT 0, \n profile_key TEXT DEFAULT NULL, \n profile_key_credential TEXT DEFAULT NULL, \n signal_profile_name TEXT DEFAULT NULL, \n profile_family_name TEXT DEFAULT NULL, \n profile_joined_name TEXT DEFAULT NULL, \n signal_profile_avatar TEXT DEFAULT NULL, \n profile_sharing INTEGER DEFAULT 0, \n last_profile_fetch INTEGER DEFAULT 0, \n unidentified_access_mode INTEGER DEFAULT 0, \n force_sms_selection INTEGER DEFAULT 0, \n storage_service_key TEXT UNIQUE DEFAULT NULL, \n mention_setting INTEGER DEFAULT 0, \n storage_proto TEXT DEFAULT NULL,\n capabilities INTEGER DEFAULT 0,\n last_session_reset BLOB DEFAULT NULL,\n wallpaper BLOB DEFAULT NULL,\n wallpaper_file TEXT DEFAULT NULL,\n about TEXT DEFAULT NULL,\n about_emoji TEXT DEFAULT NULL,\n extras BLOB DEFAULT NULL,\n groups_in_common INTEGER DEFAULT 0,\n chat_colors BLOB DEFAULT NULL,\n custom_chat_colors_id INTEGER DEFAULT 0,\n badges BLOB DEFAULT NULL,\n pni TEXT DEFAULT NULL,\n distribution_list_id INTEGER DEFAULT NULL,\n needs_pni_signature INTEGER DEFAULT 0,\n unregistered_timestamp INTEGER DEFAULT 0,\n hidden INTEGER DEFAULT 0,\n reporting_token BLOB DEFAULT NULL,\n system_nickname TEXT DEFAULT NULL\n)" - ), - Statement( - name = "group_receipts", - sql = "CREATE TABLE group_receipts (\n _id INTEGER PRIMARY KEY, \n mms_id INTEGER, \n address INTEGER, \n status INTEGER, \n timestamp INTEGER, \n unidentified INTEGER DEFAULT 0\n )" - ), - Statement( - name = "one_time_prekeys", - sql = "CREATE TABLE one_time_prekeys (\n _id INTEGER PRIMARY KEY,\n account_id TEXT NOT NULL,\n key_id INTEGER UNIQUE, \n public_key TEXT NOT NULL, \n private_key TEXT NOT NULL,\n UNIQUE(account_id, key_id)\n )" - ), - Statement( - name = "signed_prekeys", - sql = "CREATE TABLE signed_prekeys (\n _id INTEGER PRIMARY KEY,\n account_id TEXT NOT NULL,\n key_id INTEGER UNIQUE, \n public_key TEXT NOT NULL,\n private_key TEXT NOT NULL,\n signature TEXT NOT NULL, \n timestamp INTEGER DEFAULT 0,\n UNIQUE(account_id, key_id)\n )" - ), - Statement( - name = "sessions", - sql = "CREATE TABLE sessions (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n account_id TEXT NOT NULL,\n address TEXT NOT NULL,\n device INTEGER NOT NULL,\n record BLOB NOT NULL,\n UNIQUE(account_id, address, device)\n )" - ), - Statement( - name = "sender_keys", - sql = "CREATE TABLE sender_keys (\n _id INTEGER PRIMARY KEY AUTOINCREMENT, \n address TEXT NOT NULL, \n device INTEGER NOT NULL, \n distribution_id TEXT NOT NULL,\n record BLOB NOT NULL, \n created_at INTEGER NOT NULL, \n UNIQUE(address,device, distribution_id) ON CONFLICT REPLACE\n )" - ), - Statement( - name = "sender_key_shared", - sql = "CREATE TABLE sender_key_shared (\n _id INTEGER PRIMARY KEY AUTOINCREMENT, \n distribution_id TEXT NOT NULL, \n address TEXT NOT NULL, \n device INTEGER NOT NULL, \n timestamp INTEGER DEFAULT 0, \n UNIQUE(distribution_id,address, device) ON CONFLICT REPLACE\n )" - ), - Statement( - name = "pending_retry_receipts", - sql = "CREATE TABLE pending_retry_receipts(_id INTEGER PRIMARY KEY AUTOINCREMENT, author TEXT NOT NULL, device INTEGER NOT NULL, sent_timestamp INTEGER NOT NULL, received_timestamp TEXT NOT NULL, thread_id INTEGER NOT NULL, UNIQUE(author,sent_timestamp) ON CONFLICT REPLACE)" - ), - Statement( - name = "sticker", - sql = "CREATE TABLE sticker (_id INTEGER PRIMARY KEY AUTOINCREMENT, pack_id TEXT NOT NULL, pack_key TEXT NOT NULL, pack_title TEXT NOT NULL, pack_author TEXT NOT NULL, sticker_id INTEGER, cover INTEGER, pack_order INTEGER, emoji TEXT NOT NULL, content_type TEXT DEFAULT NULL, last_used INTEGER, installed INTEGER,file_path TEXT NOT NULL, file_length INTEGER, file_random BLOB, UNIQUE(pack_id, sticker_id, cover) ON CONFLICT IGNORE)" - ), - Statement( - name = "storage_key", - sql = "CREATE TABLE storage_key (_id INTEGER PRIMARY KEY AUTOINCREMENT, type INTEGER, key TEXT UNIQUE)" - ), - Statement( - name = "mention", - sql = "CREATE TABLE mention(_id INTEGER PRIMARY KEY AUTOINCREMENT, thread_id INTEGER, message_id INTEGER, recipient_id INTEGER, range_start INTEGER, range_length INTEGER)" - ), - Statement( - name = "payments", - sql = "CREATE TABLE payments(_id INTEGER PRIMARY KEY, uuid TEXT DEFAULT NULL, recipient INTEGER DEFAULT 0, recipient_address TEXT DEFAULT NULL, timestamp INTEGER, note TEXT DEFAULT NULL, direction INTEGER, state INTEGER, failure_reason INTEGER, amount BLOB NOT NULL, fee BLOB NOT NULL, transaction_record BLOB DEFAULT NULL, receipt BLOB DEFAULT NULL, payment_metadata BLOB DEFAULT NULL, receipt_public_key TEXT DEFAULT NULL, block_index INTEGER DEFAULT 0, block_timestamp INTEGER DEFAULT 0, seen INTEGER, UNIQUE(uuid) ON CONFLICT ABORT)" - ), - Statement( - name = "chat_colors", - sql = "CREATE TABLE chat_colors (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n chat_colors BLOB\n)" - ), - Statement( - name = "emoji_search", - sql = "CREATE TABLE emoji_search (\n _id INTEGER PRIMARY KEY,\n label TEXT NOT NULL,\n emoji TEXT NOT NULL,\n rank INTEGER DEFAULT 2147483647 \n )" - ), - Statement( - name = "avatar_picker", - sql = "CREATE TABLE avatar_picker (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n last_used INTEGER DEFAULT 0,\n group_id TEXT DEFAULT NULL,\n avatar BLOB NOT NULL\n)" - ), - Statement( - name = "group_call_ring", - sql = "CREATE TABLE group_call_ring (\n _id INTEGER PRIMARY KEY,\n ring_id INTEGER UNIQUE,\n date_received INTEGER,\n ring_state INTEGER\n)" - ), - Statement( - name = "reaction", - sql = "CREATE TABLE reaction (\n _id INTEGER PRIMARY KEY,\n message_id INTEGER NOT NULL REFERENCES message (_id) ON DELETE CASCADE,\n author_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,\n emoji TEXT NOT NULL,\n date_sent INTEGER NOT NULL,\n date_received INTEGER NOT NULL,\n UNIQUE(message_id, author_id) ON CONFLICT REPLACE\n)" - ), - Statement( - name = "donation_receipt", - sql = "CREATE TABLE donation_receipt (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n receipt_type TEXT NOT NULL,\n receipt_date INTEGER NOT NULL,\n amount TEXT NOT NULL,\n currency TEXT NOT NULL,\n subscription_level INTEGER NOT NULL\n)" - ), - Statement( - name = "story_sends", - sql = "CREATE TABLE story_sends (\n _id INTEGER PRIMARY KEY,\n message_id INTEGER NOT NULL REFERENCES message (_id) ON DELETE CASCADE,\n recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,\n sent_timestamp INTEGER NOT NULL,\n allows_replies INTEGER NOT NULL,\n distribution_id TEXT NOT NULL REFERENCES distribution_list (distribution_id) ON DELETE CASCADE\n)" - ), - Statement( - name = "cds", - sql = "CREATE TABLE cds (\n _id INTEGER PRIMARY KEY,\n e164 TEXT NOT NULL UNIQUE ON CONFLICT IGNORE,\n last_seen_at INTEGER DEFAULT 0\n )" - ), - Statement( - name = "remote_megaphone", - sql = "CREATE TABLE remote_megaphone (\n _id INTEGER PRIMARY KEY,\n uuid TEXT UNIQUE NOT NULL,\n priority INTEGER NOT NULL,\n countries TEXT,\n minimum_version INTEGER NOT NULL,\n dont_show_before INTEGER NOT NULL,\n dont_show_after INTEGER NOT NULL,\n show_for_days INTEGER NOT NULL,\n conditional_id TEXT,\n primary_action_id TEXT,\n secondary_action_id TEXT,\n image_url TEXT,\n image_uri TEXT DEFAULT NULL,\n title TEXT NOT NULL,\n body TEXT NOT NULL,\n primary_action_text TEXT,\n secondary_action_text TEXT,\n shown_at INTEGER DEFAULT 0,\n finished_at INTEGER DEFAULT 0,\n primary_action_data TEXT DEFAULT NULL,\n secondary_action_data TEXT DEFAULT NULL,\n snoozed_at INTEGER DEFAULT 0,\n seen_count INTEGER DEFAULT 0\n)" - ), - Statement( - name = "pending_pni_signature_message", - sql = "CREATE TABLE pending_pni_signature_message (\n _id INTEGER PRIMARY KEY,\n recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,\n sent_timestamp INTEGER NOT NULL,\n device_id INTEGER NOT NULL\n )" - ), - Statement( - name = "call", - sql = "CREATE TABLE call (\n _id INTEGER PRIMARY KEY,\n call_id INTEGER NOT NULL UNIQUE,\n message_id INTEGER NOT NULL REFERENCES message (_id) ON DELETE CASCADE,\n peer INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,\n type INTEGER NOT NULL,\n direction INTEGER NOT NULL,\n event INTEGER NOT NULL\n)" - ), - Statement( - name = "message_fts", - sql = "CREATE VIRTUAL TABLE message_fts USING fts5(body, thread_id UNINDEXED, content=message, content_rowid=_id)" - ), - Statement( - name = "remapped_recipients", - sql = "CREATE TABLE remapped_recipients (\n _id INTEGER PRIMARY KEY AUTOINCREMENT, \n old_id INTEGER UNIQUE, \n new_id INTEGER\n )" - ), - Statement( - name = "remapped_threads", - sql = "CREATE TABLE remapped_threads (\n _id INTEGER PRIMARY KEY AUTOINCREMENT, \n old_id INTEGER UNIQUE, \n new_id INTEGER\n )" - ), - Statement( - name = "msl_payload", - sql = "CREATE TABLE msl_payload (\n _id INTEGER PRIMARY KEY,\n date_sent INTEGER NOT NULL,\n content BLOB NOT NULL,\n content_hint INTEGER NOT NULL,\n urgent INTEGER NOT NULL DEFAULT 1\n )" - ), - Statement( - name = "msl_recipient", - sql = "CREATE TABLE msl_recipient (\n _id INTEGER PRIMARY KEY,\n payload_id INTEGER NOT NULL REFERENCES msl_payload (_id) ON DELETE CASCADE,\n recipient_id INTEGER NOT NULL, \n device INTEGER NOT NULL\n )" - ), - Statement( - name = "msl_message", - sql = "CREATE TABLE msl_message (\n _id INTEGER PRIMARY KEY,\n payload_id INTEGER NOT NULL REFERENCES msl_payload (_id) ON DELETE CASCADE,\n message_id INTEGER NOT NULL\n )" - ), - Statement( - name = "notification_profile", - sql = "CREATE TABLE notification_profile (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL UNIQUE,\n emoji TEXT NOT NULL,\n color TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n allow_all_calls INTEGER NOT NULL DEFAULT 0,\n allow_all_mentions INTEGER NOT NULL DEFAULT 0\n)" - ), - Statement( - name = "notification_profile_schedule", - sql = "CREATE TABLE notification_profile_schedule (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n notification_profile_id INTEGER NOT NULL REFERENCES notification_profile (_id) ON DELETE CASCADE,\n enabled INTEGER NOT NULL DEFAULT 0,\n start INTEGER NOT NULL,\n end INTEGER NOT NULL,\n days_enabled TEXT NOT NULL\n)" - ), - Statement( - name = "notification_profile_allowed_members", - sql = "CREATE TABLE notification_profile_allowed_members (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n notification_profile_id INTEGER NOT NULL REFERENCES notification_profile (_id) ON DELETE CASCADE,\n recipient_id INTEGER NOT NULL,\n UNIQUE(notification_profile_id, recipient_id) ON CONFLICT REPLACE\n)" - ), - Statement( - name = "distribution_list", - sql = "CREATE TABLE distribution_list (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT UNIQUE NOT NULL,\n distribution_id TEXT UNIQUE NOT NULL,\n recipient_id INTEGER UNIQUE REFERENCES recipient (_id),\n allows_replies INTEGER DEFAULT 1,\n deletion_timestamp INTEGER DEFAULT 0,\n is_unknown INTEGER DEFAULT 0,\n privacy_mode INTEGER DEFAULT 0\n )" - ), - Statement( - name = "distribution_list_member", - sql = "CREATE TABLE distribution_list_member (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n list_id INTEGER NOT NULL REFERENCES distribution_list (_id) ON DELETE CASCADE,\n recipient_id INTEGER NOT NULL REFERENCES recipient (_id),\n privacy_mode INTEGER DEFAULT 0\n )" - ), - Statement( - name = "recipient_group_type_index", - sql = "CREATE INDEX recipient_group_type_index ON recipient (group_type)" - ), - Statement( - name = "recipient_pni_index", - sql = "CREATE UNIQUE INDEX recipient_pni_index ON recipient (pni)" - ), - Statement( - name = "recipient_service_id_profile_key", - sql = "CREATE INDEX recipient_service_id_profile_key ON recipient (uuid, profile_key) WHERE uuid NOT NULL AND profile_key NOT NULL" - ), - Statement( - name = "mms_read_and_notified_and_thread_id_index", - sql = "CREATE INDEX mms_read_and_notified_and_thread_id_index ON message (read, notified, thread_id)" - ), - Statement( - name = "mms_type_index", - sql = "CREATE INDEX mms_type_index ON message (type)" - ), - Statement( - name = "mms_date_sent_index", - sql = "CREATE INDEX mms_date_sent_index ON message (date_sent, recipient_id, thread_id)" - ), - Statement( - name = "mms_date_server_index", - sql = "CREATE INDEX mms_date_server_index ON message (date_server)" - ), - Statement( - name = "mms_thread_date_index", - sql = "CREATE INDEX mms_thread_date_index ON message (thread_id, date_received)" - ), - Statement( - name = "mms_reactions_unread_index", - sql = "CREATE INDEX mms_reactions_unread_index ON message (reactions_unread)" - ), - Statement( - name = "mms_story_type_index", - sql = "CREATE INDEX mms_story_type_index ON message (story_type)" - ), - Statement( - name = "mms_parent_story_id_index", - sql = "CREATE INDEX mms_parent_story_id_index ON message (parent_story_id)" - ), - Statement( - name = "mms_thread_story_parent_story_scheduled_date_index", - sql = "CREATE INDEX mms_thread_story_parent_story_scheduled_date_index ON message (thread_id, date_received, story_type, parent_story_id, scheduled_date)" - ), - Statement( - name = "message_quote_id_quote_author_scheduled_date_index", - sql = "CREATE INDEX message_quote_id_quote_author_scheduled_date_index ON message (quote_id, quote_author, scheduled_date)" - ), - Statement( - name = "mms_exported_index", - sql = "CREATE INDEX mms_exported_index ON message (exported)" - ), - Statement( - name = "mms_id_type_payment_transactions_index", - sql = "CREATE INDEX mms_id_type_payment_transactions_index ON message (_id,type) WHERE type & 12884901888 != 0" - ), - Statement( - name = "part_mms_id_index", - sql = "CREATE INDEX part_mms_id_index ON part (mid)" - ), - Statement( - name = "pending_push_index", - sql = "CREATE INDEX pending_push_index ON part (pending_push)" - ), - Statement( - name = "part_sticker_pack_id_index", - sql = "CREATE INDEX part_sticker_pack_id_index ON part (sticker_pack_id)" - ), - Statement( - name = "part_data_hash_index", - sql = "CREATE INDEX part_data_hash_index ON part (data_hash)" - ), - Statement( - name = "part_data_index", - sql = "CREATE INDEX part_data_index ON part (_data)" - ), - Statement( - name = "thread_recipient_id_index", - sql = "CREATE INDEX thread_recipient_id_index ON thread (recipient_id)" - ), - Statement( - name = "archived_count_index", - sql = "CREATE INDEX archived_count_index ON thread (archived, meaningful_messages)" - ), - Statement( - name = "thread_pinned_index", - sql = "CREATE INDEX thread_pinned_index ON thread (pinned)" - ), - Statement( - name = "thread_read", - sql = "CREATE INDEX thread_read ON thread (read)" - ), - Statement( - name = "draft_thread_index", - sql = "CREATE INDEX draft_thread_index ON drafts (thread_id)" - ), - Statement( - name = "group_id_index", - sql = "CREATE UNIQUE INDEX group_id_index ON groups (group_id)" - ), - Statement( - name = "group_recipient_id_index", - sql = "CREATE UNIQUE INDEX group_recipient_id_index ON groups (recipient_id)" - ), - Statement( - name = "expected_v2_id_index", - sql = "CREATE UNIQUE INDEX expected_v2_id_index ON groups (expected_v2_id)" - ), - Statement( - name = "group_distribution_id_index", - sql = "CREATE UNIQUE INDEX group_distribution_id_index ON groups(distribution_id)" - ), - Statement( - name = "group_receipt_mms_id_index", - sql = "CREATE INDEX group_receipt_mms_id_index ON group_receipts (mms_id)" - ), - Statement( - name = "sticker_pack_id_index", - sql = "CREATE INDEX sticker_pack_id_index ON sticker (pack_id)" - ), - Statement( - name = "sticker_sticker_id_index", - sql = "CREATE INDEX sticker_sticker_id_index ON sticker (sticker_id)" - ), - Statement( - name = "storage_key_type_index", - sql = "CREATE INDEX storage_key_type_index ON storage_key (type)" - ), - Statement( - name = "mention_message_id_index", - sql = "CREATE INDEX mention_message_id_index ON mention (message_id)" - ), - Statement( - name = "mention_recipient_id_thread_id_index", - sql = "CREATE INDEX mention_recipient_id_thread_id_index ON mention (recipient_id, thread_id)" - ), - Statement( - name = "timestamp_direction_index", - sql = "CREATE INDEX timestamp_direction_index ON payments (timestamp, direction)" - ), - Statement( - name = "timestamp_index", - sql = "CREATE INDEX timestamp_index ON payments (timestamp)" - ), - Statement( - name = "receipt_public_key_index", - sql = "CREATE UNIQUE INDEX receipt_public_key_index ON payments (receipt_public_key)" - ), - Statement( - name = "msl_payload_date_sent_index", - sql = "CREATE INDEX msl_payload_date_sent_index ON msl_payload (date_sent)" - ), - Statement( - name = "msl_recipient_recipient_index", - sql = "CREATE INDEX msl_recipient_recipient_index ON msl_recipient (recipient_id, device, payload_id)" - ), - Statement( - name = "msl_recipient_payload_index", - sql = "CREATE INDEX msl_recipient_payload_index ON msl_recipient (payload_id)" - ), - Statement( - name = "msl_message_message_index", - sql = "CREATE INDEX msl_message_message_index ON msl_message (message_id, payload_id)" - ), - Statement( - name = "date_received_index", - sql = "CREATE INDEX date_received_index on group_call_ring (date_received)" - ), - Statement( - name = "notification_profile_schedule_profile_index", - sql = "CREATE INDEX notification_profile_schedule_profile_index ON notification_profile_schedule (notification_profile_id)" - ), - Statement( - name = "notification_profile_allowed_members_profile_index", - sql = "CREATE INDEX notification_profile_allowed_members_profile_index ON notification_profile_allowed_members (notification_profile_id)" - ), - Statement( - name = "donation_receipt_type_index", - sql = "CREATE INDEX donation_receipt_type_index ON donation_receipt (receipt_type)" - ), - Statement( - name = "donation_receipt_date_index", - sql = "CREATE INDEX donation_receipt_date_index ON donation_receipt (receipt_date)" - ), - Statement( - name = "story_sends_recipient_id_sent_timestamp_allows_replies_index", - sql = "CREATE INDEX story_sends_recipient_id_sent_timestamp_allows_replies_index ON story_sends (recipient_id, sent_timestamp, allows_replies)" - ), - Statement( - name = "story_sends_message_id_distribution_id_index", - sql = "CREATE INDEX story_sends_message_id_distribution_id_index ON story_sends (message_id, distribution_id)" - ), - Statement( - name = "distribution_list_member_list_id_recipient_id_privacy_mode_index", - sql = "CREATE UNIQUE INDEX distribution_list_member_list_id_recipient_id_privacy_mode_index ON distribution_list_member (list_id, recipient_id, privacy_mode)" - ), - Statement( - name = "pending_pni_recipient_sent_device_index", - sql = "CREATE UNIQUE INDEX pending_pni_recipient_sent_device_index ON pending_pni_signature_message (recipient_id, sent_timestamp, device_id)" - ), - Statement( - name = "call_call_id_index", - sql = "CREATE INDEX call_call_id_index ON call (call_id)" - ), - Statement( - name = "call_message_id_index", - sql = "CREATE INDEX call_message_id_index ON call (message_id)" - ), - Statement( - name = "message_ai", - sql = "CREATE TRIGGER message_ai AFTER INSERT ON message BEGIN\n INSERT INTO message_fts(rowid, body, thread_id) VALUES (new._id, new.body, new.thread_id);\n END" - ), - Statement( - name = "message_ad", - sql = "CREATE TRIGGER message_ad AFTER DELETE ON message BEGIN\n INSERT INTO message_fts(message_fts, rowid, body, thread_id) VALUES('delete', old._id, old.body, old.thread_id);\n END" - ), - Statement( - name = "message_au", - sql = "CREATE TRIGGER message_au AFTER UPDATE ON message BEGIN\n INSERT INTO message_fts(message_fts, rowid, body, thread_id) VALUES('delete', old._id, old.body, old.thread_id);\n INSERT INTO message_fts(rowid, body, thread_id) VALUES (new._id, new.body, new.thread_id);\n END" - ), - Statement( - name = "msl_message_delete", - sql = "CREATE TRIGGER msl_message_delete AFTER DELETE ON message \n BEGIN \n \tDELETE FROM msl_payload WHERE _id IN (SELECT payload_id FROM msl_message WHERE message_id = old._id);\n END" - ), - Statement( - name = "msl_attachment_delete", - sql = "CREATE TRIGGER msl_attachment_delete AFTER DELETE ON part\n BEGIN\n \tDELETE FROM msl_payload WHERE _id IN (SELECT payload_id FROM msl_message WHERE message_id = old.mid);\n END" - ) - ) - } -} diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/DatabaseConsistencyTest.kt b/app/src/test/java/org/thoughtcrime/securesms/database/DatabaseConsistencyTest.kt index eeba9a04e1..1277234719 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/DatabaseConsistencyTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/DatabaseConsistencyTest.kt @@ -6,7 +6,8 @@ package org.thoughtcrime.securesms.database import android.app.Application -import org.junit.Assert +import androidx.test.core.app.ApplicationProvider +import org.junit.Assert.assertTrue import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -19,23 +20,30 @@ import org.signal.core.util.getIndexes import org.signal.core.util.readToList import org.signal.core.util.requireNonNullString import org.thoughtcrime.securesms.database.helpers.SignalDatabaseMigrations -import org.thoughtcrime.securesms.testutil.SignalDatabaseMigrationRule +import org.thoughtcrime.securesms.testing.JdbcSqliteDatabase +import org.thoughtcrime.securesms.testing.TestSignalSQLiteDatabase +import org.thoughtcrime.securesms.testutil.MockAppDependenciesRule import org.thoughtcrime.securesms.testutil.SignalDatabaseRule +/** + * A test that guarantees that a freshly-created database looks the same as one that went through the upgrade path. + */ @RunWith(RobolectricTestRunner::class) -@Config(manifest = Config.NONE, application = Application::class, sdk = [35]) +@Config(manifest = Config.NONE, application = Application::class) class DatabaseConsistencyTest { + @get:Rule + val appDependencies = MockAppDependenciesRule() + @get:Rule val signalDatabaseRule = SignalDatabaseRule() - @get:Rule - val signalDatabaseMigrationRule = SignalDatabaseMigrationRule(SignalDatabaseMigrations.DATABASE_VERSION) - @Test fun testUpgradeConsistency() { val currentVersionStatements = signalDatabaseRule.readableDatabase.getAllCreateStatements() - val upgradedStatements = signalDatabaseMigrationRule.database.getAllCreateStatements() + + val upgradedDb = inMemoryUpgradedDatabase() + val upgradedStatements = upgradedDb.getAllCreateStatements() if (currentVersionStatements != upgradedStatements) { var message = "\n" @@ -51,7 +59,7 @@ class DatabaseConsistencyTest { message += "SQL entities exclusive to the upgraded database: $exclusiveToUpgrade\n\n" } else { for (currentEntry in currentByName) { - val upgradedValue: SignalDatabaseMigrationRule.Statement = upgradedByName[currentEntry.key]!! + val upgradedValue: Statement = upgradedByName[currentEntry.key]!! if (upgradedValue.sql != currentEntry.value.sql) { message += "Statement differed:\n" message += "newly-created:\n" @@ -62,8 +70,10 @@ class DatabaseConsistencyTest { } } - Assert.assertTrue(message, false) + assertTrue(message, false) } + + upgradedDb.close() } @Test @@ -83,18 +93,23 @@ class DatabaseConsistencyTest { } .map { it.table to it.column } - Assert.assertTrue("Missing indexes to cover: $notFound", notFound.isEmpty()) + assertTrue("Missing indexes to cover: $notFound", notFound.isEmpty()) } private fun List.hasPrimaryIndexFor(table: String, column: String): Boolean { return this.any { index -> index.table == table && index.columns[0] == column } } - private fun SQLiteDatabase.getAllCreateStatements(): List { + private data class Statement( + val name: String, + val sql: String + ) + + private fun SQLiteDatabase.getAllCreateStatements(): List { return this - .rawQuery("SELECT name, sql FROM sqlite_master WHERE sql NOT NULL AND name != 'sqlite_sequence' AND name != 'android_metadata'") + .rawQuery("SELECT name, sql FROM sqlite_master WHERE sql NOT NULL AND name != 'sqlite_sequence'") .readToList { cursor -> - SignalDatabaseMigrationRule.Statement( + Statement( name = cursor.requireNonNullString("name"), sql = cursor.requireNonNullString("sql").normalizeSql() ) @@ -116,4 +131,426 @@ class DatabaseConsistencyTest { .replace(Regex.fromLiteral(" )"), ")") .replace(Regex("CREATE TABLE \"([a-zA-Z_]+)\""), "CREATE TABLE $1") // for some reason SQLite will wrap table names in quotes for upgraded tables. This unwraps them. } + + /** + * Create an in-memory database from the V181 snapshot and migrate it to the current version. + */ + private fun inMemoryUpgradedDatabase(): SQLiteDatabase { + val db = JdbcSqliteDatabase.createInMemory() + SNAPSHOT_V181.forEach { db.execSQL(it.sql) } + val signalDb = TestSignalSQLiteDatabase(db) + SignalDatabaseMigrations.migrate( + context = ApplicationProvider.getApplicationContext(), + db = signalDb, + oldVersion = 181, + newVersion = SignalDatabaseMigrations.DATABASE_VERSION + ) + return signalDb + } + + /** + * This is the list of statements that existed at version 181. Never change this. + */ + private val SNAPSHOT_V181 = listOf( + Statement( + name = "message", + sql = "CREATE TABLE message (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n date_sent INTEGER NOT NULL,\n date_received INTEGER NOT NULL,\n date_server INTEGER DEFAULT -1,\n thread_id INTEGER NOT NULL REFERENCES thread (_id) ON DELETE CASCADE,\n recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,\n recipient_device_id INTEGER,\n type INTEGER NOT NULL,\n body TEXT,\n read INTEGER DEFAULT 0,\n ct_l TEXT,\n exp INTEGER,\n m_type INTEGER,\n m_size INTEGER,\n st INTEGER,\n tr_id TEXT,\n subscription_id INTEGER DEFAULT -1, \n receipt_timestamp INTEGER DEFAULT -1, \n delivery_receipt_count INTEGER DEFAULT 0, \n read_receipt_count INTEGER DEFAULT 0, \n viewed_receipt_count INTEGER DEFAULT 0,\n mismatched_identities TEXT DEFAULT NULL,\n network_failures TEXT DEFAULT NULL,\n expires_in INTEGER DEFAULT 0,\n expire_started INTEGER DEFAULT 0,\n notified INTEGER DEFAULT 0,\n quote_id INTEGER DEFAULT 0,\n quote_author INTEGER DEFAULT 0,\n quote_body TEXT DEFAULT NULL,\n quote_missing INTEGER DEFAULT 0,\n quote_mentions BLOB DEFAULT NULL,\n quote_type INTEGER DEFAULT 0,\n shared_contacts TEXT DEFAULT NULL,\n unidentified INTEGER DEFAULT 0,\n link_previews TEXT DEFAULT NULL,\n view_once INTEGER DEFAULT 0,\n reactions_unread INTEGER DEFAULT 0,\n reactions_last_seen INTEGER DEFAULT -1,\n remote_deleted INTEGER DEFAULT 0,\n mentions_self INTEGER DEFAULT 0,\n notified_timestamp INTEGER DEFAULT 0,\n server_guid TEXT DEFAULT NULL,\n message_ranges BLOB DEFAULT NULL,\n story_type INTEGER DEFAULT 0,\n parent_story_id INTEGER DEFAULT 0,\n export_state BLOB DEFAULT NULL,\n exported INTEGER DEFAULT 0,\n scheduled_date INTEGER DEFAULT -1\n )" + ), + Statement( + name = "part", + sql = "CREATE TABLE part (_id INTEGER PRIMARY KEY, mid INTEGER, seq INTEGER DEFAULT 0, ct TEXT, name TEXT, chset INTEGER, cd TEXT, fn TEXT, cid TEXT, cl TEXT, ctt_s INTEGER, ctt_t TEXT, encrypted INTEGER, pending_push INTEGER, _data TEXT, data_size INTEGER, file_name TEXT, unique_id INTEGER NOT NULL, digest BLOB, fast_preflight_id TEXT, voice_note INTEGER DEFAULT 0, borderless INTEGER DEFAULT 0, video_gif INTEGER DEFAULT 0, data_random BLOB, quote INTEGER DEFAULT 0, width INTEGER DEFAULT 0, height INTEGER DEFAULT 0, caption TEXT DEFAULT NULL, sticker_pack_id TEXT DEFAULT NULL, sticker_pack_key DEFAULT NULL, sticker_id INTEGER DEFAULT -1, sticker_emoji STRING DEFAULT NULL, data_hash TEXT DEFAULT NULL, blur_hash TEXT DEFAULT NULL, transform_properties TEXT DEFAULT NULL, transfer_file TEXT DEFAULT NULL, display_order INTEGER DEFAULT 0, upload_timestamp INTEGER DEFAULT 0, cdn_number INTEGER DEFAULT 0)" + ), + Statement( + name = "thread", + sql = "CREATE TABLE thread (\n _id INTEGER PRIMARY KEY AUTOINCREMENT, \n date INTEGER DEFAULT 0, \n meaningful_messages INTEGER DEFAULT 0,\n recipient_id INTEGER NOT NULL UNIQUE REFERENCES recipient (_id) ON DELETE CASCADE,\n read INTEGER DEFAULT 1, \n type INTEGER DEFAULT 0, \n error INTEGER DEFAULT 0, \n snippet TEXT, \n snippet_type INTEGER DEFAULT 0, \n snippet_uri TEXT DEFAULT NULL, \n snippet_content_type TEXT DEFAULT NULL, \n snippet_extras TEXT DEFAULT NULL, \n unread_count INTEGER DEFAULT 0, \n archived INTEGER DEFAULT 0, \n status INTEGER DEFAULT 0, \n delivery_receipt_count INTEGER DEFAULT 0, \n read_receipt_count INTEGER DEFAULT 0, \n expires_in INTEGER DEFAULT 0, \n last_seen INTEGER DEFAULT 0, \n has_sent INTEGER DEFAULT 0, \n last_scrolled INTEGER DEFAULT 0, \n pinned INTEGER DEFAULT 0, \n unread_self_mention_count INTEGER DEFAULT 0\n)" + ), + Statement( + name = "identities", + sql = "CREATE TABLE identities (\n _id INTEGER PRIMARY KEY AUTOINCREMENT, \n address INTEGER UNIQUE, \n identity_key TEXT, \n first_use INTEGER DEFAULT 0, \n timestamp INTEGER DEFAULT 0, \n verified INTEGER DEFAULT 0, \n nonblocking_approval INTEGER DEFAULT 0\n )" + ), + Statement( + name = "drafts", + sql = "CREATE TABLE drafts (\n _id INTEGER PRIMARY KEY, \n thread_id INTEGER, \n type TEXT, \n value TEXT\n )" + ), + Statement( + name = "push", + sql = "CREATE TABLE push (_id INTEGER PRIMARY KEY, type INTEGER, source TEXT, source_uuid TEXT, device_id INTEGER, body TEXT, content TEXT, timestamp INTEGER, server_timestamp INTEGER DEFAULT 0, server_delivered_timestamp INTEGER DEFAULT 0, server_guid TEXT DEFAULT NULL)" + ), + Statement( + name = "groups", + sql = "CREATE TABLE groups (\n _id INTEGER PRIMARY KEY, \n group_id TEXT, \n recipient_id INTEGER,\n title TEXT,\n avatar_id INTEGER, \n avatar_key BLOB,\n avatar_content_type TEXT, \n avatar_relay TEXT,\n timestamp INTEGER,\n active INTEGER DEFAULT 1,\n avatar_digest BLOB, \n mms INTEGER DEFAULT 0, \n master_key BLOB, \n revision BLOB, \n decrypted_group BLOB, \n expected_v2_id TEXT DEFAULT NULL, \n former_v1_members TEXT DEFAULT NULL, \n distribution_id TEXT DEFAULT NULL, \n display_as_story INTEGER DEFAULT 0, \n auth_service_id TEXT DEFAULT NULL, \n last_force_update_timestamp INTEGER DEFAULT 0\n )" + ), + Statement( + name = "group_membership", + sql = "CREATE TABLE group_membership ( _id INTEGER PRIMARY KEY, group_id TEXT NOT NULL, recipient_id INTEGER NOT NULL, UNIQUE(group_id, recipient_id) )" + ), + Statement( + name = "recipient", + sql = "CREATE TABLE recipient (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n uuid TEXT UNIQUE DEFAULT NULL,\n username TEXT UNIQUE DEFAULT NULL,\n phone TEXT UNIQUE DEFAULT NULL,\n email TEXT UNIQUE DEFAULT NULL,\n group_id TEXT UNIQUE DEFAULT NULL,\n group_type INTEGER DEFAULT 0,\n blocked INTEGER DEFAULT 0,\n message_ringtone TEXT DEFAULT NULL, \n message_vibrate INTEGER DEFAULT 0, \n call_ringtone TEXT DEFAULT NULL, \n call_vibrate INTEGER DEFAULT 0, \n notification_channel TEXT DEFAULT NULL, \n mute_until INTEGER DEFAULT 0, \n color TEXT DEFAULT NULL, \n seen_invite_reminder INTEGER DEFAULT 0,\n default_subscription_id INTEGER DEFAULT -1,\n message_expiration_time INTEGER DEFAULT 0,\n registered INTEGER DEFAULT 0,\n system_given_name TEXT DEFAULT NULL, \n system_family_name TEXT DEFAULT NULL, \n system_display_name TEXT DEFAULT NULL, \n system_photo_uri TEXT DEFAULT NULL, \n system_phone_label TEXT DEFAULT NULL, \n system_phone_type INTEGER DEFAULT -1, \n system_contact_uri TEXT DEFAULT NULL, \n system_info_pending INTEGER DEFAULT 0, \n profile_key TEXT DEFAULT NULL, \n profile_key_credential TEXT DEFAULT NULL, \n signal_profile_name TEXT DEFAULT NULL, \n profile_family_name TEXT DEFAULT NULL, \n profile_joined_name TEXT DEFAULT NULL, \n signal_profile_avatar TEXT DEFAULT NULL, \n profile_sharing INTEGER DEFAULT 0, \n last_profile_fetch INTEGER DEFAULT 0, \n unidentified_access_mode INTEGER DEFAULT 0, \n force_sms_selection INTEGER DEFAULT 0, \n storage_service_key TEXT UNIQUE DEFAULT NULL, \n mention_setting INTEGER DEFAULT 0, \n storage_proto TEXT DEFAULT NULL,\n capabilities INTEGER DEFAULT 0,\n last_session_reset BLOB DEFAULT NULL,\n wallpaper BLOB DEFAULT NULL,\n wallpaper_file TEXT DEFAULT NULL,\n about TEXT DEFAULT NULL,\n about_emoji TEXT DEFAULT NULL,\n extras BLOB DEFAULT NULL,\n groups_in_common INTEGER DEFAULT 0,\n chat_colors BLOB DEFAULT NULL,\n custom_chat_colors_id INTEGER DEFAULT 0,\n badges BLOB DEFAULT NULL,\n pni TEXT DEFAULT NULL,\n distribution_list_id INTEGER DEFAULT NULL,\n needs_pni_signature INTEGER DEFAULT 0,\n unregistered_timestamp INTEGER DEFAULT 0,\n hidden INTEGER DEFAULT 0,\n reporting_token BLOB DEFAULT NULL,\n system_nickname TEXT DEFAULT NULL\n)" + ), + Statement( + name = "group_receipts", + sql = "CREATE TABLE group_receipts (\n _id INTEGER PRIMARY KEY, \n mms_id INTEGER, \n address INTEGER, \n status INTEGER, \n timestamp INTEGER, \n unidentified INTEGER DEFAULT 0\n )" + ), + Statement( + name = "one_time_prekeys", + sql = "CREATE TABLE one_time_prekeys (\n _id INTEGER PRIMARY KEY,\n account_id TEXT NOT NULL,\n key_id INTEGER UNIQUE, \n public_key TEXT NOT NULL, \n private_key TEXT NOT NULL,\n UNIQUE(account_id, key_id)\n )" + ), + Statement( + name = "signed_prekeys", + sql = "CREATE TABLE signed_prekeys (\n _id INTEGER PRIMARY KEY,\n account_id TEXT NOT NULL,\n key_id INTEGER UNIQUE, \n public_key TEXT NOT NULL,\n private_key TEXT NOT NULL,\n signature TEXT NOT NULL, \n timestamp INTEGER DEFAULT 0,\n UNIQUE(account_id, key_id)\n )" + ), + Statement( + name = "sessions", + sql = "CREATE TABLE sessions (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n account_id TEXT NOT NULL,\n address TEXT NOT NULL,\n device INTEGER NOT NULL,\n record BLOB NOT NULL,\n UNIQUE(account_id, address, device)\n )" + ), + Statement( + name = "sender_keys", + sql = "CREATE TABLE sender_keys (\n _id INTEGER PRIMARY KEY AUTOINCREMENT, \n address TEXT NOT NULL, \n device INTEGER NOT NULL, \n distribution_id TEXT NOT NULL,\n record BLOB NOT NULL, \n created_at INTEGER NOT NULL, \n UNIQUE(address,device, distribution_id) ON CONFLICT REPLACE\n )" + ), + Statement( + name = "sender_key_shared", + sql = "CREATE TABLE sender_key_shared (\n _id INTEGER PRIMARY KEY AUTOINCREMENT, \n distribution_id TEXT NOT NULL, \n address TEXT NOT NULL, \n device INTEGER NOT NULL, \n timestamp INTEGER DEFAULT 0, \n UNIQUE(distribution_id,address, device) ON CONFLICT REPLACE\n )" + ), + Statement( + name = "pending_retry_receipts", + sql = "CREATE TABLE pending_retry_receipts(_id INTEGER PRIMARY KEY AUTOINCREMENT, author TEXT NOT NULL, device INTEGER NOT NULL, sent_timestamp INTEGER NOT NULL, received_timestamp TEXT NOT NULL, thread_id INTEGER NOT NULL, UNIQUE(author,sent_timestamp) ON CONFLICT REPLACE)" + ), + Statement( + name = "sticker", + sql = "CREATE TABLE sticker (_id INTEGER PRIMARY KEY AUTOINCREMENT, pack_id TEXT NOT NULL, pack_key TEXT NOT NULL, pack_title TEXT NOT NULL, pack_author TEXT NOT NULL, sticker_id INTEGER, cover INTEGER, pack_order INTEGER, emoji TEXT NOT NULL, content_type TEXT DEFAULT NULL, last_used INTEGER, installed INTEGER,file_path TEXT NOT NULL, file_length INTEGER, file_random BLOB, UNIQUE(pack_id, sticker_id, cover) ON CONFLICT IGNORE)" + ), + Statement( + name = "storage_key", + sql = "CREATE TABLE storage_key (_id INTEGER PRIMARY KEY AUTOINCREMENT, type INTEGER, key TEXT UNIQUE)" + ), + Statement( + name = "mention", + sql = "CREATE TABLE mention(_id INTEGER PRIMARY KEY AUTOINCREMENT, thread_id INTEGER, message_id INTEGER, recipient_id INTEGER, range_start INTEGER, range_length INTEGER)" + ), + Statement( + name = "payments", + sql = "CREATE TABLE payments(_id INTEGER PRIMARY KEY, uuid TEXT DEFAULT NULL, recipient INTEGER DEFAULT 0, recipient_address TEXT DEFAULT NULL, timestamp INTEGER, note TEXT DEFAULT NULL, direction INTEGER, state INTEGER, failure_reason INTEGER, amount BLOB NOT NULL, fee BLOB NOT NULL, transaction_record BLOB DEFAULT NULL, receipt BLOB DEFAULT NULL, payment_metadata BLOB DEFAULT NULL, receipt_public_key TEXT DEFAULT NULL, block_index INTEGER DEFAULT 0, block_timestamp INTEGER DEFAULT 0, seen INTEGER, UNIQUE(uuid) ON CONFLICT ABORT)" + ), + Statement( + name = "chat_colors", + sql = "CREATE TABLE chat_colors (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n chat_colors BLOB\n)" + ), + Statement( + name = "emoji_search", + sql = "CREATE TABLE emoji_search (\n _id INTEGER PRIMARY KEY,\n label TEXT NOT NULL,\n emoji TEXT NOT NULL,\n rank INTEGER DEFAULT 2147483647 \n )" + ), + Statement( + name = "avatar_picker", + sql = "CREATE TABLE avatar_picker (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n last_used INTEGER DEFAULT 0,\n group_id TEXT DEFAULT NULL,\n avatar BLOB NOT NULL\n)" + ), + Statement( + name = "group_call_ring", + sql = "CREATE TABLE group_call_ring (\n _id INTEGER PRIMARY KEY,\n ring_id INTEGER UNIQUE,\n date_received INTEGER,\n ring_state INTEGER\n)" + ), + Statement( + name = "reaction", + sql = "CREATE TABLE reaction (\n _id INTEGER PRIMARY KEY,\n message_id INTEGER NOT NULL REFERENCES message (_id) ON DELETE CASCADE,\n author_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,\n emoji TEXT NOT NULL,\n date_sent INTEGER NOT NULL,\n date_received INTEGER NOT NULL,\n UNIQUE(message_id, author_id) ON CONFLICT REPLACE\n)" + ), + Statement( + name = "donation_receipt", + sql = "CREATE TABLE donation_receipt (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n receipt_type TEXT NOT NULL,\n receipt_date INTEGER NOT NULL,\n amount TEXT NOT NULL,\n currency TEXT NOT NULL,\n subscription_level INTEGER NOT NULL\n)" + ), + Statement( + name = "story_sends", + sql = "CREATE TABLE story_sends (\n _id INTEGER PRIMARY KEY,\n message_id INTEGER NOT NULL REFERENCES message (_id) ON DELETE CASCADE,\n recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,\n sent_timestamp INTEGER NOT NULL,\n allows_replies INTEGER NOT NULL,\n distribution_id TEXT NOT NULL REFERENCES distribution_list (distribution_id) ON DELETE CASCADE\n)" + ), + Statement( + name = "cds", + sql = "CREATE TABLE cds (\n _id INTEGER PRIMARY KEY,\n e164 TEXT NOT NULL UNIQUE ON CONFLICT IGNORE,\n last_seen_at INTEGER DEFAULT 0\n )" + ), + Statement( + name = "remote_megaphone", + sql = "CREATE TABLE remote_megaphone (\n _id INTEGER PRIMARY KEY,\n uuid TEXT UNIQUE NOT NULL,\n priority INTEGER NOT NULL,\n countries TEXT,\n minimum_version INTEGER NOT NULL,\n dont_show_before INTEGER NOT NULL,\n dont_show_after INTEGER NOT NULL,\n show_for_days INTEGER NOT NULL,\n conditional_id TEXT,\n primary_action_id TEXT,\n secondary_action_id TEXT,\n image_url TEXT,\n image_uri TEXT DEFAULT NULL,\n title TEXT NOT NULL,\n body TEXT NOT NULL,\n primary_action_text TEXT,\n secondary_action_text TEXT,\n shown_at INTEGER DEFAULT 0,\n finished_at INTEGER DEFAULT 0,\n primary_action_data TEXT DEFAULT NULL,\n secondary_action_data TEXT DEFAULT NULL,\n snoozed_at INTEGER DEFAULT 0,\n seen_count INTEGER DEFAULT 0\n)" + ), + Statement( + name = "pending_pni_signature_message", + sql = "CREATE TABLE pending_pni_signature_message (\n _id INTEGER PRIMARY KEY,\n recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,\n sent_timestamp INTEGER NOT NULL,\n device_id INTEGER NOT NULL\n )" + ), + Statement( + name = "call", + sql = "CREATE TABLE call (\n _id INTEGER PRIMARY KEY,\n call_id INTEGER NOT NULL UNIQUE,\n message_id INTEGER NOT NULL REFERENCES message (_id) ON DELETE CASCADE,\n peer INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,\n type INTEGER NOT NULL,\n direction INTEGER NOT NULL,\n event INTEGER NOT NULL\n)" + ), + Statement( + name = "message_fts", + sql = "CREATE VIRTUAL TABLE message_fts USING fts5(body, thread_id UNINDEXED, content=message, content_rowid=_id)" + ), + Statement( + name = "remapped_recipients", + sql = "CREATE TABLE remapped_recipients (\n _id INTEGER PRIMARY KEY AUTOINCREMENT, \n old_id INTEGER UNIQUE, \n new_id INTEGER\n )" + ), + Statement( + name = "remapped_threads", + sql = "CREATE TABLE remapped_threads (\n _id INTEGER PRIMARY KEY AUTOINCREMENT, \n old_id INTEGER UNIQUE, \n new_id INTEGER\n )" + ), + Statement( + name = "msl_payload", + sql = "CREATE TABLE msl_payload (\n _id INTEGER PRIMARY KEY,\n date_sent INTEGER NOT NULL,\n content BLOB NOT NULL,\n content_hint INTEGER NOT NULL,\n urgent INTEGER NOT NULL DEFAULT 1\n )" + ), + Statement( + name = "msl_recipient", + sql = "CREATE TABLE msl_recipient (\n _id INTEGER PRIMARY KEY,\n payload_id INTEGER NOT NULL REFERENCES msl_payload (_id) ON DELETE CASCADE,\n recipient_id INTEGER NOT NULL, \n device INTEGER NOT NULL\n )" + ), + Statement( + name = "msl_message", + sql = "CREATE TABLE msl_message (\n _id INTEGER PRIMARY KEY,\n payload_id INTEGER NOT NULL REFERENCES msl_payload (_id) ON DELETE CASCADE,\n message_id INTEGER NOT NULL\n )" + ), + Statement( + name = "notification_profile", + sql = "CREATE TABLE notification_profile (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL UNIQUE,\n emoji TEXT NOT NULL,\n color TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n allow_all_calls INTEGER NOT NULL DEFAULT 0,\n allow_all_mentions INTEGER NOT NULL DEFAULT 0\n)" + ), + Statement( + name = "notification_profile_schedule", + sql = "CREATE TABLE notification_profile_schedule (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n notification_profile_id INTEGER NOT NULL REFERENCES notification_profile (_id) ON DELETE CASCADE,\n enabled INTEGER NOT NULL DEFAULT 0,\n start INTEGER NOT NULL,\n end INTEGER NOT NULL,\n days_enabled TEXT NOT NULL\n)" + ), + Statement( + name = "notification_profile_allowed_members", + sql = "CREATE TABLE notification_profile_allowed_members (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n notification_profile_id INTEGER NOT NULL REFERENCES notification_profile (_id) ON DELETE CASCADE,\n recipient_id INTEGER NOT NULL,\n UNIQUE(notification_profile_id, recipient_id) ON CONFLICT REPLACE\n)" + ), + Statement( + name = "distribution_list", + sql = "CREATE TABLE distribution_list (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT UNIQUE NOT NULL,\n distribution_id TEXT UNIQUE NOT NULL,\n recipient_id INTEGER UNIQUE REFERENCES recipient (_id),\n allows_replies INTEGER DEFAULT 1,\n deletion_timestamp INTEGER DEFAULT 0,\n is_unknown INTEGER DEFAULT 0,\n privacy_mode INTEGER DEFAULT 0\n )" + ), + Statement( + name = "distribution_list_member", + sql = "CREATE TABLE distribution_list_member (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n list_id INTEGER NOT NULL REFERENCES distribution_list (_id) ON DELETE CASCADE,\n recipient_id INTEGER NOT NULL REFERENCES recipient (_id),\n privacy_mode INTEGER DEFAULT 0\n )" + ), + Statement( + name = "recipient_group_type_index", + sql = "CREATE INDEX recipient_group_type_index ON recipient (group_type)" + ), + Statement( + name = "recipient_pni_index", + sql = "CREATE UNIQUE INDEX recipient_pni_index ON recipient (pni)" + ), + Statement( + name = "recipient_service_id_profile_key", + sql = "CREATE INDEX recipient_service_id_profile_key ON recipient (uuid, profile_key) WHERE uuid NOT NULL AND profile_key NOT NULL" + ), + Statement( + name = "mms_read_and_notified_and_thread_id_index", + sql = "CREATE INDEX mms_read_and_notified_and_thread_id_index ON message (read, notified, thread_id)" + ), + Statement( + name = "mms_type_index", + sql = "CREATE INDEX mms_type_index ON message (type)" + ), + Statement( + name = "mms_date_sent_index", + sql = "CREATE INDEX mms_date_sent_index ON message (date_sent, recipient_id, thread_id)" + ), + Statement( + name = "mms_date_server_index", + sql = "CREATE INDEX mms_date_server_index ON message (date_server)" + ), + Statement( + name = "mms_thread_date_index", + sql = "CREATE INDEX mms_thread_date_index ON message (thread_id, date_received)" + ), + Statement( + name = "mms_reactions_unread_index", + sql = "CREATE INDEX mms_reactions_unread_index ON message (reactions_unread)" + ), + Statement( + name = "mms_story_type_index", + sql = "CREATE INDEX mms_story_type_index ON message (story_type)" + ), + Statement( + name = "mms_parent_story_id_index", + sql = "CREATE INDEX mms_parent_story_id_index ON message (parent_story_id)" + ), + Statement( + name = "mms_thread_story_parent_story_scheduled_date_index", + sql = "CREATE INDEX mms_thread_story_parent_story_scheduled_date_index ON message (thread_id, date_received, story_type, parent_story_id, scheduled_date)" + ), + Statement( + name = "message_quote_id_quote_author_scheduled_date_index", + sql = "CREATE INDEX message_quote_id_quote_author_scheduled_date_index ON message (quote_id, quote_author, scheduled_date)" + ), + Statement( + name = "mms_exported_index", + sql = "CREATE INDEX mms_exported_index ON message (exported)" + ), + Statement( + name = "mms_id_type_payment_transactions_index", + sql = "CREATE INDEX mms_id_type_payment_transactions_index ON message (_id,type) WHERE type & 12884901888 != 0" + ), + Statement( + name = "part_mms_id_index", + sql = "CREATE INDEX part_mms_id_index ON part (mid)" + ), + Statement( + name = "pending_push_index", + sql = "CREATE INDEX pending_push_index ON part (pending_push)" + ), + Statement( + name = "part_sticker_pack_id_index", + sql = "CREATE INDEX part_sticker_pack_id_index ON part (sticker_pack_id)" + ), + Statement( + name = "part_data_hash_index", + sql = "CREATE INDEX part_data_hash_index ON part (data_hash)" + ), + Statement( + name = "part_data_index", + sql = "CREATE INDEX part_data_index ON part (_data)" + ), + Statement( + name = "thread_recipient_id_index", + sql = "CREATE INDEX thread_recipient_id_index ON thread (recipient_id)" + ), + Statement( + name = "archived_count_index", + sql = "CREATE INDEX archived_count_index ON thread (archived, meaningful_messages)" + ), + Statement( + name = "thread_pinned_index", + sql = "CREATE INDEX thread_pinned_index ON thread (pinned)" + ), + Statement( + name = "thread_read", + sql = "CREATE INDEX thread_read ON thread (read)" + ), + Statement( + name = "draft_thread_index", + sql = "CREATE INDEX draft_thread_index ON drafts (thread_id)" + ), + Statement( + name = "group_id_index", + sql = "CREATE UNIQUE INDEX group_id_index ON groups (group_id)" + ), + Statement( + name = "group_recipient_id_index", + sql = "CREATE UNIQUE INDEX group_recipient_id_index ON groups (recipient_id)" + ), + Statement( + name = "expected_v2_id_index", + sql = "CREATE UNIQUE INDEX expected_v2_id_index ON groups (expected_v2_id)" + ), + Statement( + name = "group_distribution_id_index", + sql = "CREATE UNIQUE INDEX group_distribution_id_index ON groups(distribution_id)" + ), + Statement( + name = "group_receipt_mms_id_index", + sql = "CREATE INDEX group_receipt_mms_id_index ON group_receipts (mms_id)" + ), + Statement( + name = "sticker_pack_id_index", + sql = "CREATE INDEX sticker_pack_id_index ON sticker (pack_id)" + ), + Statement( + name = "sticker_sticker_id_index", + sql = "CREATE INDEX sticker_sticker_id_index ON sticker (sticker_id)" + ), + Statement( + name = "storage_key_type_index", + sql = "CREATE INDEX storage_key_type_index ON storage_key (type)" + ), + Statement( + name = "mention_message_id_index", + sql = "CREATE INDEX mention_message_id_index ON mention (message_id)" + ), + Statement( + name = "mention_recipient_id_thread_id_index", + sql = "CREATE INDEX mention_recipient_id_thread_id_index ON mention (recipient_id, thread_id)" + ), + Statement( + name = "timestamp_direction_index", + sql = "CREATE INDEX timestamp_direction_index ON payments (timestamp, direction)" + ), + Statement( + name = "timestamp_index", + sql = "CREATE INDEX timestamp_index ON payments (timestamp)" + ), + Statement( + name = "receipt_public_key_index", + sql = "CREATE UNIQUE INDEX receipt_public_key_index ON payments (receipt_public_key)" + ), + Statement( + name = "msl_payload_date_sent_index", + sql = "CREATE INDEX msl_payload_date_sent_index ON msl_payload (date_sent)" + ), + Statement( + name = "msl_recipient_recipient_index", + sql = "CREATE INDEX msl_recipient_recipient_index ON msl_recipient (recipient_id, device, payload_id)" + ), + Statement( + name = "msl_recipient_payload_index", + sql = "CREATE INDEX msl_recipient_payload_index ON msl_recipient (payload_id)" + ), + Statement( + name = "msl_message_message_index", + sql = "CREATE INDEX msl_message_message_index ON msl_message (message_id, payload_id)" + ), + Statement( + name = "date_received_index", + sql = "CREATE INDEX date_received_index on group_call_ring (date_received)" + ), + Statement( + name = "notification_profile_schedule_profile_index", + sql = "CREATE INDEX notification_profile_schedule_profile_index ON notification_profile_schedule (notification_profile_id)" + ), + Statement( + name = "notification_profile_allowed_members_profile_index", + sql = "CREATE INDEX notification_profile_allowed_members_profile_index ON notification_profile_allowed_members (notification_profile_id)" + ), + Statement( + name = "donation_receipt_type_index", + sql = "CREATE INDEX donation_receipt_type_index ON donation_receipt (receipt_type)" + ), + Statement( + name = "donation_receipt_date_index", + sql = "CREATE INDEX donation_receipt_date_index ON donation_receipt (receipt_date)" + ), + Statement( + name = "story_sends_recipient_id_sent_timestamp_allows_replies_index", + sql = "CREATE INDEX story_sends_recipient_id_sent_timestamp_allows_replies_index ON story_sends (recipient_id, sent_timestamp, allows_replies)" + ), + Statement( + name = "story_sends_message_id_distribution_id_index", + sql = "CREATE INDEX story_sends_message_id_distribution_id_index ON story_sends (message_id, distribution_id)" + ), + Statement( + name = "distribution_list_member_list_id_recipient_id_privacy_mode_index", + sql = "CREATE UNIQUE INDEX distribution_list_member_list_id_recipient_id_privacy_mode_index ON distribution_list_member (list_id, recipient_id, privacy_mode)" + ), + Statement( + name = "pending_pni_recipient_sent_device_index", + sql = "CREATE UNIQUE INDEX pending_pni_recipient_sent_device_index ON pending_pni_signature_message (recipient_id, sent_timestamp, device_id)" + ), + Statement( + name = "call_call_id_index", + sql = "CREATE INDEX call_call_id_index ON call (call_id)" + ), + Statement( + name = "call_message_id_index", + sql = "CREATE INDEX call_message_id_index ON call (message_id)" + ), + Statement( + name = "message_ai", + sql = "CREATE TRIGGER message_ai AFTER INSERT ON message BEGIN\n INSERT INTO message_fts(rowid, body, thread_id) VALUES (new._id, new.body, new.thread_id);\n END" + ), + Statement( + name = "message_ad", + sql = "CREATE TRIGGER message_ad AFTER DELETE ON message BEGIN\n INSERT INTO message_fts(message_fts, rowid, body, thread_id) VALUES('delete', old._id, old.body, old.thread_id);\n END" + ), + Statement( + name = "message_au", + sql = "CREATE TRIGGER message_au AFTER UPDATE ON message BEGIN\n INSERT INTO message_fts(message_fts, rowid, body, thread_id) VALUES('delete', old._id, old.body, old.thread_id);\n INSERT INTO message_fts(rowid, body, thread_id) VALUES (new._id, new.body, new.thread_id);\n END" + ), + Statement( + name = "msl_message_delete", + sql = "CREATE TRIGGER msl_message_delete AFTER DELETE ON message \n BEGIN \n \tDELETE FROM msl_payload WHERE _id IN (SELECT payload_id FROM msl_message WHERE message_id = old._id);\n END" + ), + Statement( + name = "msl_attachment_delete", + sql = "CREATE TRIGGER msl_attachment_delete AFTER DELETE ON part\n BEGIN\n \tDELETE FROM msl_payload WHERE _id IN (SELECT payload_id FROM msl_message WHERE message_id = old.mid);\n END" + ) + ) } diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/SqliteCapabilityTest.kt b/app/src/test/java/org/thoughtcrime/securesms/database/SqliteCapabilityTest.kt new file mode 100644 index 0000000000..d416a0a18e --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/database/SqliteCapabilityTest.kt @@ -0,0 +1,34 @@ +package org.thoughtcrime.securesms.database + +import android.app.Application +import assertk.assertThat +import assertk.assertions.isEqualTo +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.signal.core.util.readToList +import org.signal.core.util.requireString +import org.thoughtcrime.securesms.testutil.MockAppDependenciesRule +import org.thoughtcrime.securesms.testutil.SignalDatabaseRule + +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, application = Application::class) +class SqliteCapabilityTest { + + @get:Rule + val appDependencies = MockAppDependenciesRule() + + @get:Rule + val signalDatabaseRule = SignalDatabaseRule() + + @Test + fun `json_each is supported`() { + val values = signalDatabaseRule.readableDatabase + .rawQuery("SELECT value FROM json_each('[1,2,3]')", null) + .readToList { cursor -> cursor.requireString("value") } + + assertThat(values).isEqualTo(listOf("1", "2", "3")) + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/testing/JdbcSqliteDatabase.kt b/app/src/test/java/org/thoughtcrime/securesms/testing/JdbcSqliteDatabase.kt new file mode 100644 index 0000000000..eebfacf03c --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/testing/JdbcSqliteDatabase.kt @@ -0,0 +1,427 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.testing + +import android.content.ContentValues +import android.database.Cursor +import android.database.MatrixCursor +import android.database.sqlite.SQLiteTransactionListener +import android.os.CancellationSignal +import android.util.Pair +import androidx.sqlite.db.SupportSQLiteDatabase +import androidx.sqlite.db.SupportSQLiteProgram +import androidx.sqlite.db.SupportSQLiteQuery +import androidx.sqlite.db.SupportSQLiteStatement +import java.sql.Connection +import java.sql.DriverManager +import java.sql.PreparedStatement +import java.sql.Types +import java.util.Locale + +/** + * A [SupportSQLiteDatabase] backed by JDBC (sqlite-jdbc / org.xerial). Provides a modern SQLite + * engine with FTS5 and JSON1 support for unit tests, replacing Robolectric's limited native SQLite. + */ +class JdbcSqliteDatabase private constructor(private val connection: Connection) : SupportSQLiteDatabase { + + private var transactionSuccessful = false + private var transactionNesting = 0 + + companion object { + private val CONFLICT_VALUES = arrayOf("", " OR ROLLBACK", " OR ABORT", " OR FAIL", " OR IGNORE", " OR REPLACE") + + fun createInMemory(): JdbcSqliteDatabase { + Class.forName("org.sqlite.JDBC") + val connection = DriverManager.getConnection("jdbc:sqlite::memory:") + connection.autoCommit = true + return JdbcSqliteDatabase(connection) + } + } + + // region Transaction Management + + override fun beginTransaction() { + if (transactionNesting == 0) { + connection.createStatement().use { it.execute("BEGIN IMMEDIATE") } + } else { + connection.createStatement().use { it.execute("SAVEPOINT sp_$transactionNesting") } + } + transactionNesting++ + transactionSuccessful = false + } + + override fun beginTransactionNonExclusive() { + if (transactionNesting == 0) { + connection.createStatement().use { it.execute("BEGIN DEFERRED") } + } else { + connection.createStatement().use { it.execute("SAVEPOINT sp_$transactionNesting") } + } + transactionNesting++ + transactionSuccessful = false + } + + override fun beginTransactionWithListener(transactionListener: SQLiteTransactionListener) { + beginTransaction() + transactionListener.onBegin() + } + + override fun beginTransactionWithListenerNonExclusive(transactionListener: SQLiteTransactionListener) { + beginTransactionNonExclusive() + transactionListener.onBegin() + } + + override fun endTransaction() { + transactionNesting-- + if (transactionNesting == 0) { + if (transactionSuccessful) { + connection.createStatement().use { it.execute("COMMIT") } + } else { + connection.createStatement().use { it.execute("ROLLBACK") } + } + } else { + if (!transactionSuccessful) { + connection.createStatement().use { it.execute("ROLLBACK TO SAVEPOINT sp_$transactionNesting") } + } + connection.createStatement().use { it.execute("RELEASE SAVEPOINT sp_$transactionNesting") } + } + transactionSuccessful = false + } + + override fun setTransactionSuccessful() { + transactionSuccessful = true + } + + override fun inTransaction(): Boolean = transactionNesting > 0 + + override val isDbLockedByCurrentThread: Boolean + get() = true + + override fun yieldIfContendedSafely(): Boolean = false + + override fun yieldIfContendedSafely(sleepAfterYieldDelay: Long): Boolean = false + + // endregion + + // region Query + + override fun query(query: String): Cursor = query(query, emptyArray()) + + override fun query(query: String, bindArgs: Array): Cursor { + return executeQuery(query, bindArgs) + } + + override fun query(query: SupportSQLiteQuery): Cursor { + val capture = BindingCapture(query.argCount) + query.bindTo(capture) + return executeQuery(query.sql, capture.getArgs()) + } + + override fun query(query: SupportSQLiteQuery, cancellationSignal: CancellationSignal?): Cursor { + return query(query) + } + + private fun executeQuery(sql: String, bindArgs: Array): Cursor { + val stmt = connection.prepareStatement(sql) + bindArgs(stmt, bindArgs) + + // sqlite-jdbc throws if you call executeQuery() on a non-SELECT statement. + // Some callers (e.g. migrations) pass UPDATE/INSERT through rawQuery, so we + // use execute() and check whether there's a result set. + val hasResultSet = stmt.execute() + if (!hasResultSet) { + stmt.close() + return MatrixCursor(emptyArray()) + } + + val rs = stmt.resultSet + val metaData = rs.metaData + val columnCount = metaData.columnCount + val columnNames = Array(columnCount) { metaData.getColumnLabel(it + 1) } + val cursor = MatrixCursor(columnNames) + while (rs.next()) { + val row = Array(columnCount) { rs.getObject(it + 1) } + cursor.addRow(row) + } + rs.close() + stmt.close() + return cursor + } + + // endregion + + // region Insert / Update / Delete + + override fun insert(table: String, conflictAlgorithm: Int, values: ContentValues): Long { + val keys = values.keySet().toList() + if (keys.isEmpty()) { + val sql = "INSERT${CONFLICT_VALUES[conflictAlgorithm]} INTO $table DEFAULT VALUES" + connection.createStatement().use { it.executeUpdate(sql) } + } else { + val columns = keys.joinToString(", ") + val placeholders = keys.joinToString(", ") { "?" } + val sql = "INSERT${CONFLICT_VALUES[conflictAlgorithm]} INTO $table ($columns) VALUES ($placeholders)" + val stmt = connection.prepareStatement(sql) + keys.forEachIndexed { index, key -> bindArg(stmt, index + 1, values.get(key)) } + stmt.executeUpdate() + stmt.close() + } + return connection.createStatement().use { s -> + s.executeQuery("SELECT last_insert_rowid()").use { rs -> + if (rs.next()) rs.getLong(1) else -1L + } + } + } + + override fun update(table: String, conflictAlgorithm: Int, values: ContentValues, whereClause: String?, whereArgs: Array?): Int { + val keys = values.keySet().toList() + val setClause = keys.joinToString(", ") { "$it = ?" } + val sql = buildString { + append("UPDATE${CONFLICT_VALUES[conflictAlgorithm]} $table SET $setClause") + if (!whereClause.isNullOrEmpty()) { + append(" WHERE $whereClause") + } + } + val stmt = connection.prepareStatement(sql) + var paramIndex = 1 + keys.forEach { key -> bindArg(stmt, paramIndex++, values.get(key)) } + whereArgs?.forEach { arg -> bindArg(stmt, paramIndex++, arg) } + val count = stmt.executeUpdate() + stmt.close() + return count + } + + override fun delete(table: String, whereClause: String?, whereArgs: Array?): Int { + val sql = buildString { + append("DELETE FROM $table") + if (!whereClause.isNullOrEmpty()) { + append(" WHERE $whereClause") + } + } + val stmt = connection.prepareStatement(sql) + whereArgs?.forEachIndexed { index, arg -> bindArg(stmt, index + 1, arg) } + val count = stmt.executeUpdate() + stmt.close() + return count + } + + // endregion + + // region ExecSQL + + override fun execSQL(sql: String) { + connection.createStatement().use { it.execute(sql) } + } + + override fun execSQL(sql: String, bindArgs: Array) { + val stmt = connection.prepareStatement(sql) + bindArgs(stmt, bindArgs) + stmt.execute() + stmt.close() + } + + // endregion + + // region CompileStatement + + override fun compileStatement(sql: String): SupportSQLiteStatement { + return JdbcSqliteStatement(connection.prepareStatement(sql), connection) + } + + // endregion + + // region Properties + + override var version: Int + get() { + return connection.createStatement().use { s -> + s.executeQuery("PRAGMA user_version").use { rs -> + if (rs.next()) rs.getInt(1) else 0 + } + } + } + set(value) { + connection.createStatement().use { it.execute("PRAGMA user_version = $value") } + } + + override val maximumSize: Long + get() { + val pageCount = connection.createStatement().use { s -> + s.executeQuery("PRAGMA max_page_count").use { rs -> + if (rs.next()) rs.getLong(1) else 0L + } + } + return pageSize * pageCount + } + + override fun setMaximumSize(numBytes: Long): Long { + var numPages = numBytes / pageSize + if (numBytes % pageSize != 0L) numPages++ + connection.createStatement().use { it.execute("PRAGMA max_page_count = $numPages") } + return maximumSize + } + + override var pageSize: Long + get() { + return connection.createStatement().use { s -> + s.executeQuery("PRAGMA page_size").use { rs -> + if (rs.next()) rs.getLong(1) else 4096L + } + } + } + set(value) { + connection.createStatement().use { it.execute("PRAGMA page_size = $value") } + } + + override val isReadOnly: Boolean + get() = false + + override val isOpen: Boolean + get() = !connection.isClosed + + override fun needUpgrade(newVersion: Int): Boolean = version < newVersion + + override val path: String? + get() = null + + override fun setLocale(locale: Locale) = Unit + + override fun setMaxSqlCacheSize(cacheSize: Int) = Unit + + override fun setForeignKeyConstraintsEnabled(enable: Boolean) { + connection.createStatement().use { it.execute("PRAGMA foreign_keys = ${if (enable) "ON" else "OFF"}") } + } + + override fun enableWriteAheadLogging(): Boolean = false + + override fun disableWriteAheadLogging() = Unit + + override val isWriteAheadLoggingEnabled: Boolean + get() = false + + override val attachedDbs: List>? + get() = null + + override val isDatabaseIntegrityOk: Boolean + get() = true + + override fun close() { + if (!connection.isClosed) { + connection.close() + } + } + + // endregion + + // region Helpers + + private fun bindArgs(stmt: PreparedStatement, args: Array) { + args.forEachIndexed { index, arg -> bindArg(stmt, index + 1, arg) } + } + + private fun bindArg(stmt: PreparedStatement, index: Int, arg: Any?) { + when (arg) { + null -> stmt.setNull(index, Types.NULL) + is String -> stmt.setString(index, arg) + is Long -> stmt.setLong(index, arg) + is Int -> stmt.setInt(index, arg) + is Short -> stmt.setShort(index, arg) + is Byte -> stmt.setByte(index, arg) + is Double -> stmt.setDouble(index, arg) + is Float -> stmt.setFloat(index, arg) + is ByteArray -> stmt.setBytes(index, arg) + is Boolean -> stmt.setInt(index, if (arg) 1 else 0) + else -> stmt.setString(index, arg.toString()) + } + } + + /** + * Captures binding operations from a [SupportSQLiteQuery] so they can be replayed onto a JDBC + * [PreparedStatement]. + */ + private class BindingCapture(argCount: Int) : SupportSQLiteProgram { + private val bindings = arrayOfNulls(argCount) + + override fun bindNull(index: Int) { bindings[index - 1] = NULL_SENTINEL } + override fun bindLong(index: Int, value: Long) { bindings[index - 1] = value } + override fun bindDouble(index: Int, value: Double) { bindings[index - 1] = value } + override fun bindString(index: Int, value: String) { bindings[index - 1] = value } + override fun bindBlob(index: Int, value: ByteArray) { bindings[index - 1] = value } + override fun clearBindings() { bindings.fill(null) } + override fun close() = Unit + + fun getArgs(): Array = bindings.map { if (it === NULL_SENTINEL) null else it }.toTypedArray() + + companion object { + private val NULL_SENTINEL = Object() + } + } + + // endregion +} + +/** + * A [SupportSQLiteStatement] backed by a JDBC [PreparedStatement]. + */ +class JdbcSqliteStatement( + private val statement: PreparedStatement, + private val connection: Connection +) : SupportSQLiteStatement { + + override fun execute() { + statement.execute() + } + + override fun executeUpdateDelete(): Int { + return statement.executeUpdate() + } + + override fun executeInsert(): Long { + statement.executeUpdate() + return connection.createStatement().use { s -> + s.executeQuery("SELECT last_insert_rowid()").use { rs -> + if (rs.next()) rs.getLong(1) else -1L + } + } + } + + override fun simpleQueryForLong(): Long { + val rs = statement.executeQuery() + return if (rs.next()) rs.getLong(1) else throw android.database.sqlite.SQLiteDoneException() + } + + override fun simpleQueryForString(): String? { + val rs = statement.executeQuery() + return if (rs.next()) rs.getString(1) else throw android.database.sqlite.SQLiteDoneException() + } + + override fun bindNull(index: Int) { + statement.setNull(index, Types.NULL) + } + + override fun bindLong(index: Int, value: Long) { + statement.setLong(index, value) + } + + override fun bindDouble(index: Int, value: Double) { + statement.setDouble(index, value) + } + + override fun bindString(index: Int, value: String) { + statement.setString(index, value) + } + + override fun bindBlob(index: Int, value: ByteArray) { + statement.setBytes(index, value) + } + + override fun clearBindings() { + statement.clearParameters() + } + + override fun close() { + statement.close() + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/testutil/SignalDatabaseMigrationRule.kt b/app/src/test/java/org/thoughtcrime/securesms/testutil/SignalDatabaseMigrationRule.kt index 5520608798..cfb7532fff 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/testutil/SignalDatabaseMigrationRule.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/testutil/SignalDatabaseMigrationRule.kt @@ -5,13 +5,11 @@ package org.thoughtcrime.securesms.testutil -import androidx.sqlite.db.SupportSQLiteDatabase -import androidx.sqlite.db.SupportSQLiteOpenHelper -import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory import androidx.test.core.app.ApplicationProvider import org.junit.rules.ExternalResource import org.thoughtcrime.securesms.database.SQLiteDatabase import org.thoughtcrime.securesms.database.helpers.SignalDatabaseMigrations +import org.thoughtcrime.securesms.testing.JdbcSqliteDatabase import org.thoughtcrime.securesms.testing.TestSignalSQLiteDatabase class SignalDatabaseMigrationRule(private val upgradedVersion: Int = 286) : ExternalResource() { @@ -40,25 +38,14 @@ class SignalDatabaseMigrationRule(private val upgradedVersion: Int = 286) : Exte companion object { /** - * Create an in-memory only database of a snapshot of V286. This includes - * all non-FTS tables, indexes, and triggers. + * Create an in-memory only database of a snapshot of V286. Uses sqlite-jdbc (org.xerial) to + * provide a modern SQLite with FTS5 and JSON1 support. This includes all non-FTS tables, + * indexes, and triggers. */ private fun inMemoryUpgradedDatabase(): SQLiteDatabase { - val configuration = SupportSQLiteOpenHelper.Configuration( - context = ApplicationProvider.getApplicationContext(), - name = "snapshot", - callback = object : SupportSQLiteOpenHelper.Callback(286) { - override fun onCreate(db: SupportSQLiteDatabase) { - SNAPSHOT_V286.forEach { db.execSQL(it.sql) } - } - override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) = Unit - }, - useNoBackupDirectory = false, - allowDataLossOnRecovery = true - ) - - val helper = FrameworkSQLiteOpenHelperFactory().create(configuration) - return TestSignalSQLiteDatabase(helper.writableDatabase) + val db = JdbcSqliteDatabase.createInMemory() + SNAPSHOT_V286.forEach { db.execSQL(it.sql) } + return TestSignalSQLiteDatabase(db) } private val SNAPSHOT_V286 = listOf( diff --git a/app/src/test/java/org/thoughtcrime/securesms/testutil/SignalDatabaseRule.kt b/app/src/test/java/org/thoughtcrime/securesms/testutil/SignalDatabaseRule.kt index 797f7f4180..6a0b3da7d2 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/testutil/SignalDatabaseRule.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/testutil/SignalDatabaseRule.kt @@ -5,16 +5,15 @@ package org.thoughtcrime.securesms.testutil -import androidx.sqlite.db.SupportSQLiteDatabase -import androidx.sqlite.db.SupportSQLiteOpenHelper -import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory import androidx.test.core.app.ApplicationProvider import io.mockk.every import io.mockk.mockkObject import io.mockk.unmockkObject import org.junit.rules.ExternalResource import org.thoughtcrime.securesms.database.SQLiteDatabase +import org.thoughtcrime.securesms.database.SearchTable import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.testing.JdbcSqliteDatabase import org.thoughtcrime.securesms.testing.TestSignalDatabase class SignalDatabaseRule : ExternalResource() { @@ -41,24 +40,16 @@ class SignalDatabaseRule : ExternalResource() { companion object { /** - * Create an in-memory only database mimicking one created fresh for Signal. This includes - * all non-FTS tables, indexes, and triggers. + * Create an in-memory only database mimicking one created fresh for Signal. Uses sqlite-jdbc + * (org.xerial) to provide a modern SQLite with FTS5 and JSON1 support, bypassing Robolectric's + * limited native SQLite. */ private fun inMemorySignalDatabase(): TestSignalDatabase { - val configuration = SupportSQLiteOpenHelper.Configuration( - context = ApplicationProvider.getApplicationContext(), - name = "test", - callback = object : SupportSQLiteOpenHelper.Callback(1) { - override fun onCreate(db: SupportSQLiteDatabase) = Unit - override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) = Unit - }, - useNoBackupDirectory = false, - allowDataLossOnRecovery = true - ) - - val helper = FrameworkSQLiteOpenHelperFactory().create(configuration) - val signalDatabase = TestSignalDatabase(ApplicationProvider.getApplicationContext(), helper) + val db = JdbcSqliteDatabase.createInMemory() + val signalDatabase = TestSignalDatabase(ApplicationProvider.getApplicationContext(), db, db) signalDatabase.onCreateTablesIndexesAndTriggers(signalDatabase.signalWritableDatabase) + SearchTable.CREATE_TABLE.forEach { signalDatabase.signalWritableDatabase.execSQL(it) } + SearchTable.CREATE_TRIGGERS.forEach { signalDatabase.signalWritableDatabase.execSQL(it) } return signalDatabase } diff --git a/gradle/test-libs.versions.toml b/gradle/test-libs.versions.toml index aa93ea8936..e62cf0c1ae 100644 --- a/gradle/test-libs.versions.toml +++ b/gradle/test-libs.versions.toml @@ -27,3 +27,4 @@ mockk-android = "io.mockk:mockk-android:1.13.17" square-mockwebserver = "com.squareup.okhttp3:mockwebserver:5.0.0-alpha.16" conscrypt-openjdk-uber = "org.conscrypt:conscrypt-openjdk-uber:2.5.2" diff-utils = "io.github.java-diff-utils:java-diff-utils:4.12" +sqlite-jdbc = "org.xerial:sqlite-jdbc:3.51.3.0" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 098c00ebfa..8b7c442653 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -16181,6 +16181,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -16921,6 +16926,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + +