Skip to content

feat: FTS5 full-text message search#5373

Draft
jamesarich wants to merge 1 commit into
mainfrom
feat/fts5-message-search
Draft

feat: FTS5 full-text message search#5373
jamesarich wants to merge 1 commit into
mainfrom
feat/fts5-message-search

Conversation

@jamesarich
Copy link
Copy Markdown
Collaborator

Adds full-text search over message history using Room's FTS5 support (Room 3.0.0-alpha04).

Schema (migration 38→39)

  • message_text column added to Packet entity
  • PacketFts virtual table (@Fts5(contentEntity = Packet::class)) with auto-sync triggers
  • Backfill runs once on first DB open after migration

Search Stack

  • DAO: searchMessages() / searchMessagesInConversation() with FTS5 MATCH
  • Repository: Query sanitization (wraps terms in quotes to escape FTS5 operators)
  • ViewModel: Debounced search (300ms, min 2 chars) with StateFlow<List<Message>>
  • UI: M3 TopAppBar replacement pattern — search icon in message toolbar swaps to search bar

M3 Design Decisions

  • Uses TopAppBar-based search (not SearchBar composable) since this is in-conversation "find in page"
  • Result count shown as trailing support text
  • Clear button dismisses search and restores normal toolbar

Testing

  • Database JVM tests pass (FTS5 works with BundledSQLiteDriver)
  • Build, spotless, detekt all green

Leverage Room 3.0.0-alpha04's FTS5 support to enable searching
message history. Uses external content table backed by Packet entity.

Schema changes (migration 38→39):
- Add message_text column to Packet entity
- Create PacketFts virtual table with FTS5 index on message_text
- Content triggers auto-sync FTS index on INSERT/UPDATE/DELETE

Search stack:
- PacketDao: searchMessages/searchMessagesInConversation queries
- DatabaseManager: backfillSearchIndexIfNeeded on DB switch
- PacketRepository: searchMessages with FTS query sanitization
- MessageViewModel: debounced search (300ms, min 2 chars)
- MessageSearchBar: M3 TopAppBar replacement pattern

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions github-actions Bot added the enhancement New feature or request label May 6, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented May 6, 2026

❌ 5 Tests Failed:

