Refactor database query functions out of LimitOffsetPagingSource
QueryItemCount() and QueryDatabase() has been moved into RoomPagingUtil
Test: ./gradlew room:room-paging:cC
Test: ./gradlew :room:integration-tests:room-testapp-kotlin:cC
Bug: 226465173
Change-Id: I20b82c68f970a8f8fce68f344634263cef98e0e8
diff --git a/room/room-paging/src/main/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt b/room/room-paging/src/main/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt
index e5935d6..023e5b6 100644
--- a/room/room-paging/src/main/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt
+++ b/room/room-paging/src/main/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt
@@ -25,9 +25,9 @@
import androidx.room.RoomSQLiteQuery
import androidx.room.getQueryDispatcher
import androidx.room.paging.util.ThreadSafeInvalidationObserver
-import androidx.room.paging.util.getLimit
-import androidx.room.paging.util.getOffset
import androidx.room.paging.util.getClippedRefreshKey
+import androidx.room.paging.util.queryDatabase
+import androidx.room.paging.util.queryItemCount
import androidx.room.withTransaction
import androidx.sqlite.db.SupportSQLiteQuery
import kotlinx.coroutines.withContext
@@ -75,13 +75,7 @@
if (tempCount < 0) {
initialLoad(params)
} else {
- // otherwise, it is a subsequent load
- val loadResult = loadFromDb(params, tempCount)
- // manually check if database has been updated. If so, the observers's
- // invalidation callback will invalidate this paging source
- db.invalidationTracker.refreshVersionsSync()
- @Suppress("UNCHECKED_CAST")
- if (invalid) INVALID as LoadResult.Invalid<Int, Value> else loadResult
+ nonInitialLoad(params, tempCount)
}
}
}
@@ -98,95 +92,22 @@
*/
private suspend fun initialLoad(params: LoadParams<Int>): LoadResult<Int, Value> {
return db.withTransaction {
- val tempCount = queryItemCount()
+ val tempCount = queryItemCount(sourceQuery, db)
itemCount.set(tempCount)
- loadFromDb(params, tempCount)
+ queryDatabase(params, sourceQuery, db, tempCount, ::convertRows)
}
}
- private suspend fun loadFromDb(
+ private suspend fun nonInitialLoad(
params: LoadParams<Int>,
- itemCount: Int,
+ tempCount: Int,
): LoadResult<Int, Value> {
- val key = params.key ?: 0
- val limit: Int = getLimit(params, key)
- val offset: Int = getOffset(params, key, itemCount)
- return queryDatabase(offset, limit, itemCount)
- }
-
- /**
- * calls RoomDatabase.query() to return a cursor and then calls convertRows() to extract and
- * return list of data
- *
- * throws [IllegalArgumentException] from [CursorUtil] if column does not exist
- *
- * @param offset offset parameter for LIMIT/OFFSET query. Bounded within user-supplied offset
- * if it is supplied
- *
- * @param limit limit parameter for LIMIT/OFFSET query. Bounded within user-supplied limit
- * if it is supplied
- */
- private suspend fun queryDatabase(
- offset: Int,
- limit: Int,
- itemCount: Int,
- ): LoadResult<Int, Value> {
- val limitOffsetQuery =
- "SELECT * FROM ( ${sourceQuery.sql} ) LIMIT $limit OFFSET $offset"
- val sqLiteQuery: RoomSQLiteQuery = RoomSQLiteQuery.acquire(
- limitOffsetQuery,
- sourceQuery.argCount
- )
- sqLiteQuery.copyArgumentsFrom(sourceQuery)
- val cursor = db.query(sqLiteQuery)
- val data: List<Value>
- try {
- data = convertRows(cursor)
- } finally {
- cursor.close()
- sqLiteQuery.release()
- }
- val nextPosToLoad = offset + data.size
- val nextKey =
- if (data.isEmpty() || data.size < limit || nextPosToLoad >= itemCount) {
- null
- } else {
- nextPosToLoad
- }
- val prevKey = if (offset <= 0 || data.isEmpty()) null else offset
- return LoadResult.Page(
- data = data,
- prevKey = prevKey,
- nextKey = nextKey,
- itemsBefore = offset,
- itemsAfter = maxOf(0, itemCount - nextPosToLoad)
- )
- }
-
- /**
- * returns count of requested items to calculate itemsAfter and itemsBefore for use in creating
- * LoadResult.Page<>
- *
- * throws error when the column value is null, the column type is not an integral type,
- * or the integer value is outside the range [Integer.MIN_VALUE, Integer.MAX_VALUE]
- */
- private fun queryItemCount(): Int {
- val countQuery = "SELECT COUNT(*) FROM ( ${sourceQuery.sql} )"
- val sqLiteQuery: RoomSQLiteQuery = RoomSQLiteQuery.acquire(
- countQuery,
- sourceQuery.argCount
- )
- sqLiteQuery.copyArgumentsFrom(sourceQuery)
- val cursor: Cursor = db.query(sqLiteQuery)
- try {
- if (cursor.moveToFirst()) {
- return cursor.getInt(0)
- }
- return 0
- } finally {
- cursor.close()
- sqLiteQuery.release()
- }
+ val loadResult = queryDatabase(params, sourceQuery, db, tempCount, ::convertRows)
+ // manually check if database has been updated. If so, the observers's
+ // invalidation callback will invalidate this paging source
+ db.invalidationTracker.refreshVersionsSync()
+ @Suppress("UNCHECKED_CAST")
+ return if (invalid) INVALID as LoadResult.Invalid<Int, Value> else loadResult
}
@NonNull
diff --git a/room/room-paging/src/main/kotlin/androidx/room/paging/util/RoomPagingUtil.kt b/room/room-paging/src/main/kotlin/androidx/room/paging/util/RoomPagingUtil.kt
index 3cff53b..2511fc6 100644
--- a/room/room-paging/src/main/kotlin/androidx/room/paging/util/RoomPagingUtil.kt
+++ b/room/room-paging/src/main/kotlin/androidx/room/paging/util/RoomPagingUtil.kt
@@ -17,9 +17,17 @@
package androidx.room.paging.util
+import android.database.Cursor
import androidx.annotation.RestrictTo
import androidx.paging.PagingSource
+import androidx.paging.PagingSource.LoadParams
+import androidx.paging.PagingSource.LoadParams.Prepend
+import androidx.paging.PagingSource.LoadParams.Append
+import androidx.paging.PagingSource.LoadParams.Refresh
+import androidx.paging.PagingSource.LoadResult
import androidx.paging.PagingState
+import androidx.room.RoomDatabase
+import androidx.room.RoomSQLiteQuery
/**
* Calculates query limit based on LoadType.
@@ -27,9 +35,9 @@
* Prepend: If requested loadSize is larger than available number of items to prepend, it will
* query with OFFSET = 0, LIMIT = prevKey
*/
-fun getLimit(params: PagingSource.LoadParams<Int>, key: Int): Int {
+fun getLimit(params: LoadParams<Int>, key: Int): Int {
return when (params) {
- is PagingSource.LoadParams.Prepend ->
+ is Prepend ->
if (key < params.loadSize) {
key
} else {
@@ -61,16 +69,16 @@
* viewed item, hypothetically [getClippedRefreshKey] may return key = 60. If loadSize = 10, then items
* 31-40 will be loaded.
*/
-fun getOffset(params: PagingSource.LoadParams<Int>, key: Int, itemCount: Int): Int {
+fun getOffset(params: LoadParams<Int>, key: Int, itemCount: Int): Int {
return when (params) {
- is PagingSource.LoadParams.Prepend ->
+ is Prepend ->
if (key < params.loadSize) {
0
} else {
key - params.loadSize
}
- is PagingSource.LoadParams.Append -> key
- is PagingSource.LoadParams.Refresh ->
+ is Append -> key
+ is Refresh ->
if (key >= itemCount) {
maxOf(0, itemCount - params.loadSize)
} else {
@@ -80,6 +88,94 @@
}
/**
+ * calls RoomDatabase.query() to return a cursor and then calls convertRows() to extract and
+ * return list of data
+ *
+ * throws [IllegalArgumentException] from CursorUtil if column does not exist
+ *
+ * @param params load params to calculate query limit and offset
+ *
+ * @param sourceQuery user provided [RoomSQLiteQuery] for database query
+ *
+ * @param db the [RoomDatabase] to query from
+ *
+ * @param itemCount the db row count, triggers a new PagingSource generation if itemCount changes,
+ * i.e. items are added / removed
+ *
+ * @param convertRows the function to iterate data with provided [Cursor] to return List<Value>
+ */
+fun <Value : Any> queryDatabase(
+ params: LoadParams<Int>,
+ sourceQuery: RoomSQLiteQuery,
+ db: RoomDatabase,
+ itemCount: Int,
+ convertRows: (Cursor) -> List<Value>,
+): LoadResult<Int, Value> {
+ val key = params.key ?: 0
+ val limit: Int = getLimit(params, key)
+ val offset: Int = getOffset(params, key, itemCount)
+ val limitOffsetQuery =
+ "SELECT * FROM ( ${sourceQuery.sql} ) LIMIT $limit OFFSET $offset"
+ val sqLiteQuery: RoomSQLiteQuery = RoomSQLiteQuery.acquire(
+ limitOffsetQuery,
+ sourceQuery.argCount
+ )
+ sqLiteQuery.copyArgumentsFrom(sourceQuery)
+ val cursor = db.query(sqLiteQuery)
+ val data: List<Value>
+ try {
+ data = convertRows(cursor)
+ } finally {
+ cursor.close()
+ sqLiteQuery.release()
+ }
+ val nextPosToLoad = offset + data.size
+ val nextKey =
+ if (data.isEmpty() || data.size < limit || nextPosToLoad >= itemCount) {
+ null
+ } else {
+ nextPosToLoad
+ }
+ val prevKey = if (offset <= 0 || data.isEmpty()) null else offset
+ return LoadResult.Page(
+ data = data,
+ prevKey = prevKey,
+ nextKey = nextKey,
+ itemsBefore = offset,
+ itemsAfter = maxOf(0, itemCount - nextPosToLoad)
+ )
+}
+
+/**
+ * returns count of requested items to calculate itemsAfter and itemsBefore for use in creating
+ * LoadResult.Page<>
+ *
+ * throws error when the column value is null, the column type is not an integral type,
+ * or the integer value is outside the range [Integer.MIN_VALUE, Integer.MAX_VALUE]
+ */
+fun queryItemCount(
+ sourceQuery: RoomSQLiteQuery,
+ db: RoomDatabase
+): Int {
+ val countQuery = "SELECT COUNT(*) FROM ( ${sourceQuery.sql} )"
+ val sqLiteQuery: RoomSQLiteQuery = RoomSQLiteQuery.acquire(
+ countQuery,
+ sourceQuery.argCount
+ )
+ sqLiteQuery.copyArgumentsFrom(sourceQuery)
+ val cursor: Cursor = db.query(sqLiteQuery)
+ try {
+ if (cursor.moveToFirst()) {
+ return cursor.getInt(0)
+ }
+ return 0
+ } finally {
+ cursor.close()
+ sqLiteQuery.release()
+ }
+}
+
+/**
* Returns the key for [PagingSource] for a non-initial REFRESH load.
*
* To prevent a negative key, key is clipped to 0 when the number of items available before