Add new `convertRows()` methods that wrap an SQLiteStatement with a Cursor.

This CL refactors LimitOffsetDataSource and LimitOffsetPagingSource for backwards compatibility by adding a new `convertRows` method in each API that allows generating code using the new SQLite driver API for Paging2 sources.

Bug: 319660042
Test: DaoKotlinCodeGenTest.kt
Change-Id: I526504dd4760f8ada561d6ef38b99e830f666ae6
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/paging/LimitOffsetDataSourceTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/paging/LimitOffsetDataSourceTest.java
index cf95bce..94984a6 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/paging/LimitOffsetDataSourceTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/paging/LimitOffsetDataSourceTest.java
@@ -16,10 +16,9 @@
 
 package androidx.room.integration.testapp.paging;
 
-import static junit.framework.Assert.assertFalse;
-
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertFalse;
 
 import androidx.annotation.NonNull;
 import androidx.room.integration.testapp.test.TestDatabaseTest;
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt b/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt
index 07a1f63..0ce5c3f 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt
@@ -303,6 +303,7 @@
     val DB_UTIL_DROP_FTS_SYNC_TRIGGERS = RoomTypeNames.DB_UTIL.packageMember("dropFtsSyncTriggers")
     val DB_UTIL_PERFORM_SUSPENDING = RoomTypeNames.DB_UTIL.packageMember("performSuspending")
     val DB_UTIL_PERFORM_BLOCKING = RoomTypeNames.DB_UTIL.packageMember("performBlocking")
+    val DB_UTIL_SUPPORT_DB_TO_CONNECTION = RoomTypeNames.DB_UTIL.packageMember("toSQLiteConnection")
     val DB_UTIL_PERFORM_IN_TRANSACTION_SUSPENDING =
         RoomTypeNames.DB_UTIL.packageMember("performInTransactionSuspending")
     val CURSOR_UTIL_GET_COLUMN_INDEX = RoomTypeNames.CURSOR_UTIL.packageMember("getColumnIndex")
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/binderprovider/MultiTypedPagingSourceQueryResultBinderProvider.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/binderprovider/MultiTypedPagingSourceQueryResultBinderProvider.kt
index ebc088f..de31dcf 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/binderprovider/MultiTypedPagingSourceQueryResultBinderProvider.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/binderprovider/MultiTypedPagingSourceQueryResultBinderProvider.kt
@@ -31,6 +31,7 @@
 import androidx.room.solver.TypeAdapterExtras
 import androidx.room.solver.query.result.ListQueryResultAdapter
 import androidx.room.solver.query.result.MultiTypedPagingSourceQueryResultBinder
+import androidx.room.solver.query.result.Paging3PagingSourceQueryResultBinder
 import androidx.room.solver.query.result.QueryResultBinder
 
 class MultiTypedPagingSourceQueryResultBinderProvider(
@@ -70,8 +71,8 @@
             ((listAdapter?.accessedTableNames() ?: emptyList()) + query.tables.map { it.name })
                 .toSet()
 
-        val convertRowsOverrideInfo =
-            if (pagingSourceTypeName == PagingTypeNames.PAGING_SOURCE) {
+        return if (pagingSourceTypeName == PagingTypeNames.PAGING_SOURCE) {
+            val convertRowsOverrideInfo =
                 ConvertRowsOverrideInfo(
                     method = convertExecutableElement,
                     continuationParamName = convertExecutableElement.parameters.last().name,
@@ -79,16 +80,19 @@
                         context.processingEnv.getDeclaredType(roomPagingSourceTypeElement, typeArg),
                     returnTypeName = LIST.parametrizedBy(typeArg.asTypeName())
                 )
-            } else {
-                null
-            }
-
-        return MultiTypedPagingSourceQueryResultBinder(
-            listAdapter = listAdapter,
-            tableNames = tableNames,
-            className = roomPagingClassName,
-            convertRowsOverrideInfo = convertRowsOverrideInfo
-        )
+            Paging3PagingSourceQueryResultBinder(
+                listAdapter = listAdapter,
+                tableNames = tableNames,
+                className = roomPagingClassName,
+                convertRowsOverrideInfo = convertRowsOverrideInfo
+            )
+        } else {
+            MultiTypedPagingSourceQueryResultBinder(
+                listAdapter = listAdapter,
+                tableNames = tableNames,
+                className = roomPagingClassName
+            )
+        }
     }
 
     override fun matches(declared: XType): Boolean {
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/CursorQueryResultBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/CursorQueryResultBinder.kt
index 1d2de07..258d8f6 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/CursorQueryResultBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/CursorQueryResultBinder.kt
@@ -17,14 +17,19 @@
 package androidx.room.solver.query.result
 
 import androidx.room.compiler.codegen.XPropertySpec
+import androidx.room.compiler.codegen.XTypeName
 import androidx.room.ext.AndroidTypeNames
 import androidx.room.solver.CodeGenScope
 
 class CursorQueryResultBinder : QueryResultBinder(NO_OP_RESULT_ADAPTER) {
+
+    override val usesCompatQueryWriter = true
+
     override fun convertAndReturn(
-        roomSQLiteQueryVar: String,
-        canReleaseQuery: Boolean,
+        sqlQueryVar: String,
         dbProperty: XPropertySpec,
+        bindStatement: (CodeGenScope.(String) -> Unit)?,
+        returnTypeName: XTypeName,
         inTransaction: Boolean,
         scope: CodeGenScope
     ) {
@@ -42,7 +47,7 @@
                 AndroidTypeNames.CURSOR,
                 "%N.query(%L)",
                 dbProperty,
-                roomSQLiteQueryVar
+                sqlQueryVar
             )
             transactionWrapper?.commitTransaction()
             addStatement("return %L", resultName)
@@ -50,6 +55,19 @@
         }
     }
 
+    override fun convertAndReturn(
+        roomSQLiteQueryVar: String,
+        canReleaseQuery: Boolean,
+        dbProperty: XPropertySpec,
+        inTransaction: Boolean,
+        scope: CodeGenScope
+    ) {
+        error(
+            "This convertAndReturn() should never be invoked, it will be removed once " +
+                "migration to drivers is completed."
+        )
+    }
+
     companion object {
         private val NO_OP_RESULT_ADAPTER =
             object : QueryResultAdapter(emptyList()) {
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/DataSourceFactoryQueryResultBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/DataSourceFactoryQueryResultBinder.kt
index 2204dac..89ab607 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/DataSourceFactoryQueryResultBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/DataSourceFactoryQueryResultBinder.kt
@@ -30,10 +30,15 @@
 
     val typeName: XTypeName = positionalDataSourceQueryResultBinder.itemTypeName
 
+    override val usesCompatQueryWriter = true
+
+    override fun isMigratedToDriver(): Boolean = true
+
     override fun convertAndReturn(
-        roomSQLiteQueryVar: String,
-        canReleaseQuery: Boolean,
+        sqlQueryVar: String,
         dbProperty: XPropertySpec,
+        bindStatement: (CodeGenScope.(String) -> Unit)?,
+        returnTypeName: XTypeName,
         inTransaction: Boolean,
         scope: CodeGenScope
     ) {
@@ -48,8 +53,10 @@
                             )
                         )
                         addCreateMethod(
-                            roomSQLiteQueryVar = roomSQLiteQueryVar,
+                            roomSQLiteQueryVar = sqlQueryVar,
                             dbProperty = dbProperty,
+                            bindStatement = bindStatement,
+                            returnTypeName = returnTypeName,
                             inTransaction = inTransaction,
                             scope = scope
                         )
@@ -59,11 +66,26 @@
         }
     }
 