Tests completed Failed Passed Skipped
2136 5 2131 0
View the top 1 failed test(s) by shortest run time
org.meshtastic.app.ui.NavigationAssemblyTest::verifyNavigationGraphsAssembleWithoutCrashing
Stack Traces | 27.3s run time
android.database.sqlite.SQLiteException: no such module: FTS5 (code 1 SQLITE_ERROR)
	at org.robolectric.nativeruntime.SQLiteConnectionNatives.nativeExecute(Native Method)
	at org.robolectric.shadows.ShadowNativeSQLiteConnection.lambda$nativeExecute$10(ShadowNativeSQLiteConnection.java:132)
	at org.robolectric.util.PerfStatsCollector.measure(PerfStatsCollector.java:83)
	at org.robolectric.shadows.ShadowNativeSQLiteConnection.nativeExecute(ShadowNativeSQLiteConnection.java:130)
	at android.database.sqlite.SQLiteConnection.nativeExecute(SQLiteConnection.java)
	at android.database.sqlite.SQLiteConnection.execute(SQLiteConnection.java:730)
	at android.database.sqlite.SQLiteSession.execute(SQLiteSession.java:621)
	at android.database.sqlite.SQLiteStatement.execute(SQLiteStatement.java:47)
	at androidx.sqlite.db.framework.FrameworkSQLiteStatement.execute(FrameworkSQLiteStatement.android.kt:30)
	at androidx.sqlite.driver.SupportSQLiteStatement$OtherSQLiteStatement.step(SupportSQLiteStatement.android.kt:540)
	at androidx.sqlite.SQLite__SQLite_nonWebKt.execSQL(SQLite.nonWeb.kt:26)
	at androidx.sqlite.SQLite.execSQL(Unknown Source)
	at org.meshtastic.core.database.MeshtasticDatabase_Impl$createOpenDelegate$_openDelegate$1.createAllTables(MeshtasticDatabase_Impl.kt:96)
	at androidx.room3.BaseRoomConnectionManager.onCreate(RoomConnectionManager.kt:190)
	at androidx.room3.BaseRoomConnectionManager.configureDatabase(RoomConnectionManager.kt:130)
	at androidx.room3.BaseRoomConnectionManager.access$configureDatabase(RoomConnectionManager.kt:38)
	at androidx.room3.BaseRoomConnectionManager$openLocked$$inlined$withLock$1.invokeSuspend(ExclusiveMutex.kt:97)
	at androidx.room3.BaseRoomConnectionManager$openLocked$$inlined$withLock$1.invoke(ExclusiveMutex.kt)
	at androidx.room3.coroutines.ReentrantMutexKt$withReentrantLock$2.invokeSuspend(ReentrantMutex.kt:38)
	at androidx.room3.coroutines.ReentrantMutexKt$withReentrantLock$2.invoke(ReentrantMutex.kt)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatched(Undispatched.kt:66)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:43)
	at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:497)
	at kotlinx.coroutines.BuildersKt.withContext(Unknown Source)
	at androidx.room3.coroutines.ReentrantMutexKt.withReentrantLock(ReentrantMutex.kt:38)
	at androidx.room3.BaseRoomConnectionManager.openLocked(RoomConnectionManager.kt:367)
	at androidx.room3.BaseRoomConnectionManager.access$openLocked(RoomConnectionManager.kt:38)
	at androidx.room3.BaseRoomConnectionManager$createConnectionFactory$1.invokeSuspend(RoomConnectionManager.kt:64)
	at androidx.room3.BaseRoomConnectionManager$createConnectionFactory$1.invoke(RoomConnectionManager.kt)
	at androidx.room3.coroutines.PassthroughConnectionPool$useConnection$2.invokeSuspend(PassthroughConnectionPool.kt:78)
	at androidx.room3.coroutines.PassthroughConnectionPool$useConnection$2.invoke(PassthroughConnectionPool.kt)
	at androidx.room3.coroutines.PassthroughConnectionPool$withInitializationLock$3.invokeSuspend(PassthroughConnectionPool.kt:121)
	at androidx.room3.coroutines.PassthroughConnectionPool$withInitializationLock$3.invoke(PassthroughConnectionPool.kt)
	at androidx.room3.coroutines.ReentrantMutexKt$withReentrantLock$2.invokeSuspend(ReentrantMutex.kt:38)
	at androidx.room3.coroutines.ReentrantMutexKt$withReentrantLock$2.invoke(ReentrantMutex.kt)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatched(Undispatched.kt:66)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:43)
	at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:497)
	at kotlinx.coroutines.BuildersKt.withContext(Unknown Source)
	at androidx.room3.coroutines.ReentrantMutexKt.withReentrantLock(ReentrantMutex.kt:38)
	at androidx.room3.coroutines.PassthroughConnectionPool.withInitializationLock(PassthroughConnectionPool.kt:121)
	at androidx.room3.coroutines.PassthroughConnectionPool.useConnection(PassthroughConnectionPool.kt:76)
	at androidx.room3.RoomConnectionManager.useConnection(RoomConnectionManager.android.kt:99)
	at androidx.room3.RoomDatabase.useConnection(RoomDatabase.android.kt:408)
	at androidx.room3.util.DBUtil__DBUtilKt$performSuspending$2.invokeSuspend(DBUtil.kt:208)
	at androidx.room3.util.DBUtil__DBUtilKt$performSuspending$2.invoke(DBUtil.kt)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatched(Undispatched.kt:66)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:43)
	at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:497)
	at kotlinx.coroutines.BuildersKt.withContext(Unknown Source)
	at androidx.room3.util.DBUtil__DBUtilKt.performSuspending(DBUtil.kt:47)
	at androidx.room3.util.DBUtil.performSuspending(Unknown Source)
	at org.meshtastic.core.database.dao.PacketDao_Impl.getAllUserPacketsForMigration(PacketDao_Impl.kt:2548)
	at org.meshtastic.core.database.DatabaseManager.backfillSearchIndexIfNeeded(DatabaseManager.kt:303)
	at org.meshtastic.core.database.DatabaseManager.access$backfillSearchIndexIfNeeded(DatabaseManager.kt:49)
	at org.meshtastic.core.database.DatabaseManager$switchActiveDatabase$2$4.invokeSuspend(DatabaseManager.kt:153)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:34)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:100)
	at kotlinx.coroutines.internal.LimitedDispatcher$Worker.run(LimitedDispatcher.kt:124)
	at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:89)
	at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:586)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:798)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:717)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:704)
