In this sample project, I have a DAO that is using Paging 2 on an FTS4 table to search it, using a user-supplied search expression:
@Query("SELECT snippet(paragraphsFts) FROM paragraphs JOIN paragraphsFts "+
"ON paragraphs.id == paragraphsFts.rowid WHERE paragraphsFts.prose "+
"MATCH :search ORDER BY sequence")
abstract fun filtered(search: String): DataSource.Factory<Int, String>
I have a BookRepository
with a filtered()
function that just passes through the call to the DAO, and I have a SearchViewModel
that converts the DataSource.Factory
to a LiveData
:
class SearchViewModel(search: String, repo: BookRepository) : ViewModel() {
val paragraphs = repo.filtered(search).toLiveData(pageSize = 15)
}
And I have a fragment that observes the results:
vm.paragraphs.observe(this.viewLifecycleOwner) {
adapter.submitList(it)
}
This works fine, so long as the user does not enter a search expression with a syntax error. In that case, Room throws an exception, and I cannot see where I can catch it.
2020-08-30 08:50:50.923 13948-14019 E/AndroidRuntime: FATAL EXCEPTION: arch_disk_io_3
Process: com.commonsware.room.pagedfts, PID: 13948
android.database.sqlite.SQLiteException: malformed MATCH expression: [-9] (code 1 SQLITE_ERROR)
at android.database.sqlite.SQLiteConnection.nativeExecuteForCursorWindow(Native Method)
at android.database.sqlite.SQLiteConnection.executeForCursorWindow(SQLiteConnection.java:942)
at android.database.sqlite.SQLiteSession.executeForCursorWindow(SQLiteSession.java:838)
at android.database.sqlite.SQLiteQuery.fillWindow(SQLiteQuery.java:62)
at android.database.sqlite.SQLiteCursor.fillWindow(SQLiteCursor.java:153)
at android.database.sqlite.SQLiteCursor.getCount(SQLiteCursor.java:140)
at android.database.AbstractCursor.moveToPosition(AbstractCursor.java:232)
at android.database.AbstractCursor.moveToFirst(AbstractCursor.java:271)
at androidx.room.paging.LimitOffsetDataSource.countItems(LimitOffsetDataSource.java:89)
at androidx.room.paging.LimitOffsetDataSource.loadInitial(LimitOffsetDataSource.java:119)
at androidx.paging.PositionalDataSource.dispatchLoadInitial(PositionalDataSource.java:286)
at androidx.paging.TiledPagedList.<init>(TiledPagedList.java:107)
at androidx.paging.PagedList.create(PagedList.java:229)
at androidx.paging.PagedList$Builder.build(PagedList.java:388)
at androidx.paging.LivePagedListBuilder$1.compute(LivePagedListBuilder.java:206)
at androidx.paging.LivePagedListBuilder$1.compute(LivePagedListBuilder.java:171)
at androidx.lifecycle.ComputableLiveData$2.run(ComputableLiveData.java:101)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:919)
The exception is occurring on a background thread used by the LiveData
created by toLiveData()
. So a try
/catch
in my paragraphs
declaration will not work, as the exception is not thrown during creation of the LiveData
. Wrapping my observe()
call in a try
/catch
does not work, as the exception is not thrown on that thread.
If I were not using Paging, and instead had my DAO's filtered()
function be a suspend
function, I would get the exception directly and can catch and handle it in the viewmodel. Or, if filtered()
were a blocking call, I would get the exception directly. Or, if filtered()
returned an RxJava type, I would get the exception through normal RxJava exception handling approaches (e.g., onError
lambdas). But I cannot quite figure out where I should be intercepting this exception when using Paging.
It's possible that Paging 3 has a solution for this, which would be great to know, though Paging 3 right now is still in alpha.
While I have not tried it, I suspect that if I supply my own Executor
to toLiveData()
that I could catch the exception there. Even if that is a possible workound, that seems like I am relying on a side effect (replacing how we schedule work on threads to be able to catch an exception).
LIKE
instead ofMATCH
to prevent this error? – ValkyrieMATCH
to enable full FTS syntax and to behave like a normal search engine. Suppose the user types infoo
.LIKE '%foo%'
matchesfood
, which is not typical search engine behavior. SQLite's documentation for FTS emphasizes the use ofMATCH
. – Kindliness