+    override fun convertAndReturn(
+        roomSQLiteQueryVar: String,
+        canReleaseQuery: Boolean,
+        dbProperty: XPropertySpec,
+        inTransaction: Boolean,
+        scope: CodeGenScope
+    ) {
+        error(
+            "This convertAndReturn() should never be invoked, it will be removed once " +
+                "migration to drivers is completed."
+        )
+    }
+
     private fun XTypeSpec.Builder.addCreateMethod(
         roomSQLiteQueryVar: String,
         dbProperty: XPropertySpec,
         inTransaction: Boolean,
-        scope: CodeGenScope
+        scope: CodeGenScope,
+        bindStatement: (CodeGenScope.(String) -> Unit)?,
+        returnTypeName: XTypeName
     ) {
         addFunction(
             XFunSpec.builder(
@@ -75,9 +97,10 @@
                     returns(positionalDataSourceQueryResultBinder.typeName)
                     val countedBinderScope = scope.fork()
                     positionalDataSourceQueryResultBinder.convertAndReturn(
-                        roomSQLiteQueryVar = roomSQLiteQueryVar,
-                        canReleaseQuery = true,
+                        sqlQueryVar = roomSQLiteQueryVar,
                         dbProperty = dbProperty,
+                        bindStatement = bindStatement,
+                        returnTypeName = returnTypeName,
                         inTransaction = inTransaction,
                         scope = countedBinderScope
                     )
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MultiTypedPagingSourceQueryResultBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MultiTypedPagingSourceQueryResultBinder.kt
index 33f05e1..b0ac1de4 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MultiTypedPagingSourceQueryResultBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MultiTypedPagingSourceQueryResultBinder.kt
@@ -16,44 +16,33 @@
 
 package androidx.room.solver.query.result
 
-import androidx.room.compiler.codegen.CodeLanguage
 import androidx.room.compiler.codegen.VisibilityModifier
 import androidx.room.compiler.codegen.XClassName
-import androidx.room.compiler.codegen.XCodeBlock
-import androidx.room.compiler.codegen.XCodeBlock.Builder.Companion.applyTo
 import androidx.room.compiler.codegen.XFunSpec
 import androidx.room.compiler.codegen.XPropertySpec
 import androidx.room.compiler.codegen.XTypeName
 import androidx.room.compiler.codegen.XTypeSpec
-import androidx.room.ext.AndroidTypeNames
 import androidx.room.ext.CommonTypeNames
-import androidx.room.ext.Function1TypeSpec
-import androidx.room.ext.InvokeWithLambdaParameter
-import androidx.room.ext.KotlinTypeNames
-import androidx.room.ext.LambdaSpec
-import androidx.room.ext.RoomMemberNames.DB_UTIL_PERFORM_SUSPENDING
-import androidx.room.ext.RoomTypeNames.RAW_QUERY
 import androidx.room.ext.SQLiteDriverTypeNames
 import androidx.room.solver.CodeGenScope
-import androidx.room.solver.binderprovider.ConvertRowsOverrideInfo
 
 /**
- * This Binder binds queries directly to native Paging3 PagingSource (i.e.
- * [androidx.room.paging.LimitOffsetPagingSource]) or its subclasses such as
- * [androidx.room.paging.guava.LimitOffsetListenableFuturePagingSource]. Used solely by Paging3.
+ * This Binder binds queries directly to Non-KMP compatible Paging3 PagingSource Binders. Used
+ * solely by Non-KMP Paging3.
  */
 class MultiTypedPagingSourceQueryResultBinder(
     private val listAdapter: ListQueryResultAdapter?,
     private val tableNames: Set<String>,
-    className: XClassName,
-    val convertRowsOverrideInfo: ConvertRowsOverrideInfo?
+    className: XClassName
 ) : QueryResultBinder(listAdapter) {
 
     private val itemTypeName: XTypeName =
         listAdapter?.rowAdapters?.firstOrNull()?.out?.asTypeName() ?: XTypeName.ANY_OBJECT
     private val pagingSourceTypeName: XTypeName = className.parametrizedBy(itemTypeName)
 
-    override fun isMigratedToDriver(): Boolean = convertRowsOverrideInfo != null
+    override val usesCompatQueryWriter = true
+
+    override fun isMigratedToDriver(): Boolean = true
 
     override fun convertAndReturn(
         roomSQLiteQueryVar: String,
@@ -62,12 +51,26 @@
         inTransaction: Boolean,
         scope: CodeGenScope
     ) {
+        error(
+            "This convertAndReturn() should never be invoked, it will be removed once " +
+                "migration to drivers is completed."
+        )
+    }
+
+    override fun convertAndReturn(
+        sqlQueryVar: String,
+        dbProperty: XPropertySpec,
+        bindStatement: (CodeGenScope.(String) -> Unit)?,
+        returnTypeName: XTypeName,
+        inTransaction: Boolean,
+        scope: CodeGenScope
+    ) {
         scope.builder.apply {
             val tableNamesList = tableNames.joinToString(", ") { "\"$it\"" }
             val pagingSourceSpec =
                 XTypeSpec.anonymousClassBuilder(
                         argsFormat = "%L, %N, %L",
-                        roomSQLiteQueryVar,
+                        sqlQueryVar,
                         dbProperty,
                         tableNamesList
                     )
@@ -81,11 +84,11 @@
                                 )
                                 .apply {
                                     val rowsScope = scope.fork()
-                                    val cursorParamName = "cursor"
+                                    val cursorParamName = "statement"
                                     val resultVar = scope.getTmpVar("_result")
                                     returns(CommonTypeNames.LIST.parametrizedBy(itemTypeName))
                                     addParameter(
-                                        typeName = AndroidTypeNames.CURSOR,
+                                        typeName = SQLiteDriverTypeNames.STATEMENT,
                                         name = cursorParamName
                                     )
                                     listAdapter?.convert(resultVar, cursorParamName, rowsScope)
@@ -99,203 +102,4 @@
             addStatement("return %L", pagingSourceSpec)
         }
     }
-
-    override fun convertAndReturn(
-        sqlQueryVar: String,
-        dbProperty: XPropertySpec,
-        bindStatement: (CodeGenScope.(String) -> Unit)?,
-        returnTypeName: XTypeName,
-        inTransaction: Boolean,
-        scope: CodeGenScope
-    ) {
-        checkNotNull(convertRowsOverrideInfo) {
-            "This version of `convertAndReturn` should only be called when the binder is for the " +
-                "base PagingSource. "
-        }
-        val rawQueryVarName = scope.getTmpVar("_rawQuery")
-        val stmtVarName = scope.getTmpVar("_stmt")
-
-        when (scope.language) {
-            CodeLanguage.JAVA -> {
-                val assignExpr =
-                    if (bindStatement != null) {
-                        XCodeBlock.ofNewInstance(
-                            typeName = RAW_QUERY,
-                            "%L, %L",
-                            sqlQueryVar,
-                            Function1TypeSpec(
-                                parameterTypeName = SQLiteDriverTypeNames.STATEMENT,
-                                parameterName = stmtVarName,
-                                returnTypeName = KotlinTypeNames.UNIT
-                            ) {
-                                val functionScope = scope.fork()
-                                functionScope.builder
-                                    .apply { bindStatement.invoke(functionScope, stmtVarName) }
-                                    .build()
-                                addCode(functionScope.generate())
-                                addStatement("return %T.INSTANCE", KotlinTypeNames.UNIT)
-                            }
-                        )
-                    } else {
-                        XCodeBlock.ofNewInstance(typeName = RAW_QUERY, "%L", sqlQueryVar)
-                    }
-                scope.builder.apply {
-                    addLocalVariable(
-                        name = rawQueryVarName,
-                        typeName = RAW_QUERY,
-                        assignExpr = assignExpr
-                    )
-                    addStatement(
-                        "return %L",
-                        createPagingSourceSpec(
-                            scope = scope,
-                            rawQueryVarName = rawQueryVarName,
-                            dbProperty = dbProperty,
-                            stmtVarName = stmtVarName,
-                            inTransaction = inTransaction
-                        )
-                    )
-                }
-            }
-            CodeLanguage.KOTLIN -> {
-                scope.builder.apply {
-                    if (bindStatement != null) {
-                        beginControlFlow(
-                            "val %L: %T = %T(%N) { %L ->",
-                            rawQueryVarName,
-                            RAW_QUERY,
-                            RAW_QUERY,
-                            sqlQueryVar,
-                            stmtVarName
-                        )
-                        bindStatement.invoke(scope, stmtVarName)
-                        endControlFlow()
-                    } else {
-                        addLocalVariable(
-                            name = rawQueryVarName,
-                            typeName = RAW_QUERY,
-                            assignExpr =
-                                XCodeBlock.ofNewInstance(
-                                    typeName = RAW_QUERY,
-                                    argsFormat = "%N",
-                                    sqlQueryVar
-                                )
-                        )
-                    }
-                    addStatement(
-                        "return %L",
-                        createPagingSourceSpec(
-                            scope = scope,
-                            rawQueryVarName = rawQueryVarName,
-                            stmtVarName = stmtVarName,
-                            dbProperty = dbProperty,
-                            inTransaction = false
-                        )
-                    )
-                }
-            }
-        }
-    }
-
-    private fun XCodeBlock.Builder.createPagingSourceSpec(
-        scope: CodeGenScope,
-        rawQueryVarName: String,
-        stmtVarName: String,
-        dbProperty: XPropertySpec,
-        inTransaction: Boolean
-    ): XTypeSpec {
-        val tableNamesList = tableNames.joinToString(", ") { "\"$it\"" }
-        return XTypeSpec.anonymousClassBuilder(
-                argsFormat = "%L, %N, %L",
-                rawQueryVarName,
-                dbProperty,
-                tableNamesList
-            )
-            .apply {
-                superclass(pagingSourceTypeName)
-                addFunction(
-                    createConvertRowsMethod(
-                        scope = scope,
-                        dbProperty = dbProperty,
-                        stmtVarName = stmtVarName,
-                        inTransaction = inTransaction
-                    )
-                )
-            }
-            .build()
-    }
-
-    private fun createConvertRowsMethod(
-        scope: CodeGenScope,
-        dbProperty: XPropertySpec,
-        stmtVarName: String,
-        inTransaction: Boolean
-    ): XFunSpec {
-        val resultVar = scope.getTmpVar("_result")
-        checkNotNull(convertRowsOverrideInfo)
-        return XFunSpec.overridingBuilder(
-                element = convertRowsOverrideInfo.method,
-                owner = convertRowsOverrideInfo.owner
-            )
-            .apply {
-                val limitRawQueryParamName = "limitOffsetQuery"
-                val rowsScope = scope.fork()
-                val connectionVar = scope.getTmpVar("_connection")
-                val performBlock =
-                    InvokeWithLambdaParameter(
-                        scope = scope,
-                        functionName = DB_UTIL_PERFORM_SUSPENDING,
-                        argFormat = listOf("%N", "%L", "%L"),
-                        args = listOf(dbProperty, /* isReadOnly= */ true, inTransaction),
-                        continuationParamName = convertRowsOverrideInfo.continuationParamName,
-                        lambdaSpec =
-                            object :
-                                LambdaSpec(
-                                    parameterTypeName = SQLiteDriverTypeNames.CONNECTION,
-                                    parameterName = connectionVar,
-                                    returnTypeName = convertRowsOverrideInfo.returnTypeName,
-                                    javaLambdaSyntaxAvailable = scope.javaLambdaSyntaxAvailable
-                                ) {
-                                override fun XCodeBlock.Builder.body(scope: CodeGenScope) {
-                                    applyTo { language ->
-                                        val getSql =
-                                            when (language) {
-                                                CodeLanguage.JAVA -> "getSql()"
-                                                CodeLanguage.KOTLIN -> "sql"
-                                            }
-                                        addLocalVal(
-                                            stmtVarName,
-                                            SQLiteDriverTypeNames.STATEMENT,
-                                            "%L.prepare(%L.$getSql)",
-                                            connectionVar,
-                                            limitRawQueryParamName
-                                        )
-                                    }
-                                    addStatement(
-                                        "%L.getBindingFunction().invoke(%L)",
-                                        limitRawQueryParamName,
-                                        stmtVarName
-                                    )
-                                    beginControlFlow("try").apply {
-                                        listAdapter?.convert(resultVar, stmtVarName, scope)
-                                        applyTo { language ->
-                                            val returnPrefix =
-                                                when (language) {
-                                                    CodeLanguage.JAVA -> "return "
-                                                    CodeLanguage.KOTLIN -> ""
-                                                }
-                                            addStatement("$returnPrefix%L", resultVar)
-                                        }
-                                    }
-                                    nextControlFlow("finally")
-                                    addStatement("%L.close()", stmtVarName)
-                                    endControlFlow()
-                                }
-                            }
-                    )
-                rowsScope.builder.add("return %L", performBlock)
-                addCode(rowsScope.generate())
-            }
-            .build()
-    }
 }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/Paging3SourceQueryResultBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/Paging3SourceQueryResultBinder.kt
new file mode 100644
index 0000000..d05aa69
--- /dev/null
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/Paging3SourceQueryResultBinder.kt
@@ -0,0 +1,260 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.solver.query.result
+
+import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.codegen.XClassName
+import androidx.room.compiler.codegen.XCodeBlock
+import androidx.room.compiler.codegen.XCodeBlock.Builder.Companion.applyTo
+import androidx.room.compiler.codegen.XFunSpec
+import androidx.room.compiler.codegen.XPropertySpec
+import androidx.room.compiler.codegen.XTypeName
+import androidx.room.compiler.codegen.XTypeSpec
+import androidx.room.ext.Function1TypeSpec
+import androidx.room.ext.InvokeWithLambdaParameter
+import androidx.room.ext.KotlinTypeNames
+import androidx.room.ext.LambdaSpec
+import androidx.room.ext.RoomMemberNames.DB_UTIL_PERFORM_SUSPENDING
+import androidx.room.ext.RoomTypeNames.RAW_QUERY
+import androidx.room.ext.SQLiteDriverTypeNames
+import androidx.room.solver.CodeGenScope
+import androidx.room.solver.binderprovider.ConvertRowsOverrideInfo
+
+/**
+ * This Binder binds queries directly to KMP Compatible Paging3 PagingSource (i.e.
+ * [androidx.room.paging.LimitOffsetPagingSource]). Used solely by KMP Paging3.
+ */
+class Paging3PagingSourceQueryResultBinder(
+    private val listAdapter: ListQueryResultAdapter?,
+    private val tableNames: Set<String>,
+    private val convertRowsOverrideInfo: ConvertRowsOverrideInfo,
+    className: XClassName
+) : QueryResultBinder(listAdapter) {
+
+    private val itemTypeName: XTypeName =
+        listAdapter?.rowAdapters?.firstOrNull()?.out?.asTypeName() ?: XTypeName.ANY_OBJECT
+    private val pagingSourceTypeName: XTypeName = className.parametrizedBy(itemTypeName)
+
+    override fun isMigratedToDriver(): Boolean = true
+
+    override fun convertAndReturn(
+        roomSQLiteQueryVar: String,
+        canReleaseQuery: Boolean,
+        dbProperty: XPropertySpec,
+        inTransaction: Boolean,
+        scope: CodeGenScope
+    ) {
+        error(
+            "This convertAndReturn() should never be invoked, it will be removed once " +
+                "migration to drivers is completed."
+        )
+    }
+
+    override fun convertAndReturn(
+        sqlQueryVar: String,
+        dbProperty: XPropertySpec,
+        bindStatement: (CodeGenScope.(String) -> Unit)?,
+        returnTypeName: XTypeName,
+        inTransaction: Boolean,
+        scope: CodeGenScope
+    ) {
+        val rawQueryVarName = scope.getTmpVar("_rawQuery")
+        val stmtVarName = scope.getTmpVar("_stmt")
+
+        when (scope.language) {
+            CodeLanguage.JAVA -> {
+                val assignExpr =
+                    if (bindStatement != null) {
+                        XCodeBlock.ofNewInstance(
+                            typeName = RAW_QUERY,
+                            "%L, %L",
+                            sqlQueryVar,
+                            Function1TypeSpec(
+                                parameterTypeName = SQLiteDriverTypeNames.STATEMENT,
+                                parameterName = stmtVarName,
+                                returnTypeName = KotlinTypeNames.UNIT
+                            ) {
+                                val functionScope = scope.fork()
+                                functionScope.builder
+                                    .apply { bindStatement.invoke(functionScope, stmtVarName) }
+                                    .build()
+                                addCode(functionScope.generate())
+                                addStatement("return %T.INSTANCE", KotlinTypeNames.UNIT)
+                            }
+                        )
+                    } else {
+                        XCodeBlock.ofNewInstance(typeName = RAW_QUERY, "%L", sqlQueryVar)
+                    }
+                scope.builder.apply {
+                    addLocalVariable(
+                        name = rawQueryVarName,
+                        typeName = RAW_QUERY,
+                        assignExpr = assignExpr
+                    )
+                    addStatement(
+                        "return %L",
+                        createPagingSourceSpec(
+                            scope = scope,
+                            rawQueryVarName = rawQueryVarName,
+                            stmtVarName = stmtVarName,
+                            dbProperty = dbProperty,
+                            inTransaction = inTransaction
+                        )
+                    )
+                }
+            }
+            CodeLanguage.KOTLIN -> {
+                scope.builder.apply {
+                    if (bindStatement != null) {
+                        beginControlFlow(
+                            "val %L: %T = %T(%N) { %L ->",
+                            rawQueryVarName,
+                            RAW_QUERY,
+                            RAW_QUERY,
+                            sqlQueryVar,
+                            stmtVarName
+                        )
+                        bindStatement.invoke(scope, stmtVarName)
+                        endControlFlow()
+                    } else {
+                        addLocalVariable(
+                            name = rawQueryVarName,
+                            typeName = RAW_QUERY,
+                            assignExpr =
+                                XCodeBlock.ofNewInstance(
+                                    typeName = RAW_QUERY,
+                                    argsFormat = "%N",
+                                    sqlQueryVar
+                                )
+                        )
+                    }
+                    addStatement(
+                        "return %L",
+                        createPagingSourceSpec(
+                            scope = scope,
+                            rawQueryVarName = rawQueryVarName,
+                            stmtVarName = stmtVarName,
+                            dbProperty = dbProperty,
+                            inTransaction = false
+                        )
+                    )
+                }
+            }
+        }
+    }
+
+    private fun createPagingSourceSpec(
+        scope: CodeGenScope,
+        rawQueryVarName: String,
+        stmtVarName: String,
+        dbProperty: XPropertySpec,
+        inTransaction: Boolean
+    ): XTypeSpec {
+        val tableNamesList = tableNames.joinToString(", ") { "\"$it\"" }
+        return XTypeSpec.anonymousClassBuilder(
+                argsFormat = "%L, %N, %L",
+                rawQueryVarName,
+                dbProperty,
+                tableNamesList
+            )
+            .apply {
+                superclass(pagingSourceTypeName)
+                addFunction(
+                    createConvertRowsMethod(
+                        scope = scope,
+                        dbProperty = dbProperty,
+                        stmtVarName = stmtVarName,
+                        inTransaction = inTransaction
+                    )
+                )
+            }
+            .build()
+    }
+
+    private fun createConvertRowsMethod(
+        scope: CodeGenScope,
+        dbProperty: XPropertySpec,
+        stmtVarName: String,
+        inTransaction: Boolean
+    ): XFunSpec {
+        val resultVar = scope.getTmpVar("_result")
+        return XFunSpec.overridingBuilder(
+                element = convertRowsOverrideInfo.method,
+                owner = convertRowsOverrideInfo.owner
+            )
+            .apply {
+                val limitRawQueryParamName = "limitOffsetQuery"
+                val rowsScope = scope.fork()
+                val connectionVar = scope.getTmpVar("_connection")
+                val performBlock =
+                    InvokeWithLambdaParameter(
+                        scope = scope,
+                        functionName = DB_UTIL_PERFORM_SUSPENDING,
+                        argFormat = listOf("%N", "%L", "%L"),
+                        args = listOf(dbProperty, /* isReadOnly= */ true, inTransaction),
+                        continuationParamName = convertRowsOverrideInfo.continuationParamName,
+                        lambdaSpec =
+                            object :
+                                LambdaSpec(
+                                    parameterTypeName = SQLiteDriverTypeNames.CONNECTION,
+                                    parameterName = connectionVar,
+                                    returnTypeName = convertRowsOverrideInfo.returnTypeName,
+                                    javaLambdaSyntaxAvailable = scope.javaLambdaSyntaxAvailable
+                                ) {
+                                override fun XCodeBlock.Builder.body(scope: CodeGenScope) {
+                                    applyTo { language ->
+                                        val getSql =
+                                            when (language) {
+                                                CodeLanguage.JAVA -> "getSql()"
+                                                CodeLanguage.KOTLIN -> "sql"
+                                            }
+                                        addLocalVal(
+                                            stmtVarName,
+                                            SQLiteDriverTypeNames.STATEMENT,
+                                            "%L.prepare(%L.$getSql)",
+                                            connectionVar,
+                                            limitRawQueryParamName
+                                        )
+                                    }
+                                    addStatement(
+                                        "%L.getBindingFunction().invoke(%L)",
+                                        limitRawQueryParamName,
+                                        stmtVarName
+                                    )
+                                    beginControlFlow("try").apply {
+                                        listAdapter?.convert(resultVar, stmtVarName, scope)
+                                        applyTo { language ->
+                                            val returnPrefix =
+                                                when (language) {
+                                                    CodeLanguage.JAVA -> "return "
+                                                    CodeLanguage.KOTLIN -> ""
+                                                }
+                                            addStatement("$returnPrefix%L", resultVar)
+                                        }
+                                    }
+                                    nextControlFlow("finally")
+                                    addStatement("%L.close()", stmtVarName)
+                                    endControlFlow()
+                                }
+                            }
+                    )
+                rowsScope.builder.add("return %L", performBlock)
+                addCode(rowsScope.generate())
+            }
+            .build()
+    }
+}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PositionalDataSourceQueryResultBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PositionalDataSourceQueryResultBinder.kt
index 360af30..ea76a1c 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PositionalDataSourceQueryResultBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PositionalDataSourceQueryResultBinder.kt
@@ -16,14 +16,18 @@
 
 package androidx.room.solver.query.result
 
+import androidx.room.compiler.codegen.CodeLanguage
 import androidx.room.compiler.codegen.VisibilityModifier
+import androidx.room.compiler.codegen.XCodeBlock.Builder.Companion.applyTo
 import androidx.room.compiler.codegen.XFunSpec
 import androidx.room.compiler.codegen.XPropertySpec
 import androidx.room.compiler.codegen.XTypeName
 import androidx.room.compiler.codegen.XTypeSpec
-import androidx.room.ext.AndroidTypeNames.CURSOR
 import androidx.room.ext.CommonTypeNames.LIST
+import androidx.room.ext.RoomMemberNames.DB_UTIL_SUPPORT_DB_TO_CONNECTION
 import androidx.room.ext.RoomTypeNames
+import androidx.room.ext.SQLiteDriverTypeNames
+import androidx.room.ext.SQLiteDriverTypeNames.CONNECTION
 import androidx.room.solver.CodeGenScope
 
 /** Used by Paging2 pipeline */
@@ -35,10 +39,15 @@
         listAdapter?.rowAdapters?.firstOrNull()?.out?.asTypeName() ?: XTypeName.ANY_OBJECT
     val typeName: XTypeName = RoomTypeNames.LIMIT_OFFSET_DATA_SOURCE.parametrizedBy(itemTypeName)
 
+    override val usesCompatQueryWriter = true
+
+    override fun isMigratedToDriver(): Boolean = true
+
     override fun convertAndReturn(
-        roomSQLiteQueryVar: String,
-        canReleaseQuery: Boolean,
+        sqlQueryVar: String,
         dbProperty: XPropertySpec,
+        bindStatement: (CodeGenScope.(String) -> Unit)?,
+        returnTypeName: XTypeName,
         inTransaction: Boolean,
         scope: CodeGenScope
     ) {
@@ -50,7 +59,7 @@
             XTypeSpec.anonymousClassBuilder(
                     "%N, %L, %L, %L%L",
                     dbProperty,
-                    roomSQLiteQueryVar,
+                    sqlQueryVar,
                     inTransaction,
                     true,
                     tableNamesList
@@ -60,9 +69,41 @@
                     addConvertRowsMethod(scope)
                 }
                 .build()
+        val rowAdapter = listAdapter?.rowAdapters?.first()
+        if (rowAdapter is PojoRowAdapter && rowAdapter.relationCollectors.isNotEmpty()) {
+            // @Relation use found, initialize a connection.
+            val connectionVar = scope.getTmpVar("_connection")
+            scope.builder.applyTo { language ->
+                val assignExprFormat =
+                    when (language) {
+                        CodeLanguage.JAVA -> "%M(%L.getOpenHelper().getWritableDatabase())"
+                        CodeLanguage.KOTLIN -> "%M(%L.openHelper.writableDatabase)"
+                    }
+                addLocalVal(
+                    name = connectionVar,
+                    typeName = CONNECTION,
+                    assignExprFormat = assignExprFormat,
+                    DB_UTIL_SUPPORT_DB_TO_CONNECTION,
+                    dbProperty.name
+                )
+            }
+        }
         scope.builder.addStatement("return %L", spec)
     }
 
+    override fun convertAndReturn(
+        roomSQLiteQueryVar: String,
+        canReleaseQuery: Boolean,
+        dbProperty: XPropertySpec,
+        inTransaction: Boolean,
+        scope: CodeGenScope
+    ) {
+        error(
+            "This convertAndReturn() should never be invoked, it will be removed once " +
+                "migration to drivers is completed."
+        )
+    }
+
     private fun XTypeSpec.Builder.addConvertRowsMethod(scope: CodeGenScope) {
         addFunction(
             XFunSpec.builder(
@@ -72,8 +113,8 @@
                 )
                 .apply {
                     returns(LIST.parametrizedBy(itemTypeName))
-                    val cursorParamName = "cursor"
-                    addParameter(cursorParamName, CURSOR)
+                    val cursorParamName = "statement"
+                    addParameter(cursorParamName, SQLiteDriverTypeNames.STATEMENT)
                     val resultVar = scope.getTmpVar("_res")
                     val rowsScope = scope.fork()
                     listAdapter?.convert(resultVar, cursorParamName, rowsScope)
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/QueryResultBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/QueryResultBinder.kt
index 286394d..a1d11a6 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/QueryResultBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/QueryResultBinder.kt
@@ -44,6 +44,8 @@
     // TODO(b/319660042): Remove once migration to driver API is done.
     open fun isMigratedToDriver(): Boolean = false
 
+    open val usesCompatQueryWriter: Boolean = false
+
     /**
      * Receives the SQL and a function to bind args into a statement, it must then generate the code
      * that steps on the query, reads its columns and returns the result.
@@ -56,6 +58,6 @@
         inTransaction: Boolean,
         scope: CodeGenScope
     ) {
-        error("Result binder has not been migrated to use driver API.")
+        error("Result binder has not been migrated to use driver API. ")
     }
 }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt
index 5fd0551..ed4c8f76 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt
@@ -316,19 +316,24 @@
 
     private fun createRawQueryMethod(method: RawQueryMethod): XFunSpec {
         return overrideWithoutAnnotations(method.element, declaredDao)
-            .addCode(createRawQueryMethodBody(method))
+            .addCode(
+                if (
+                    method.runtimeQueryParam == null ||
+                        method.queryResultBinder.usesCompatQueryWriter
+                ) {
+                    compatCreateRawQueryMethodBody(method)
+                } else {
+                    createRawQueryMethodBody(method)
+                }
+            )
             .build()
     }
 
     private fun createRawQueryMethodBody(method: RawQueryMethod): XCodeBlock {
-        if (method.runtimeQueryParam == null || !method.queryResultBinder.isMigratedToDriver()) {
-            return compatCreateRawQueryMethodBody(method)
-        }
-
         val scope = CodeGenScope(this@DaoWriter, useDriverApi = true)
         val sqlQueryVar = scope.getTmpVar("_sql")
         val rawQueryParamName =
-            if (method.runtimeQueryParam.isSupportQuery()) {
+            if (method.runtimeQueryParam!!.isSupportQuery()) {
                 val rawQueryVar = scope.getTmpVar("_rawQuery")
                 scope.builder.addLocalVariable(
                     name = rawQueryVar,
@@ -352,6 +357,7 @@
             rawQueryParamName,
             XCodeBlock.ofString(java = "getSql()", kotlin = "sql")
         )
+
         if (method.returnsValue) {
             method.queryResultBinder.convertAndReturn(
                 sqlQueryVar = sqlQueryVar,
@@ -371,19 +377,17 @@
         return scope.generate()
     }
 
+    /** Used by the Non-KMP Paging3 binders and the Paging2 binders. */
     private fun compatCreateRawQueryMethodBody(method: RawQueryMethod): XCodeBlock =
         XCodeBlock.builder()
             .apply {
-                val scope = CodeGenScope(this@DaoWriter)
+                val scope = CodeGenScope(this@DaoWriter, useDriverApi = true)
                 val roomSQLiteQueryVar: String
                 val queryParam = method.runtimeQueryParam
-                val shouldReleaseQuery: Boolean
                 if (queryParam?.isSupportQuery() == true) {
-                    roomSQLiteQueryVar = queryParam.paramName
-                    shouldReleaseQuery = false
+                    queryParam.paramName
                 } else if (queryParam?.isString() == true) {
                     roomSQLiteQueryVar = scope.getTmpVar("_statement")
-                    shouldReleaseQuery = true
                     addLocalVariable(
                         name = roomSQLiteQueryVar,
                         typeName = ROOM_SQL_QUERY,
@@ -397,7 +401,6 @@
                 } else {
                     // try to generate compiling code. we would've already reported this error
                     roomSQLiteQueryVar = scope.getTmpVar("_statement")
-                    shouldReleaseQuery = false
                     addLocalVariable(
                         name = roomSQLiteQueryVar,
                         typeName = ROOM_SQL_QUERY,
@@ -409,16 +412,24 @@
                             ),
                     )
                 }
-                if (method.returnsValue) {
-                    // don't generate code because it will create 1 more error. The original
-                    // error is already reported by the processor.
-                    method.queryResultBinder.convertAndReturn(
-                        roomSQLiteQueryVar = roomSQLiteQueryVar,
-                        canReleaseQuery = shouldReleaseQuery,
-                        dbProperty = dbProperty,
-                        inTransaction = method.inTransaction,
-                        scope = scope
-                    )
+                val rawQueryParamName = method.runtimeQueryParam?.paramName
+                if (rawQueryParamName != null) {
+                    if (method.returnsValue) {
+                        method.queryResultBinder.convertAndReturn(
+                            sqlQueryVar = rawQueryParamName,
+                            dbProperty = dbProperty,
+                            bindStatement = { stmtVar ->
+                                this.builder.addStatement(
+                                    "%L.getBindingFunction().invoke(%L)",
+                                    rawQueryParamName,
+                                    stmtVar
+                                )
+                            },
+                            returnTypeName = method.returnType.asTypeName(),
+                            inTransaction = method.inTransaction,
+                            scope = scope
+                        )
+                    }
                 }
                 add(scope.generate())
             }
@@ -630,7 +641,6 @@
         if (!method.preparedQueryResultBinder.isMigratedToDriver()) {
             return compatCreatePreparedQueryMethodBody(method)
         }
-
         val scope = CodeGenScope(this, useDriverApi = true)
         val queryWriter = QueryWriter(method)
         val sqlVar = scope.getTmpVar("_sql")
@@ -671,27 +681,35 @@
     }
 
     private fun createQueryMethodBody(method: ReadQueryMethod): XCodeBlock {
-        if (!method.queryResultBinder.isMigratedToDriver()) {
-            return compatCreateQueryMethodBody(method)
-        }
-
         val scope = CodeGenScope(this, useDriverApi = true)
         val queryWriter = QueryWriter(method)
-        val sqlVar = scope.getTmpVar("_sql")
-        val listSizeArgs = queryWriter.prepareQuery(sqlVar, scope)
+        val sqlStringVar = scope.getTmpVar("_sql")
+
+        val (sqlVar, listSizeArgs) =
+            if (method.queryResultBinder.usesCompatQueryWriter) {
+                val roomSQLiteQueryVar = scope.getTmpVar("_statement")
+                queryWriter.prepareReadAndBind(sqlStringVar, roomSQLiteQueryVar, scope)
+                roomSQLiteQueryVar to emptyList()
+            } else {
+                sqlStringVar to queryWriter.prepareQuery(sqlStringVar, scope)
+            }
+
+        val bindStatement: (CodeGenScope.(String) -> Unit)? =
+            if (queryWriter.parameters.isNotEmpty()) {
+                { stmtVar -> queryWriter.bindArgs(stmtVar, listSizeArgs, this) }
+            } else {
+                null
+            }
+
         method.queryResultBinder.convertAndReturn(
             sqlQueryVar = sqlVar,
             dbProperty = dbProperty,
-            bindStatement =
-                if (queryWriter.parameters.isNotEmpty()) {
-                    { stmtVar -> queryWriter.bindArgs(stmtVar, listSizeArgs, this) }
-                } else {
-                    null
-                },
+            bindStatement = bindStatement,
             returnTypeName = method.returnType.asTypeName(),
             inTransaction = method.inTransaction,
             scope = scope
         )
+
         return scope.generate()
     }
 
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
index d95c391..4ecbbf4 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
@@ -61,6 +61,7 @@
 import androidx.room.solver.binderprovider.RxQueryResultBinderProvider
 import androidx.room.solver.query.parameter.CollectionQueryParameterAdapter
 import androidx.room.solver.query.result.MultiTypedPagingSourceQueryResultBinder
+import androidx.room.solver.query.result.Paging3PagingSourceQueryResultBinder
 import androidx.room.solver.shortcut.binderprovider.GuavaListenableFutureDeleteOrUpdateMethodBinderProvider
 import androidx.room.solver.shortcut.binderprovider.GuavaListenableFutureInsertOrUpsertMethodBinderProvider
 import androidx.room.solver.shortcut.binderprovider.RxCallableDeleteOrUpdateMethodBinderProvider
@@ -1428,7 +1429,7 @@
             val parsedDao = parser.process()
             val binder =
                 parsedDao.queryMethods.filterIsInstance<ReadQueryMethod>().first().queryResultBinder
-            assertThat(binder is MultiTypedPagingSourceQueryResultBinder).isTrue()
+            assertThat(binder is Paging3PagingSourceQueryResultBinder).isTrue()
 
             val pagingSourceXRawType: XRawType? =
                 invocation.context.processingEnv
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt
index 71326b5..6c29224 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt
@@ -800,7 +800,7 @@
             @Entity
             data class MyEntity(
                 @PrimaryKey
-                val pk: Int,
+                val pk: String,
             )
             """
                     .trimIndent()
@@ -1938,6 +1938,9 @@
             abstract class MyDao {
                 @Query("SELECT * from MyEntity")
                 abstract fun getDataSourceFactory(): DataSource.Factory<Int, MyEntity>
+
+                @Query("SELECT * FROM MyEntity WHERE pk > :gt ORDER BY pk ASC")
+                abstract fun getDataSourceFactoryWithArgs(gt: Long): DataSource.Factory<Int, MyEntity>
             }
 
             @Entity
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoRelationshipKotlinCodeGenTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoRelationshipKotlinCodeGenTest.kt
index 27b0618..26ec990 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoRelationshipKotlinCodeGenTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoRelationshipKotlinCodeGenTest.kt
@@ -465,4 +465,95 @@
             )
         runTest(sources = listOf(src), expectedFilePath = getTestGoldenPath(testName.methodName))
     }
+
+    @Test
+    fun relations_dataSource() {
+        val src =
+            Source.kotlin(
+                "MyDao.kt",
+                """
+            import androidx.room.*
+            import androidx.paging.DataSource
+
+            @Dao
+            @Suppress(
+                RoomWarnings.RELATION_QUERY_WITHOUT_TRANSACTION,
+                RoomWarnings.MISSING_INDEX_ON_JUNCTION
+            )
+            interface MyDao {
+                // 1 to 1
+                @Query("SELECT * FROM Song")
+                fun getSongsWithArtist(): DataSource.Factory<Int, SongWithArtist>
+
+                // 1 to many
+                @Query("SELECT * FROM Artist")
+                fun getArtistAndSongs(): DataSource.Factory<Int, ArtistAndSongs>
+
+                // many to many
+                @Query("SELECT * FROM Playlist")
+                fun getPlaylistAndSongs(): DataSource.Factory<Int, PlaylistAndSongs>
+            }
+
+            data class SongWithArtist(
+                @Embedded
+                val song: Song,
+                @Relation(parentColumn = "artistKey", entityColumn = "artistId")
+                val artist: Artist
+            )
+
+            data class ArtistAndSongs(
+                @Embedded
+                val artist: Artist,
+                @Relation(parentColumn = "artistId", entityColumn = "artistKey")
+                val songs: List<Song>
+            )
+
+            data class PlaylistAndSongs(
+                @Embedded
+                val playlist: Playlist,
+                @Relation(
+                    parentColumn = "playlistId",
+                    entityColumn = "songId",
+                    associateBy = Junction(
+                        value = PlaylistSongXRef::class,
+                        parentColumn = "playlistKey",
+                        entityColumn = "songKey",
+                    )
+                )
+                val songs: List<Song>
+            )
+
+            @Entity
+            data class Artist(
+                @PrimaryKey
+                val artistId: Long
+            )
+
+            @Entity
+            data class Song(
+                @PrimaryKey
+                val songId: Long,
+                val artistKey: Long
+            )
+
+            @Entity
+            data class Playlist(
+                @PrimaryKey
+                val playlistId: Long,
+            )
+
+            @Entity(primaryKeys = ["playlistKey", "songKey"])
+            data class PlaylistSongXRef(
+                val playlistKey: Long,
+                val songKey: Long,
+            )
+            """
+                    .trimIndent()
+            )
+        runTest(
+            sources =
+                listOf(src, databaseSrc, COMMON.DATA_SOURCE_FACTORY, COMMON.POSITIONAL_DATA_SOURCE),
+            expectedFilePath = getTestGoldenPath(testName.methodName)
+        )
+    }
 }
diff --git a/room/room-compiler/src/test/test-data/common/input/LimitOffsetListenableFuturePagingSource.kt b/room/room-compiler/src/test/test-data/common/input/LimitOffsetListenableFuturePagingSource.kt
index 4a8eaed..cecbc5c 100644
--- a/room/room-compiler/src/test/test-data/common/input/LimitOffsetListenableFuturePagingSource.kt
+++ b/room/room-compiler/src/test/test-data/common/input/LimitOffsetListenableFuturePagingSource.kt
@@ -15,9 +15,9 @@
  */
 package androidx.room.paging.guava
 
-import android.database.Cursor
 import androidx.room.RoomDatabase
 import androidx.room.RoomSQLiteQuery
+import androidx.sqlite.SQLiteStatement
 
 abstract class LimitOffsetListenableFuturePagingSource<T : Any>(
     private val sourceQuery: RoomSQLiteQuery,
@@ -28,5 +28,5 @@
         db,
         *tables
     ) {
-    protected abstract override fun convertRows(cursor: Cursor): List<T>
+    protected override abstract fun convertRows(statement: SQLiteStatement): List<T>
 }
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/common/input/LimitOffsetRx2PagingSource.kt b/room/room-compiler/src/test/test-data/common/input/LimitOffsetRx2PagingSource.kt
index 1eef542..a3184f0 100644
--- a/room/room-compiler/src/test/test-data/common/input/LimitOffsetRx2PagingSource.kt
+++ b/room/room-compiler/src/test/test-data/common/input/LimitOffsetRx2PagingSource.kt
@@ -15,9 +15,9 @@
  */
 package androidx.room.paging.rxjava2
 
-import android.database.Cursor
 import androidx.room.RoomDatabase
 import androidx.room.RoomSQLiteQuery
+import androidx.sqlite.SQLiteStatement
 
 abstract class LimitOffsetRxPagingSource<T : Any>(
     private val sourceQuery: RoomSQLiteQuery,
@@ -28,5 +28,5 @@
     db,
     *tables
 ) {
-    protected abstract override fun convertRows(cursor: Cursor): List<T>
+    protected override abstract fun convertRows(statement: SQLiteStatement): List<T>
 }
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/common/input/LimitOffsetRx3PagingSource.kt b/room/room-compiler/src/test/test-data/common/input/LimitOffsetRx3PagingSource.kt
index bfb1294..7b2b8b6 100644
--- a/room/room-compiler/src/test/test-data/common/input/LimitOffsetRx3PagingSource.kt
+++ b/room/room-compiler/src/test/test-data/common/input/LimitOffsetRx3PagingSource.kt
@@ -15,9 +15,9 @@
  */
 package androidx.room.paging.rxjava3
 
-import android.database.Cursor
 import androidx.room.RoomDatabase
 import androidx.room.RoomSQLiteQuery
+import androidx.sqlite.SQLiteStatement
 
 abstract class LimitOffsetRxPagingSource<T : Any>(
     private val sourceQuery: RoomSQLiteQuery,
@@ -28,5 +28,5 @@
     db,
     *tables
 ) {
-    protected abstract override fun convertRows(cursor: Cursor): List<T>
+    protected override abstract fun convertRows(statement: SQLiteStatement): List<T>
 }
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/common/input/ListenableFuturePagingSource.kt b/room/room-compiler/src/test/test-data/common/input/ListenableFuturePagingSource.kt
index 1e84ae6..042e6f1 100644
--- a/room/room-compiler/src/test/test-data/common/input/ListenableFuturePagingSource.kt
+++ b/room/room-compiler/src/test/test-data/common/input/ListenableFuturePagingSource.kt
@@ -15,10 +15,10 @@
  */
 package androidx.paging
 
-import android.database.Cursor
 import androidx.paging.PagingState
 import androidx.room.RoomDatabase
 import androidx.room.RoomSQLiteQuery
+import androidx.sqlite.SQLiteStatement
 
 @Suppress("UNUSED_PARAMETER")
 abstract class ListenableFuturePagingSource<K : Any, T : Any>(
@@ -33,5 +33,6 @@
     override public suspend fun load(params: LoadParams<K>): LoadResult<K, T> {
         return LoadResult.Invalid()
     }
-    protected abstract fun convertRows(cursor: Cursor): List<T>
+
+    protected abstract fun convertRows(statement: SQLiteStatement): List<T>
 }
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/common/input/Rx2PagingSource.kt b/room/room-compiler/src/test/test-data/common/input/Rx2PagingSource.kt
index a98e980..e673d19 100644
--- a/room/room-compiler/src/test/test-data/common/input/Rx2PagingSource.kt
+++ b/room/room-compiler/src/test/test-data/common/input/Rx2PagingSource.kt
@@ -19,6 +19,8 @@
 import androidx.paging.PagingState
 import androidx.room.RoomDatabase
 import androidx.room.RoomSQLiteQuery
+import androidx.room.paging.CursorSQLiteStatement
+import androidx.sqlite.SQLiteStatement
 
 @Suppress("UNUSED_PARAMETER")
 abstract class RxPagingSource<K : Any, T : Any>(
@@ -34,5 +36,14 @@
         return LoadResult.Invalid()
     }
 
-    protected abstract fun convertRows(cursor: Cursor): List<T>
+    protected open fun convertRows(cursor: Cursor): List<T> {
+        return convertRows(CursorSQLiteStatement(cursor))
+    }
+
+    protected open fun convertRows(statement: SQLiteStatement): List<T> {
+        throw NotImplementedError(
+            "Unexpected call to a function with no implementation that Room is suppose to " +
+                "generate."
+        )
+    }
 }
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/common/input/Rx3PagingSource.kt b/room/room-compiler/src/test/test-data/common/input/Rx3PagingSource.kt
index 212cd77..2b16445 100644
--- a/room/room-compiler/src/test/test-data/common/input/Rx3PagingSource.kt
+++ b/room/room-compiler/src/test/test-data/common/input/Rx3PagingSource.kt
@@ -19,6 +19,8 @@
 import androidx.paging.PagingState
 import androidx.room.RoomDatabase
 import androidx.room.RoomSQLiteQuery
+import androidx.room.paging.CursorSQLiteStatement
+import androidx.sqlite.SQLiteStatement
 
 @Suppress("UNUSED_PARAMETER")
 abstract class RxPagingSource<K : Any, T : Any>(
@@ -34,5 +36,14 @@
         return LoadResult.Invalid()
     }
 
-    protected abstract fun convertRows(cursor: Cursor): List<T>
+    protected open fun convertRows(cursor: Cursor): List<T> {
+        return convertRows(CursorSQLiteStatement(cursor))
+    }
+
+    protected open fun convertRows(statement: SQLiteStatement): List<T> {
+        throw NotImplementedError(
+            "Unexpected call to a function with no implementation that Room is suppose to " +
+                "generate."
+        )
+    }
 }
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/multiTypedPagingSourceResultBinder.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/multiTypedPagingSourceResultBinder.kt
index fb9b14c..4615252 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/multiTypedPagingSourceResultBinder.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/multiTypedPagingSourceResultBinder.kt
@@ -1,4 +1,3 @@
-import android.database.Cursor
 import androidx.paging.ListenableFuturePagingSource
 import androidx.paging.PagingSource
 import androidx.room.RoomDatabase
@@ -47,8 +46,8 @@
           val _result: MutableList<MyEntity> = mutableListOf()
           while (_stmt.step()) {
             val _item: MyEntity
-            val _tmpPk: Int
-            _tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
+            val _tmpPk: String
+            _tmpPk = _stmt.getText(_cursorIndexOfPk)
             _item = MyEntity(_tmpPk)
             _result.add(_item)
           }
@@ -76,8 +75,8 @@
           val _result: MutableList<MyEntity> = mutableListOf()
           while (_stmt.step()) {
             val _item: MyEntity
-            val _tmpPk: Int
-            _tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
+            val _tmpPk: String
+            _tmpPk = _stmt.getText(_cursorIndexOfPk)
             _item = MyEntity(_tmpPk)
             _result.add(_item)
           }
@@ -93,13 +92,13 @@
     val _sql: String = "SELECT pk FROM MyEntity"
     val _statement: RoomSQLiteQuery = acquire(_sql, 0)
     return object : Rxjava2LimitOffsetRxPagingSource<MyEntity>(_statement, __db, "MyEntity") {
-      protected override fun convertRows(cursor: Cursor): List<MyEntity> {
+      protected override fun convertRows(statement: SQLiteStatement): List<MyEntity> {
         val _cursorIndexOfPk: Int = 0
         val _result: MutableList<MyEntity> = mutableListOf()
-        while (cursor.moveToNext()) {
+        while (statement.step()) {
           val _item: MyEntity
-          val _tmpPk: Int
-          _tmpPk = cursor.getInt(_cursorIndexOfPk)
+          val _tmpPk: String
+          _tmpPk = statement.getText(_cursorIndexOfPk)
           _item = MyEntity(_tmpPk)
           _result.add(_item)
         }
@@ -112,13 +111,13 @@
     val _sql: String = "SELECT pk FROM MyEntity"
     val _statement: RoomSQLiteQuery = acquire(_sql, 0)
     return object : Rxjava3LimitOffsetRxPagingSource<MyEntity>(_statement, __db, "MyEntity") {
-      protected override fun convertRows(cursor: Cursor): List<MyEntity> {
+      protected override fun convertRows(statement: SQLiteStatement): List<MyEntity> {
         val _cursorIndexOfPk: Int = 0
         val _result: MutableList<MyEntity> = mutableListOf()
-        while (cursor.moveToNext()) {
+        while (statement.step()) {
           val _item: MyEntity
-          val _tmpPk: Int
-          _tmpPk = cursor.getInt(_cursorIndexOfPk)
+          val _tmpPk: String
+          _tmpPk = statement.getText(_cursorIndexOfPk)
           _item = MyEntity(_tmpPk)
           _result.add(_item)
         }
@@ -132,13 +131,13 @@
     val _statement: RoomSQLiteQuery = acquire(_sql, 0)
     return object : LimitOffsetListenableFuturePagingSource<MyEntity>(_statement, __db, "MyEntity")
         {
-      protected override fun convertRows(cursor: Cursor): List<MyEntity> {
+      protected override fun convertRows(statement: SQLiteStatement): List<MyEntity> {
         val _cursorIndexOfPk: Int = 0
         val _result: MutableList<MyEntity> = mutableListOf()
-        while (cursor.moveToNext()) {
+        while (statement.step()) {
           val _item: MyEntity
-          val _tmpPk: Int
-          _tmpPk = cursor.getInt(_cursorIndexOfPk)
+          val _tmpPk: String
+          _tmpPk = statement.getText(_cursorIndexOfPk)
           _item = MyEntity(_tmpPk)
           _result.add(_item)
         }
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/paging_dataSource.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/paging_dataSource.kt
index 2730d8f..f8e49bc 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/paging_dataSource.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/paging_dataSource.kt
@@ -1,12 +1,13 @@
-import android.database.Cursor
 import androidx.paging.DataSource
 import androidx.room.RoomDatabase
 import androidx.room.RoomSQLiteQuery
 import androidx.room.RoomSQLiteQuery.Companion.acquire
 import androidx.room.paging.LimitOffsetDataSource
 import androidx.room.util.getColumnIndexOrThrow
+import androidx.sqlite.SQLiteStatement
 import javax.`annotation`.processing.Generated
 import kotlin.Int
+import kotlin.Long
 import kotlin.String
 import kotlin.Suppress
 import kotlin.collections.List
@@ -30,16 +31,43 @@
     return object : DataSource.Factory<Int, MyEntity>() {
       public override fun create(): LimitOffsetDataSource<MyEntity> = object :
           LimitOffsetDataSource<MyEntity>(__db, _statement, false, true, "MyEntity") {
-        protected override fun convertRows(cursor: Cursor): List<MyEntity> {
-          val _cursorIndexOfPk: Int = getColumnIndexOrThrow(cursor, "pk")
-          val _cursorIndexOfOther: Int = getColumnIndexOrThrow(cursor, "other")
+        protected override fun convertRows(statement: SQLiteStatement): List<MyEntity> {
+          val _cursorIndexOfPk: Int = getColumnIndexOrThrow(statement, "pk")
+          val _cursorIndexOfOther: Int = getColumnIndexOrThrow(statement, "other")
           val _res: MutableList<MyEntity> = mutableListOf()
-          while (cursor.moveToNext()) {
+          while (statement.step()) {
             val _item: MyEntity
             val _tmpPk: Int
-            _tmpPk = cursor.getInt(_cursorIndexOfPk)
+            _tmpPk = statement.getLong(_cursorIndexOfPk).toInt()
             val _tmpOther: String
-            _tmpOther = cursor.getString(_cursorIndexOfOther)
+            _tmpOther = statement.getText(_cursorIndexOfOther)
+            _item = MyEntity(_tmpPk,_tmpOther)
+            _res.add(_item)
+          }
+          return _res
+        }
+      }
+    }
+  }
+
+  public override fun getDataSourceFactoryWithArgs(gt: Long): DataSource.Factory<Int, MyEntity> {
+    val _sql: String = "SELECT * FROM MyEntity WHERE pk > ? ORDER BY pk ASC"
+    val _statement: RoomSQLiteQuery = acquire(_sql, 1)
+    var _argIndex: Int = 1
+    _statement.bindLong(_argIndex, gt)
+    return object : DataSource.Factory<Int, MyEntity>() {
+      public override fun create(): LimitOffsetDataSource<MyEntity> = object :
+          LimitOffsetDataSource<MyEntity>(__db, _statement, false, true, "MyEntity") {
+        protected override fun convertRows(statement: SQLiteStatement): List<MyEntity> {
+          val _cursorIndexOfPk: Int = getColumnIndexOrThrow(statement, "pk")
+          val _cursorIndexOfOther: Int = getColumnIndexOrThrow(statement, "other")
+          val _res: MutableList<MyEntity> = mutableListOf()
+          while (statement.step()) {
+            val _item: MyEntity
+            val _tmpPk: Int
+            _tmpPk = statement.getLong(_cursorIndexOfPk).toInt()
+            val _tmpOther: String
+            _tmpOther = statement.getText(_cursorIndexOfOther)
             _item = MyEntity(_tmpPk,_tmpOther)
             _res.add(_item)
           }
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/relations_dataSource.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/relations_dataSource.kt
new file mode 100644
index 0000000..68783b0
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/relations_dataSource.kt
@@ -0,0 +1,314 @@
+import androidx.paging.DataSource
+import androidx.room.RoomDatabase
+import androidx.room.RoomSQLiteQuery
+import androidx.room.RoomSQLiteQuery.Companion.acquire
+import androidx.room.paging.LimitOffsetDataSource
+import androidx.room.util.appendPlaceholders
+import androidx.room.util.getColumnIndex
+import androidx.room.util.getColumnIndexOrThrow
+import androidx.room.util.recursiveFetchMap
+import androidx.room.util.toSQLiteConnection
+import androidx.sqlite.SQLiteConnection
+import androidx.sqlite.SQLiteStatement
+import javax.`annotation`.processing.Generated
+import kotlin.Int
+import kotlin.Long
+import kotlin.String
+import kotlin.Suppress
+import kotlin.collections.List
+import kotlin.collections.MutableList
+import kotlin.collections.MutableMap
+import kotlin.collections.Set
+import kotlin.collections.mutableListOf
+import kotlin.collections.mutableMapOf
+import kotlin.reflect.KClass
+import kotlin.text.StringBuilder
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["UNCHECKED_CAST", "DEPRECATION", "REDUNDANT_PROJECTION", "REMOVAL"])
+public class MyDao_Impl(
+  __db: RoomDatabase,
+) : MyDao {
+  private val __db: RoomDatabase
+  init {
+    this.__db = __db
+  }
+
+  public override fun getSongsWithArtist(): DataSource.Factory<Int, SongWithArtist> {
+    val _sql: String = "SELECT * FROM Song"
+    val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+    return object : DataSource.Factory<Int, SongWithArtist>() {
+      public override fun create(): LimitOffsetDataSource<SongWithArtist> {
+        val _connection: SQLiteConnection = toSQLiteConnection(__db.openHelper.writableDatabase)
+        return object : LimitOffsetDataSource<SongWithArtist>(__db, _statement, false, true,
+            "Artist", "Song") {
+          protected override fun convertRows(statement: SQLiteStatement): List<SongWithArtist> {
+            val _cursorIndexOfSongId: Int = getColumnIndexOrThrow(statement, "songId")
+            val _cursorIndexOfArtistKey: Int = getColumnIndexOrThrow(statement, "artistKey")
+            val _collectionArtist: MutableMap<Long, Artist?> = mutableMapOf()
+            while (statement.step()) {
+              val _tmpKey: Long
+              _tmpKey = statement.getLong(_cursorIndexOfArtistKey)
+              _collectionArtist.put(_tmpKey, null)
+            }
+            statement.reset()
+            __fetchRelationshipArtistAsArtist(_connection, _collectionArtist)
+            val _res: MutableList<SongWithArtist> = mutableListOf()
+            while (statement.step()) {
+              val _item: SongWithArtist
+              val _tmpSong: Song
+              val _tmpSongId: Long
+              _tmpSongId = statement.getLong(_cursorIndexOfSongId)
+              val _tmpArtistKey: Long
+              _tmpArtistKey = statement.getLong(_cursorIndexOfArtistKey)
+              _tmpSong = Song(_tmpSongId,_tmpArtistKey)
+              val _tmpArtist: Artist?
+              val _tmpKey_1: Long
+              _tmpKey_1 = statement.getLong(_cursorIndexOfArtistKey)
+              _tmpArtist = _collectionArtist.get(_tmpKey_1)
+              if (_tmpArtist == null) {
+                error("Relationship item 'artist' was expected to be NON-NULL but is NULL in @Relation involving a parent column named 'artistKey' and entityColumn named 'artistId'.")
+              }
+              _item = SongWithArtist(_tmpSong,_tmpArtist)
+              _res.add(_item)
+            }
+            return _res
+          }
+        }
+      }
+    }
+  }
+
+  public override fun getArtistAndSongs(): DataSource.Factory<Int, ArtistAndSongs> {
+    val _sql: String = "SELECT * FROM Artist"
+    val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+    return object : DataSource.Factory<Int, ArtistAndSongs>() {
+      public override fun create(): LimitOffsetDataSource<ArtistAndSongs> {
+        val _connection: SQLiteConnection = toSQLiteConnection(__db.openHelper.writableDatabase)
+        return object : LimitOffsetDataSource<ArtistAndSongs>(__db, _statement, false, true, "Song",
+            "Artist") {
+          protected override fun convertRows(statement: SQLiteStatement): List<ArtistAndSongs> {
+            val _cursorIndexOfArtistId: Int = getColumnIndexOrThrow(statement, "artistId")
+            val _collectionSongs: MutableMap<Long, MutableList<Song>> = mutableMapOf()
+            while (statement.step()) {
+              val _tmpKey: Long
+              _tmpKey = statement.getLong(_cursorIndexOfArtistId)
+              if (!_collectionSongs.containsKey(_tmpKey)) {
+                _collectionSongs.put(_tmpKey, mutableListOf())
+              }
+            }
+            statement.reset()
+            __fetchRelationshipSongAsSong(_connection, _collectionSongs)
+            val _res: MutableList<ArtistAndSongs> = mutableListOf()
+            while (statement.step()) {
+              val _item: ArtistAndSongs
+              val _tmpArtist: Artist
+              val _tmpArtistId: Long
+              _tmpArtistId = statement.getLong(_cursorIndexOfArtistId)
+              _tmpArtist = Artist(_tmpArtistId)
+              val _tmpSongsCollection: MutableList<Song>
+              val _tmpKey_1: Long
+              _tmpKey_1 = statement.getLong(_cursorIndexOfArtistId)
+              _tmpSongsCollection = _collectionSongs.getValue(_tmpKey_1)
+              _item = ArtistAndSongs(_tmpArtist,_tmpSongsCollection)
+              _res.add(_item)
+            }
+            return _res
+          }
+        }
+      }
+    }
+  }
+
+  public override fun getPlaylistAndSongs(): DataSource.Factory<Int, PlaylistAndSongs> {
+    val _sql: String = "SELECT * FROM Playlist"
+    val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+    return object : DataSource.Factory<Int, PlaylistAndSongs>() {
+      public override fun create(): LimitOffsetDataSource<PlaylistAndSongs> {
+        val _connection: SQLiteConnection = toSQLiteConnection(__db.openHelper.writableDatabase)
+        return object : LimitOffsetDataSource<PlaylistAndSongs>(__db, _statement, false, true,
+            "PlaylistSongXRef", "Song", "Playlist") {
+          protected override fun convertRows(statement: SQLiteStatement): List<PlaylistAndSongs> {
+            val _cursorIndexOfPlaylistId: Int = getColumnIndexOrThrow(statement, "playlistId")
+            val _collectionSongs: MutableMap<Long, MutableList<Song>> = mutableMapOf()
+            while (statement.step()) {
+              val _tmpKey: Long
+              _tmpKey = statement.getLong(_cursorIndexOfPlaylistId)
+              if (!_collectionSongs.containsKey(_tmpKey)) {
+                _collectionSongs.put(_tmpKey, mutableListOf())
+              }
+            }
+            statement.reset()
+            __fetchRelationshipSongAsSong_1(_connection, _collectionSongs)
+            val _res: MutableList<PlaylistAndSongs> = mutableListOf()
+            while (statement.step()) {
+              val _item: PlaylistAndSongs
+              val _tmpPlaylist: Playlist
+              val _tmpPlaylistId: Long
+              _tmpPlaylistId = statement.getLong(_cursorIndexOfPlaylistId)
+              _tmpPlaylist = Playlist(_tmpPlaylistId)
+              val _tmpSongsCollection: MutableList<Song>
+              val _tmpKey_1: Long
+              _tmpKey_1 = statement.getLong(_cursorIndexOfPlaylistId)
+              _tmpSongsCollection = _collectionSongs.getValue(_tmpKey_1)
+              _item = PlaylistAndSongs(_tmpPlaylist,_tmpSongsCollection)
+              _res.add(_item)
+            }
+            return _res
+          }
+        }
+      }
+    }
+  }
+
+  private fun __fetchRelationshipArtistAsArtist(_connection: SQLiteConnection,
+      _map: MutableMap<Long, Artist?>) {
+    val __mapKeySet: Set<Long> = _map.keys
+    if (__mapKeySet.isEmpty()) {
+      return
+    }
+    if (_map.size > 999) {
+      recursiveFetchMap(_map, false) { _tmpMap ->
+        __fetchRelationshipArtistAsArtist(_connection, _tmpMap)
+      }
+      return
+    }
+    val _stringBuilder: StringBuilder = StringBuilder()
+    _stringBuilder.append("SELECT `artistId` FROM `Artist` WHERE `artistId` IN (")
+    val _inputSize: Int = __mapKeySet.size
+    appendPlaceholders(_stringBuilder, _inputSize)
+    _stringBuilder.append(")")
+    val _sql: String = _stringBuilder.toString()
+    val _stmt: SQLiteStatement = _connection.prepare(_sql)
+    var _argIndex: Int = 1
+    for (_item: Long in __mapKeySet) {
+      _stmt.bindLong(_argIndex, _item)
+      _argIndex++
+    }
+    try {
+      val _itemKeyIndex: Int = getColumnIndex(_stmt, "artistId")
+      if (_itemKeyIndex == -1) {
+        return
+      }
+      val _cursorIndexOfArtistId: Int = 0
+      while (_stmt.step()) {
+        val _tmpKey: Long
+        _tmpKey = _stmt.getLong(_itemKeyIndex)
+        if (_map.containsKey(_tmpKey)) {
+          val _item_1: Artist
+          val _tmpArtistId: Long
+          _tmpArtistId = _stmt.getLong(_cursorIndexOfArtistId)
+          _item_1 = Artist(_tmpArtistId)
+          _map.put(_tmpKey, _item_1)
+        }
+      }
+    } finally {
+      _stmt.close()
+    }
+  }
+
+  private fun __fetchRelationshipSongAsSong(_connection: SQLiteConnection,
+      _map: MutableMap<Long, MutableList<Song>>) {
+    val __mapKeySet: Set<Long> = _map.keys
+    if (__mapKeySet.isEmpty()) {
+      return
+    }
+    if (_map.size > 999) {
+      recursiveFetchMap(_map, true) { _tmpMap ->
+        __fetchRelationshipSongAsSong(_connection, _tmpMap)
+      }
+      return
+    }
+    val _stringBuilder: StringBuilder = StringBuilder()
+    _stringBuilder.append("SELECT `songId`,`artistKey` FROM `Song` WHERE `artistKey` IN (")
+    val _inputSize: Int = __mapKeySet.size
+    appendPlaceholders(_stringBuilder, _inputSize)
+    _stringBuilder.append(")")
+    val _sql: String = _stringBuilder.toString()
+    val _stmt: SQLiteStatement = _connection.prepare(_sql)
+    var _argIndex: Int = 1
+    for (_item: Long in __mapKeySet) {
+      _stmt.bindLong(_argIndex, _item)
+      _argIndex++
+    }
+    try {
+      val _itemKeyIndex: Int = getColumnIndex(_stmt, "artistKey")
+      if (_itemKeyIndex == -1) {
+        return
+      }
+      val _cursorIndexOfSongId: Int = 0
+      val _cursorIndexOfArtistKey: Int = 1
+      while (_stmt.step()) {
+        val _tmpKey: Long
+        _tmpKey = _stmt.getLong(_itemKeyIndex)
+        val _tmpRelation: MutableList<Song>? = _map.get(_tmpKey)
+        if (_tmpRelation != null) {
+          val _item_1: Song
+          val _tmpSongId: Long
+          _tmpSongId = _stmt.getLong(_cursorIndexOfSongId)
+          val _tmpArtistKey: Long
+          _tmpArtistKey = _stmt.getLong(_cursorIndexOfArtistKey)
+          _item_1 = Song(_tmpSongId,_tmpArtistKey)
+          _tmpRelation.add(_item_1)
+        }
+      }
+    } finally {
+      _stmt.close()
+    }
+  }
+
+  private fun __fetchRelationshipSongAsSong_1(_connection: SQLiteConnection,
+      _map: MutableMap<Long, MutableList<Song>>) {
+    val __mapKeySet: Set<Long> = _map.keys
+    if (__mapKeySet.isEmpty()) {
+      return
+    }
+    if (_map.size > 999) {
+      recursiveFetchMap(_map, true) { _tmpMap ->
+        __fetchRelationshipSongAsSong_1(_connection, _tmpMap)
+      }
+      return
+    }
+    val _stringBuilder: StringBuilder = StringBuilder()
+    _stringBuilder.append("SELECT `Song`.`songId` AS `songId`,`Song`.`artistKey` AS `artistKey`,_junction.`playlistKey` FROM `PlaylistSongXRef` AS _junction INNER JOIN `Song` ON (_junction.`songKey` = `Song`.`songId`) WHERE _junction.`playlistKey` IN (")
+    val _inputSize: Int = __mapKeySet.size
+    appendPlaceholders(_stringBuilder, _inputSize)
+    _stringBuilder.append(")")
+    val _sql: String = _stringBuilder.toString()
+    val _stmt: SQLiteStatement = _connection.prepare(_sql)
+    var _argIndex: Int = 1
+    for (_item: Long in __mapKeySet) {
+      _stmt.bindLong(_argIndex, _item)
+      _argIndex++
+    }
+    try {
+      // _junction.playlistKey
+      val _itemKeyIndex: Int = 2
+      if (_itemKeyIndex == -1) {
+        return
+      }
+      val _cursorIndexOfSongId: Int = 0
+      val _cursorIndexOfArtistKey: Int = 1
+      while (_stmt.step()) {
+        val _tmpKey: Long
+        _tmpKey = _stmt.getLong(_itemKeyIndex)
+        val _tmpRelation: MutableList<Song>? = _map.get(_tmpKey)
+        if (_tmpRelation != null) {
+          val _item_1: Song
+          val _tmpSongId: Long
+          _tmpSongId = _stmt.getLong(_cursorIndexOfSongId)
+          val _tmpArtistKey: Long
+          _tmpArtistKey = _stmt.getLong(_cursorIndexOfArtistKey)
+          _item_1 = Song(_tmpSongId,_tmpArtistKey)
+          _tmpRelation.add(_item_1)
+        }
+      }
+    } finally {
+      _stmt.close()
+    }
+  }
+
+  public companion object {
+    public fun getRequiredConverters(): List<KClass<*>> = emptyList()
+  }
+}
diff --git a/room/room-paging-guava/src/main/java/androidx/room/paging/guava/LimitOffsetListenableFuturePagingSource.kt b/room/room-paging-guava/src/main/java/androidx/room/paging/guava/LimitOffsetListenableFuturePagingSource.kt
index 0e5309c..2022ec2 100644
--- a/room/room-paging-guava/src/main/java/androidx/room/paging/guava/LimitOffsetListenableFuturePagingSource.kt
+++ b/room/room-paging-guava/src/main/java/androidx/room/paging/guava/LimitOffsetListenableFuturePagingSource.kt
@@ -18,7 +18,6 @@
 
 import android.database.Cursor
 import android.os.CancellationSignal
-import androidx.annotation.NonNull
 import androidx.annotation.RestrictTo
 import androidx.annotation.VisibleForTesting
 import androidx.paging.ListenableFuturePagingSource
@@ -26,12 +25,14 @@
 import androidx.room.RoomDatabase
 import androidx.room.RoomSQLiteQuery
 import androidx.room.guava.createListenableFuture
+import androidx.room.paging.CursorSQLiteStatement
 import androidx.room.paging.util.INITIAL_ITEM_COUNT
 import androidx.room.paging.util.INVALID
 import androidx.room.paging.util.ThreadSafeInvalidationObserver
 import androidx.room.paging.util.getClippedRefreshKey
 import androidx.room.paging.util.queryDatabase
 import androidx.room.paging.util.queryItemCount
+import androidx.sqlite.SQLiteStatement
 import androidx.sqlite.db.SupportSQLiteQuery
 import com.google.common.util.concurrent.Futures
 import com.google.common.util.concurrent.ListenableFuture
@@ -160,7 +161,16 @@
         )
     }
 
-    @NonNull protected abstract fun convertRows(cursor: Cursor): List<Value>
+    protected open fun convertRows(cursor: Cursor): List<Value> {
+        return convertRows(CursorSQLiteStatement(cursor))
+    }
+
+    protected open fun convertRows(statement: SQLiteStatement): List<Value> {
+        throw NotImplementedError(
+            "Unexpected call to a function with no implementation that Room is suppose to " +
+                "generate. Please file a bug at: $BUG_LINK."
+        )
+    }
 
     override val jumpingSupported: Boolean
         get() = true
@@ -168,4 +178,9 @@
     override fun getRefreshKey(state: PagingState<Int, Value>): Int? {
         return state.getClippedRefreshKey()
     }
+
+    companion object {
+        const val BUG_LINK =
+            "https://issuetracker.google.com/issues/new?component=413107&template=1096568"
+    }
 }
diff --git a/room/room-paging-rxjava2/src/main/java/androidx/room/paging/rxjava2/LimitOffsetRxPagingSource.kt b/room/room-paging-rxjava2/src/main/java/androidx/room/paging/rxjava2/LimitOffsetRxPagingSource.kt
index 7bd8f30..0f4f9e0 100644
--- a/room/room-paging-rxjava2/src/main/java/androidx/room/paging/rxjava2/LimitOffsetRxPagingSource.kt
+++ b/room/room-paging-rxjava2/src/main/java/androidx/room/paging/rxjava2/LimitOffsetRxPagingSource.kt
@@ -25,12 +25,14 @@
 import androidx.room.RoomDatabase
 import androidx.room.RoomSQLiteQuery
 import androidx.room.RxRoom
+import androidx.room.paging.CursorSQLiteStatement
 import androidx.room.paging.util.INITIAL_ITEM_COUNT
 import androidx.room.paging.util.INVALID
 import androidx.room.paging.util.ThreadSafeInvalidationObserver
 import androidx.room.paging.util.getClippedRefreshKey
 import androidx.room.paging.util.queryDatabase
 import androidx.room.paging.util.queryItemCount
+import androidx.sqlite.SQLiteStatement
 import androidx.sqlite.db.SupportSQLiteQuery
 import io.reactivex.Single
 import io.reactivex.schedulers.Schedulers
@@ -100,7 +102,17 @@
         return if (invalid) INVALID as LoadResult.Invalid<Int, Value> else result
     }
 
-    @NonNull protected abstract fun convertRows(cursor: Cursor): List<Value>
+    @NonNull
+    protected open fun convertRows(cursor: Cursor): List<Value> {
+        return convertRows(CursorSQLiteStatement(cursor))
+    }
+
+    protected open fun convertRows(statement: SQLiteStatement): List<Value> {
+        throw NotImplementedError(
+            "Unexpected call to a function with no implementation that Room is suppose to " +
+                "generate. Please file a bug at: $BUG_LINK."
+        )
+    }
 
     override fun getRefreshKey(state: PagingState<Int, Value>): Int? {
         return state.getClippedRefreshKey()
@@ -108,4 +120,9 @@
 
     override val jumpingSupported: Boolean
         get() = true
+
+    companion object {
+        const val BUG_LINK =
+            "https://issuetracker.google.com/issues/new?component=413107&template=1096568"
+    }
 }
diff --git a/room/room-paging-rxjava3/src/main/java/androidx/room/paging/rxjava3/LimitOffsetRxPagingSource.kt b/room/room-paging-rxjava3/src/main/java/androidx/room/paging/rxjava3/LimitOffsetRxPagingSource.kt
index a2792a0..b602e0f 100644
--- a/room/room-paging-rxjava3/src/main/java/androidx/room/paging/rxjava3/LimitOffsetRxPagingSource.kt
+++ b/room/room-paging-rxjava3/src/main/java/androidx/room/paging/rxjava3/LimitOffsetRxPagingSource.kt
@@ -17,13 +17,13 @@
 package androidx.room.paging.rxjava3
 
 import android.database.Cursor
-import androidx.annotation.NonNull
 import androidx.annotation.RestrictTo
 import androidx.annotation.VisibleForTesting
 import androidx.paging.PagingState
 import androidx.paging.rxjava3.RxPagingSource
 import androidx.room.RoomDatabase
 import androidx.room.RoomSQLiteQuery
+import androidx.room.paging.CursorSQLiteStatement
 import androidx.room.paging.util.INITIAL_ITEM_COUNT
 import androidx.room.paging.util.INVALID
 import androidx.room.paging.util.ThreadSafeInvalidationObserver
@@ -31,6 +31,7 @@
 import androidx.room.paging.util.queryDatabase
 import androidx.room.paging.util.queryItemCount
 import androidx.room.rxjava3.createSingle
+import androidx.sqlite.SQLiteStatement
 import androidx.sqlite.db.SupportSQLiteQuery
 import io.reactivex.rxjava3.core.Single
 import io.reactivex.rxjava3.schedulers.Schedulers
@@ -100,7 +101,16 @@
         return if (invalid) INVALID as LoadResult.Invalid<Int, Value> else result
     }
 
-    @NonNull protected abstract fun convertRows(cursor: Cursor): List<Value>
+    protected open fun convertRows(cursor: Cursor): List<Value> {
+        return convertRows(CursorSQLiteStatement(cursor))
+    }
+
+    protected open fun convertRows(statement: SQLiteStatement): List<Value> {
+        throw NotImplementedError(
+            "Unexpected call to a function with no implementation that Room is suppose to " +
+                "generate. Please file a bug at: $BUG_LINK."
+        )
+    }
 
     override fun getRefreshKey(state: PagingState<Int, Value>): Int? {
         return state.getClippedRefreshKey()
@@ -108,4 +118,9 @@
 
     override val jumpingSupported: Boolean
         get() = true
+
+    companion object {
+        const val BUG_LINK =
+            "https://issuetracker.google.com/issues/new?component=413107&template=1096568"
+    }
 }
diff --git a/room/room-paging/src/androidMain/kotlin/androidx/room/paging/CursorSQLiteStatement.android.kt b/room/room-paging/src/androidMain/kotlin/androidx/room/paging/SQLiteStatementCursor.android.kt
similarity index 94%
rename from room/room-paging/src/androidMain/kotlin/androidx/room/paging/CursorSQLiteStatement.android.kt
rename to room/room-paging/src/androidMain/kotlin/androidx/room/paging/SQLiteStatementCursor.android.kt
index d9389a0..1fe7955 100644
--- a/room/room-paging/src/androidMain/kotlin/androidx/room/paging/CursorSQLiteStatement.android.kt
+++ b/room/room-paging/src/androidMain/kotlin/androidx/room/paging/SQLiteStatementCursor.android.kt
@@ -19,7 +19,7 @@
 import android.database.AbstractCursor
 import androidx.sqlite.SQLiteStatement
 
-/** Wrapper class for backwards compatibility in room-paging. */
+/** Cursor backed by a SQLiteStatement used for backwards compatibility of Paging APIs. */
 internal class SQLiteStatementCursor(
     private val statement: SQLiteStatement,
     private val rowCount: Int
diff --git a/room/room-runtime/api/restricted_current.txt b/room/room-runtime/api/restricted_current.txt
index a63957d..df44066 100644
--- a/room/room-runtime/api/restricted_current.txt
+++ b/room/room-runtime/api/restricted_current.txt
@@ -485,7 +485,8 @@
     ctor protected LimitOffsetDataSource(androidx.room.RoomDatabase, androidx.room.RoomSQLiteQuery, boolean, java.lang.String!...);
     ctor protected LimitOffsetDataSource(androidx.room.RoomDatabase, androidx.sqlite.db.SupportSQLiteQuery, boolean, boolean, java.lang.String!...);
     ctor protected LimitOffsetDataSource(androidx.room.RoomDatabase, androidx.sqlite.db.SupportSQLiteQuery, boolean, java.lang.String!...);
-    method protected abstract java.util.List<T!> convertRows(android.database.Cursor);
+    method protected java.util.List<T!> convertRows(android.database.Cursor);
+    method protected java.util.List<T!> convertRows(androidx.sqlite.SQLiteStatement);
     method public void loadInitial(androidx.paging.PositionalDataSource.LoadInitialParams, androidx.paging.PositionalDataSource.LoadInitialCallback<T!>);
     method public void loadRange(androidx.paging.PositionalDataSource.LoadRangeParams, androidx.paging.PositionalDataSource.LoadRangeCallback<T!>);
   }
@@ -521,6 +522,7 @@
     method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static android.database.Cursor query(androidx.room.RoomDatabase db, androidx.sqlite.db.SupportSQLiteQuery sqLiteQuery, boolean maybeCopy);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static android.database.Cursor query(androidx.room.RoomDatabase db, androidx.sqlite.db.SupportSQLiteQuery sqLiteQuery, boolean maybeCopy, android.os.CancellationSignal? signal);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @kotlin.jvm.Throws(exceptionClasses=IOException::class) public static int readVersion(java.io.File databaseFile) throws java.io.IOException;
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.sqlite.SQLiteConnection toSQLiteConnection(androidx.sqlite.db.SupportSQLiteDatabase db);
   }
 
   @RestrictTo({androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX}) public final class FileUtil {
diff --git a/room/room-runtime/src/androidMain/java/androidx/room/paging/CursorSQLiteStatement.android.kt b/room/room-runtime/src/androidMain/java/androidx/room/paging/CursorSQLiteStatement.android.kt
new file mode 100644
index 0000000..75b740c
--- /dev/null
+++ b/room/room-runtime/src/androidMain/java/androidx/room/paging/CursorSQLiteStatement.android.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.paging
+
+import android.database.Cursor
+import android.database.Cursor.FIELD_TYPE_BLOB
+import android.database.Cursor.FIELD_TYPE_FLOAT
+import android.database.Cursor.FIELD_TYPE_INTEGER
+import android.database.Cursor.FIELD_TYPE_NULL
+import android.database.Cursor.FIELD_TYPE_STRING
+import androidx.annotation.RestrictTo
+import androidx.sqlite.SQLITE_DATA_BLOB
+import androidx.sqlite.SQLITE_DATA_FLOAT
+import androidx.sqlite.SQLITE_DATA_INTEGER
+import androidx.sqlite.SQLITE_DATA_NULL
+import androidx.sqlite.SQLITE_DATA_TEXT
+import androidx.sqlite.SQLiteStatement
+
+/** SQLiteStatement backed by a Cursor used for backwards compatibility of Paging2 APIs. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class CursorSQLiteStatement(private val cursor: Cursor) : SQLiteStatement {
+    override fun getBlob(index: Int): ByteArray = cursor.getBlob(index)
+
+    override fun getDouble(index: Int): Double = cursor.getDouble(index)
+
+    override fun getLong(index: Int): Long = cursor.getLong(index)
+
+    override fun getText(index: Int): String = cursor.getString(index)
+
+    override fun isNull(index: Int): Boolean = cursor.isNull(index)
+
+    override fun getColumnCount(): Int = cursor.columnCount
+
+    override fun getColumnName(index: Int): String = cursor.getColumnName(index)
+
+    override fun getColumnType(index: Int): Int = cursor.getDataType(index)
+
+    override fun step(): Boolean = cursor.moveToNext()
+
+    override fun reset() {
+        cursor.moveToPosition(-1)
+    }
+
+    override fun close() {
+        cursor.close()
+    }
+
+    override fun bindBlob(index: Int, value: ByteArray) =
+        error("Only get*() calls are allowed on a Cursor backed SQLiteStatement")
+
+    override fun bindDouble(index: Int, value: Double) =
+        error("Only get*() calls are allowed on a Cursor backed SQLiteStatement")
+
+    override fun bindLong(index: Int, value: Long) =
+        error("Only get*() calls are allowed on a Cursor backed SQLiteStatement")
+
+    override fun bindText(index: Int, value: String) =
+        error("Only get*() calls are allowed on a Cursor backed SQLiteStatement")
+
+    override fun bindNull(index: Int) =
+        error("Only get*() calls are allowed on a Cursor backed SQLiteStatement")
+
+    override fun clearBindings() =
+        error("Only get*() calls are allowed on a Cursor backed SQLiteStatement")
+
+    companion object {
+        private fun Cursor.getDataType(index: Int): Int {
+            val fieldType = this.getType(index)
+            return when (this.getType(index)) {
+                FIELD_TYPE_NULL -> SQLITE_DATA_NULL
+                FIELD_TYPE_INTEGER -> SQLITE_DATA_INTEGER
+                FIELD_TYPE_FLOAT -> SQLITE_DATA_FLOAT
+                FIELD_TYPE_STRING -> SQLITE_DATA_TEXT
+                FIELD_TYPE_BLOB -> SQLITE_DATA_BLOB
+                else -> error("Unknown field type: $fieldType")
+            }
+        }
+    }
+}
diff --git a/room/room-runtime/src/androidMain/java/androidx/room/paging/LimitOffsetDataSource.java b/room/room-runtime/src/androidMain/java/androidx/room/paging/LimitOffsetDataSource.java
index 2b5c391..3cdc6c0 100644
--- a/room/room-runtime/src/androidMain/java/androidx/room/paging/LimitOffsetDataSource.java
+++ b/room/room-runtime/src/androidMain/java/androidx/room/paging/LimitOffsetDataSource.java
@@ -23,6 +23,7 @@
 import androidx.room.InvalidationTracker;
 import androidx.room.RoomDatabase;
 import androidx.room.RoomSQLiteQuery;
+import androidx.sqlite.SQLiteStatement;
 import androidx.sqlite.db.SupportSQLiteQuery;
 
 import java.util.Collections;
@@ -145,7 +146,21 @@
 
     @NonNull
     @SuppressWarnings("WeakerAccess")
-    protected abstract List<T> convertRows(@NonNull Cursor cursor);
+    protected List<T> convertRows(@NonNull Cursor cursor) {
+        return convertRows(new CursorSQLiteStatement(cursor));
+    }
+
+    @NonNull
+    protected List<T> convertRows(@NonNull SQLiteStatement statement) {
+        throw new UnsupportedOperationException(
+                "Unexpected call to a function with no implementation that Room is supposed to "
+                    + "generate. Please file a bug at: "
+                    + BUG_LINK + "."
+        );
+    }
+
+    private static final String BUG_LINK =
+            "https://issuetracker.google.com/issues/new?component=413107&template=1096568";
 
     @SuppressWarnings("deprecation")
     @Override
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/util/DBUtil.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/util/DBUtil.android.kt
index 8cb5d70..7faeb6a 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/util/DBUtil.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/util/DBUtil.android.kt
@@ -234,3 +234,8 @@
 fun createCancellationSignal(): CancellationSignal {
     return CancellationSignal()
 }
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+fun toSQLiteConnection(db: SupportSQLiteDatabase): SQLiteConnection {
+    return SupportSQLiteConnection(db)
+}