View the full list of 4 ❄️ flaky test(s)
org.meshtastic.core.database.dao.MigrationTest::testMigrateChannelsByPSK_disambiguateByName

Flake rate in main: 13.33% (Passed 26 times, Failed 4 times)

Stack Traces | 21.2s run time
android.database.sqlite.SQLiteException: no such module: FTS5 (code 1 SQLITE_ERROR)
	at org.robolectric.nativeruntime.SQLiteConnectionNatives.nativeExecute(Native Method)
	at org.robolectric.shadows.ShadowNativeSQLiteConnection.lambda$nativeExecute$10(ShadowNativeSQLiteConnection.java:132)
	at org.robolectric.util.PerfStatsCollector.measure(PerfStatsCollector.java:83)
	at org.robolectric.shadows.ShadowNativeSQLiteConnection.nativeExecute(ShadowNativeSQLiteConnection.java:130)
	at android.database.sqlite.SQLiteConnection.nativeExecute(SQLiteConnection.java)
	at android.database.sqlite.SQLiteConnection.execute(SQLiteConnection.java:730)
	at android.database.sqlite.SQLiteSession.execute(SQLiteSession.java:621)
	at android.database.sqlite.SQLiteStatement.execute(SQLiteStatement.java:47)
	at androidx.sqlite.db.framework.FrameworkSQLiteStatement.execute(FrameworkSQLiteStatement.android.kt:30)
	at androidx.sqlite.driver.SupportSQLiteStatement$OtherSQLiteStatement.step(SupportSQLiteStatement.android.kt:540)
	at androidx.sqlite.SQLite__SQLite_nonWebKt.execSQL(SQLite.nonWeb.kt:26)
	at androidx.sqlite.SQLite.execSQL(Unknown Source)
	at org.meshtastic.core.database.MeshtasticDatabase_Impl$createOpenDelegate$_openDelegate$1.createAllTables(MeshtasticDatabase_Impl.kt:96)
	at androidx.room3.BaseRoomConnectionManager.onCreate(RoomConnectionManager.kt:190)
	at androidx.room3.BaseRoomConnectionManager.configureDatabase(RoomConnectionManager.kt:130)
	at androidx.room3.BaseRoomConnectionManager.access$configureDatabase(RoomConnectionManager.kt:38)
	at androidx.room3.BaseRoomConnectionManager$openLocked$$inlined$withLock$1.invokeSuspend(ExclusiveMutex.kt:97)
	at androidx.room3.BaseRoomConnectionManager$openLocked$$inlined$withLock$1.invoke(ExclusiveMutex.kt)
	at androidx.room3.coroutines.ReentrantMutexKt$withReentrantLock$2.invokeSuspend(ReentrantMutex.kt:38)
	at androidx.room3.coroutines.ReentrantMutexKt$withReentrantLock$2.invoke(ReentrantMutex.kt)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatched(Undispatched.kt:66)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:43)
	at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:497)
	at kotlinx.coroutines.BuildersKt.withContext(Unknown Source)
	at androidx.room3.coroutines.ReentrantMutexKt.withReentrantLock(ReentrantMutex.kt:38)
	at androidx.room3.BaseRoomConnectionManager.openLocked(RoomConnectionManager.kt:367)
	at androidx.room3.BaseRoomConnectionManager.access$openLocked(RoomConnectionManager.kt:38)
	at androidx.room3.BaseRoomConnectionManager$createConnectionFactory$1.invokeSuspend(RoomConnectionManager.kt:64)
	at androidx.room3.BaseRoomConnectionManager$createConnectionFactory$1.invoke(RoomConnectionManager.kt)
	at androidx.room3.coroutines.PassthroughConnectionPool$useConnection$2.invokeSuspend(PassthroughConnectionPool.kt:78)
	at androidx.room3.coroutines.PassthroughConnectionPool$useConnection$2.invoke(PassthroughConnectionPool.kt)
	at androidx.room3.coroutines.PassthroughConnectionPool$withInitializationLock$3.invokeSuspend(PassthroughConnectionPool.kt:121)
	at androidx.room3.coroutines.PassthroughConnectionPool$withInitializationLock$3.invoke(PassthroughConnectionPool.kt)
	at androidx.room3.coroutines.ReentrantMutexKt$withReentrantLock$2.invokeSuspend(ReentrantMutex.kt:38)
	at androidx.room3.coroutines.ReentrantMutexKt$withReentrantLock$2.invoke(ReentrantMutex.kt)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatched(Undispatched.kt:66)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:43)
	at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:497)
	at kotlinx.coroutines.BuildersKt.withContext(Unknown Source)
	at androidx.room3.coroutines.ReentrantMutexKt.withReentrantLock(ReentrantMutex.kt:38)
	at androidx.room3.coroutines.PassthroughConnectionPool.withInitializationLock(PassthroughConnectionPool.kt:121)
	at androidx.room3.coroutines.PassthroughConnectionPool.useConnection(PassthroughConnectionPool.kt:76)
	at androidx.room3.RoomConnectionManager.useConnection(RoomConnectionManager.android.kt:99)
	at androidx.room3.RoomDatabase.useConnection(RoomDatabase.android.kt:408)
	at androidx.room3.util.DBUtil__DBUtilKt$performSuspending$2.invokeSuspend(DBUtil.kt:208)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:34)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:100)
	at kotlinx.coroutines.internal.LimitedDispatcher$Worker.run(LimitedDispatcher.kt:124)
	at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:89)
	at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:586)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:798)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:717)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:704)
org.meshtastic.core.database.dao.MigrationTest::testMigrateChannelsByPSK_duplicatePSK

Flake rate in main: 13.33% (Passed 26 times, Failed 4 times)

Stack Traces | 0.173s run time
android.database.sqlite.SQLiteException: no such module: FTS5 (code 1 SQLITE_ERROR)
	at org.robolectric.nativeruntime.SQLiteConnectionNatives.nativeExecute(Native Method)
	at org.robolectric.shadows.ShadowNativeSQLiteConnection.lambda$nativeExecute$10(ShadowNativeSQLiteConnection.java:132)
	at org.robolectric.util.PerfStatsCollector.measure(PerfStatsCollector.java:83)
	at org.robolectric.shadows.ShadowNativeSQLiteConnection.nativeExecute(ShadowNativeSQLiteConnection.java:130)
	at android.database.sqlite.SQLiteConnection.nativeExecute(SQLiteConnection.java)
	at android.database.sqlite.SQLiteConnection.execute(SQLiteConnection.java:730)
	at android.database.sqlite.SQLiteSession.execute(SQLiteSession.java:621)
	at android.database.sqlite.SQLiteStatement.execute(SQLiteStatement.java:47)
	at androidx.sqlite.db.framework.FrameworkSQLiteStatement.execute(FrameworkSQLiteStatement.android.kt:30)
	at androidx.sqlite.driver.SupportSQLiteStatement$OtherSQLiteStatement.step(SupportSQLiteStatement.android.kt:540)
	at androidx.sqlite.SQLite__SQLite_nonWebKt.execSQL(SQLite.nonWeb.kt:26)
	at androidx.sqlite.SQLite.execSQL(Unknown Source)
	at org.meshtastic.core.database.MeshtasticDatabase_Impl$createOpenDelegate$_openDelegate$1.createAllTables(MeshtasticDatabase_Impl.kt:96)
	at androidx.room3.BaseRoomConnectionManager.onCreate(RoomConnectionManager.kt:190)
	at androidx.room3.BaseRoomConnectionManager.configureDatabase(RoomConnectionManager.kt:130)
	at androidx.room3.BaseRoomConnectionManager.access$configureDatabase(RoomConnectionManager.kt:38)
	at androidx.room3.BaseRoomConnectionManager$openLocked$$inlined$withLock$1.invokeSuspend(ExclusiveMutex.kt:97)
	at androidx.room3.BaseRoomConnectionManager$openLocked$$inlined$withLock$1.invoke(ExclusiveMutex.kt)
	at androidx.room3.coroutines.ReentrantMutexKt$withReentrantLock$2.invokeSuspend(ReentrantMutex.kt:38)
	at androidx.room3.coroutines.ReentrantMutexKt$withReentrantLock$2.invoke(ReentrantMutex.kt)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatched(Undispatched.kt:66)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:43)
	at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:497)
	at kotlinx.coroutines.BuildersKt.withContext(Unknown Source)
	at androidx.room3.coroutines.ReentrantMutexKt.withReentrantLock(ReentrantMutex.kt:38)
	at androidx.room3.BaseRoomConnectionManager.openLocked(RoomConnectionManager.kt:367)
	at androidx.room3.BaseRoomConnectionManager.access$openLocked(RoomConnectionManager.kt:38)
	at androidx.room3.BaseRoomConnectionManager$createConnectionFactory$1.invokeSuspend(RoomConnectionManager.kt:64)
	at androidx.room3.BaseRoomConnectionManager$createConnectionFactory$1.invoke(RoomConnectionManager.kt)
	at androidx.room3.coroutines.PassthroughConnectionPool$useConnection$2.invokeSuspend(PassthroughConnectionPool.kt:78)
	at androidx.room3.coroutines.PassthroughConnectionPool$useConnection$2.invoke(PassthroughConnectionPool.kt)
	at androidx.room3.coroutines.PassthroughConnectionPool$withInitializationLock$3.invokeSuspend(PassthroughConnectionPool.kt:121)
	at androidx.room3.coroutines.PassthroughConnectionPool$withInitializationLock$3.invoke(PassthroughConnectionPool.kt)
	at androidx.room3.coroutines.ReentrantMutexKt$withReentrantLock$2.invokeSuspend(ReentrantMutex.kt:38)
	at androidx.room3.coroutines.ReentrantMutexKt$withReentrantLock$2.invoke(ReentrantMutex.kt)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatched(Undispatched.kt:66)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:43)
	at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:497)
	at kotlinx.coroutines.BuildersKt.withContext(Unknown Source)
	at androidx.room3.coroutines.ReentrantMutexKt.withReentrantLock(ReentrantMutex.kt:38)
	at androidx.room3.coroutines.PassthroughConnectionPool.withInitializationLock(PassthroughConnectionPool.kt:121)
	at androidx.room3.coroutines.PassthroughConnectionPool.useConnection(PassthroughConnectionPool.kt:76)
	at androidx.room3.RoomConnectionManager.useConnection(RoomConnectionManager.android.kt:99)
	at androidx.room3.RoomDatabase.useConnection(RoomDatabase.android.kt:408)
	at androidx.room3.util.DBUtil__DBUtilKt$performSuspending$2.invokeSuspend(DBUtil.kt:208)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:34)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:100)
	at kotlinx.coroutines.internal.LimitedDispatcher$Worker.run(LimitedDispatcher.kt:124)
	at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:89)
	at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:586)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:798)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:717)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:704)
org.meshtastic.core.database.dao.MigrationTest::testMigrateChannelsByPSK_preferSameIndexIfStillAmbiguous

Flake rate in main: 13.33% (Passed 26 times, Failed 4 times)

Stack Traces | 0.213s run time
android.database.sqlite.SQLiteException: no such module: FTS5 (code 1 SQLITE_ERROR)
	at org.robolectric.nativeruntime.SQLiteConnectionNatives.nativeExecute(Native Method)
	at org.robolectric.shadows.ShadowNativeSQLiteConnection.lambda$nativeExecute$10(ShadowNativeSQLiteConnection.java:132)
	at org.robolectric.util.PerfStatsCollector.measure(PerfStatsCollector.java:83)
	at org.robolectric.shadows.ShadowNativeSQLiteConnection.nativeExecute(ShadowNativeSQLiteConnection.java:130)
	at android.database.sqlite.SQLiteConnection.nativeExecute(SQLiteConnection.java)
	at android.database.sqlite.SQLiteConnection.execute(SQLiteConnection.java:730)
	at android.database.sqlite.SQLiteSession.execute(SQLiteSession.java:621)
	at android.database.sqlite.SQLiteStatement.execute(SQLiteStatement.java:47)
	at androidx.sqlite.db.framework.FrameworkSQLiteStatement.execute(FrameworkSQLiteStatement.android.kt:30)
	at androidx.sqlite.driver.SupportSQLiteStatement$OtherSQLiteStatement.step(SupportSQLiteStatement.android.kt:540)
	at androidx.sqlite.SQLite__SQLite_nonWebKt.execSQL(SQLite.nonWeb.kt:26)
	at androidx.sqlite.SQLite.execSQL(Unknown Source)
	at org.meshtastic.core.database.MeshtasticDatabase_Impl$createOpenDelegate$_openDelegate$1.createAllTables(MeshtasticDatabase_Impl.kt:96)
	at androidx.room3.BaseRoomConnectionManager.onCreate(RoomConnectionManager.kt:190)
	at androidx.room3.BaseRoomConnectionManager.configureDatabase(RoomConnectionManager.kt:130)
	at androidx.room3.BaseRoomConnectionManager.access$configureDatabase(RoomConnectionManager.kt:38)
	at androidx.room3.BaseRoomConnectionManager$openLocked$$inlined$withLock$1.invokeSuspend(ExclusiveMutex.kt:97)
	at androidx.room3.BaseRoomConnectionManager$openLocked$$inlined$withLock$1.invoke(ExclusiveMutex.kt)
	at androidx.room3.coroutines.ReentrantMutexKt$withReentrantLock$2.invokeSuspend(ReentrantMutex.kt:38)
	at androidx.room3.coroutines.ReentrantMutexKt$withReentrantLock$2.invoke(ReentrantMutex.kt)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatched(Undispatched.kt:66)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:43)
	at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:497)
	at kotlinx.coroutines.BuildersKt.withContext(Unknown Source)
	at androidx.room3.coroutines.ReentrantMutexKt.withReentrantLock(ReentrantMutex.kt:38)
	at androidx.room3.BaseRoomConnectionManager.openLocked(RoomConnectionManager.kt:367)
	at androidx.room3.BaseRoomConnectionManager.access$openLocked(RoomConnectionManager.kt:38)
	at androidx.room3.BaseRoomConnectionManager$createConnectionFactory$1.invokeSuspend(RoomConnectionManager.kt:64)
	at androidx.room3.BaseRoomConnectionManager$createConnectionFactory$1.invoke(RoomConnectionManager.kt)
	at androidx.room3.coroutines.PassthroughConnectionPool$useConnection$2.invokeSuspend(PassthroughConnectionPool.kt:78)
	at androidx.room3.coroutines.PassthroughConnectionPool$useConnection$2.invoke(PassthroughConnectionPool.kt)
	at androidx.room3.coroutines.PassthroughConnectionPool$withInitializationLock$3.invokeSuspend(PassthroughConnectionPool.kt:121)
	at androidx.room3.coroutines.PassthroughConnectionPool$withInitializationLock$3.invoke(PassthroughConnectionPool.kt)
	at androidx.room3.coroutines.ReentrantMutexKt$withReentrantLock$2.invokeSuspend(ReentrantMutex.kt:38)
	at androidx.room3.coroutines.ReentrantMutexKt$withReentrantLock$2.invoke(ReentrantMutex.kt)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatched(Undispatched.kt:66)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:43)
	at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:497)
	at kotlinx.coroutines.BuildersKt.withContext(Unknown Source)
	at androidx.room3.coroutines.ReentrantMutexKt.withReentrantLock(ReentrantMutex.kt:38)
	at androidx.room3.coroutines.PassthroughConnectionPool.withInitializationLock(PassthroughConnectionPool.kt:121)
	at androidx.room3.coroutines.PassthroughConnectionPool.useConnection(PassthroughConnectionPool.kt:76)
	at androidx.room3.RoomConnectionManager.useConnection(RoomConnectionManager.android.kt:99)
	at androidx.room3.RoomDatabase.useConnection(RoomDatabase.android.kt:408)
	at androidx.room3.util.DBUtil__DBUtilKt$performSuspending$2.invokeSuspend(DBUtil.kt:208)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:34)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:100)
	at kotlinx.coroutines.internal.LimitedDispatcher$Worker.run(LimitedDispatcher.kt:124)
	at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:89)
	at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:586)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:798)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:717)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:704)
org.meshtastic.core.database.dao.MigrationTest::testMigrateChannelsByPSK_reorder

Flake rate in main: 13.33% (Passed 26 times, Failed 4 times)

Stack Traces | 0.22s run time
android.database.sqlite.SQLiteException: no such module: FTS5 (code 1 SQLITE_ERROR)
	at org.robolectric.nativeruntime.SQLiteConnectionNatives.nativeExecute(Native Method)
	at org.robolectric.shadows.ShadowNativeSQLiteConnection.lambda$nativeExecute$10(ShadowNativeSQLiteConnection.java:132)
	at org.robolectric.util.PerfStatsCollector.measure(PerfStatsCollector.java:83)
	at org.robolectric.shadows.ShadowNativeSQLiteConnection.nativeExecute(ShadowNativeSQLiteConnection.java:130)
	at android.database.sqlite.SQLiteConnection.nativeExecute(SQLiteConnection.java)
	at android.database.sqlite.SQLiteConnection.execute(SQLiteConnection.java:730)
	at android.database.sqlite.SQLiteSession.execute(SQLiteSession.java:621)
	at android.database.sqlite.SQLiteStatement.execute(SQLiteStatement.java:47)
	at androidx.sqlite.db.framework.FrameworkSQLiteStatement.execute(FrameworkSQLiteStatement.android.kt:30)
	at androidx.sqlite.driver.SupportSQLiteStatement$OtherSQLiteStatement.step(SupportSQLiteStatement.android.kt:540)
	at androidx.sqlite.SQLite__SQLite_nonWebKt.execSQL(SQLite.nonWeb.kt:26)
	at androidx.sqlite.SQLite.execSQL(Unknown Source)
	at org.meshtastic.core.database.MeshtasticDatabase_Impl$createOpenDelegate$_openDelegate$1.createAllTables(MeshtasticDatabase_Impl.kt:96)
	at androidx.room3.BaseRoomConnectionManager.onCreate(RoomConnectionManager.kt:190)
	at androidx.room3.BaseRoomConnectionManager.configureDatabase(RoomConnectionManager.kt:130)
	at androidx.room3.BaseRoomConnectionManager.access$configureDatabase(RoomConnectionManager.kt:38)
	at androidx.room3.BaseRoomConnectionManager$openLocked$$inlined$withLock$1.invokeSuspend(ExclusiveMutex.kt:97)
	at androidx.room3.BaseRoomConnectionManager$openLocked$$inlined$withLock$1.invoke(ExclusiveMutex.kt)
	at androidx.room3.coroutines.ReentrantMutexKt$withReentrantLock$2.invokeSuspend(ReentrantMutex.kt:38)
	at androidx.room3.coroutines.ReentrantMutexKt$withReentrantLock$2.invoke(ReentrantMutex.kt)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatched(Undispatched.kt:66)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:43)
	at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:497)
	at kotlinx.coroutines.BuildersKt.withContext(Unknown Source)
	at androidx.room3.coroutines.ReentrantMutexKt.withReentrantLock(ReentrantMutex.kt:38)
	at androidx.room3.BaseRoomConnectionManager.openLocked(RoomConnectionManager.kt:367)
	at androidx.room3.BaseRoomConnectionManager.access$openLocked(RoomConnectionManager.kt:38)
	at androidx.room3.BaseRoomConnectionManager$createConnectionFactory$1.invokeSuspend(RoomConnectionManager.kt:64)
	at androidx.room3.BaseRoomConnectionManager$createConnectionFactory$1.invoke(RoomConnectionManager.kt)
	at androidx.room3.coroutines.PassthroughConnectionPool$useConnection$2.invokeSuspend(PassthroughConnectionPool.kt:78)
	at androidx.room3.coroutines.PassthroughConnectionPool$useConnection$2.invoke(PassthroughConnectionPool.kt)
	at androidx.room3.coroutines.PassthroughConnectionPool$withInitializationLock$3.invokeSuspend(PassthroughConnectionPool.kt:121)
	at androidx.room3.coroutines.PassthroughConnectionPool$withInitializationLock$3.invoke(PassthroughConnectionPool.kt)
	at androidx.room3.coroutines.ReentrantMutexKt$withReentrantLock$2.invokeSuspend(ReentrantMutex.kt:38)
	at androidx.room3.coroutines.ReentrantMutexKt$withReentrantLock$2.invoke(ReentrantMutex.kt)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatched(Undispatched.kt:66)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:43)
	at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:497)
	at kotlinx.coroutines.BuildersKt.withContext(Unknown Source)
	at androidx.room3.coroutines.ReentrantMutexKt.withReentrantLock(ReentrantMutex.kt:38)
	at androidx.room3.coroutines.PassthroughConnectionPool.withInitializationLock(PassthroughConnectionPool.kt:121)
	at androidx.room3.coroutines.PassthroughConnectionPool.useConnection(PassthroughConnectionPool.kt:76)
	at androidx.room3.RoomConnectionManager.useConnection(RoomConnectionManager.android.kt:99)
	at androidx.room3.RoomDatabase.useConnection(RoomDatabase.android.kt:408)
	at androidx.room3.util.DBUtil__DBUtilKt$performSuspending$2.invokeSuspend(DBUtil.kt:208)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:34)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:100)
	at kotlinx.coroutines.internal.LimitedDispatcher$Worker.run(LimitedDispatcher.kt:124)
	at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:89)
	at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:586)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:798)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:717)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:704)

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@jamesarich jamesarich added this to the 2.8.0 milestone May 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant