Migrating RelationCollector to driver APIs.

This CL introduces a follow-up TODO item which would be
to add support for LongSparseArray when room-runtime can
depend on androidx.collections in commonMain.

Bug: 299168035
Bug: 333906821
Test: DaoRelationshipKotlinCodeGenTest.kt
Change-Id: I855a47d2afd73993117e4259d68706976cec269f
diff --git a/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BaseQueryTest.kt b/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BaseQueryTest.kt
index 1eb28cf..0166c7d 100644
--- a/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BaseQueryTest.kt
+++ b/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BaseQueryTest.kt
@@ -416,4 +416,57 @@
         assertContentEquals(arrayOf(1, 2), resultArrayWithLong)
         assertContentEquals(longArrayOf(1, 2), resultLongArray)
     }
+
+    @Test
+    fun relation1to1() = runTest {
+        val sampleEntity1 = SampleEntity(1, 1)
+        val sampleEntity2 = SampleEntity2(1, 2)
+        db.dao().insert(sampleEntity1)
+        db.dao().insert(sampleEntity2)
+        assertThat(
+            db.dao().getSample1To2()
+        ).isEqualTo(
+            SampleDao.Sample1And2(sample1 = sampleEntity1, sample2 = sampleEntity2)
+        )
+    }
+
+    @Test
+    fun relation1toMany() = runTest {
+        val sampleEntity1 = SampleEntity(1, 1)
+        val sampleEntity2 = SampleEntity2(1, 2)
+        val sampleEntity2s = listOf(sampleEntity2, SampleEntity2(2, 3))
+
+        db.dao().insert(sampleEntity1)
+        db.dao().insertSampleEntity2List(sampleEntity2s)
+
+        assertThat(
+            db.dao().getSample1ToMany()
+        ).isEqualTo(
+            SampleDao.Sample1AndMany(
+                sample1 = sampleEntity1,
+                sample2s = listOf(sampleEntity2)
+            )
+        )
+    }
+
+    @Test
+    fun relationManytoMany() = runTest {
+        val sampleEntity1 = SampleEntity(1, 1)
+        val sampleEntity1s = listOf(sampleEntity1, SampleEntity(2, 2))
+
+        val sampleEntity2 = SampleEntity2(1, 1)
+        val sampleEntity2s = listOf(sampleEntity2, SampleEntity2(2, 2))
+
+        db.dao().insertSampleEntityList(sampleEntity1s)
+        db.dao().insertSampleEntity2List(sampleEntity2s)
+
+        assertThat(
+            db.dao().getSampleManyToMany()
+        ).isEqualTo(
+            SampleDao.SampleManyAndMany(
+                sample1 = sampleEntity1,
+                sample2s = listOf()
+            )
+        )
+    }
 }
diff --git a/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SampleDatabase.kt b/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SampleDatabase.kt
index 8787aed..f41e88e 100644
--- a/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SampleDatabase.kt
+++ b/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SampleDatabase.kt
@@ -20,12 +20,16 @@
 import androidx.room.Dao
 import androidx.room.Database
 import androidx.room.Delete
+import androidx.room.Embedded
 import androidx.room.Entity
 import androidx.room.ForeignKey
+import androidx.room.Index
 import androidx.room.Insert
+import androidx.room.Junction
 import androidx.room.MapColumn
 import androidx.room.PrimaryKey
 import androidx.room.Query
+import androidx.room.Relation
 import androidx.room.RewriteQueriesToDropUnusedColumns
 import androidx.room.RoomDatabase
 import androidx.room.Transaction
@@ -71,6 +75,15 @@
     val dataCopy: Long
 )
 
+@Entity(
+    primaryKeys = ["sample1Key", "sample2Key"],
+    indices = [Index("sample1Key"), Index("sample2Key")]
+)
+data class Sample1Sample2XRef(
+    val sample1Key: Long,
+    val sample2Key: Long,
+)
+
 @Dao
 interface SampleDao {
 
@@ -142,6 +155,12 @@
     suspend fun insertArray(entities: Array<SampleEntity>)
 
     @Insert
+    suspend fun insertSampleEntityList(entities: List<SampleEntity>)
+
+    @Insert
+    suspend fun insertSampleEntity2List(entities: List<SampleEntity2>)
+
+    @Insert
     suspend fun insert(entity: SampleEntity2)
 
     @Insert
@@ -167,6 +186,47 @@
 
     @Query("SELECT pk FROM SampleEntity")
     suspend fun queryOfLongArray(): LongArray
+
+    @Transaction
+    @Query("SELECT * FROM SampleEntity")
+    suspend fun getSample1To2(): Sample1And2
+
+    @Transaction
+    @Query("SELECT * FROM SampleEntity")
+    suspend fun getSample1ToMany(): Sample1AndMany
+
+    @Transaction
+    @Query("SELECT * FROM SampleEntity")
+    suspend fun getSampleManyToMany(): SampleManyAndMany
+
+    data class Sample1And2(
+        @Embedded
+        val sample1: SampleEntity,
+        @Relation(parentColumn = "pk", entityColumn = "pk2")
+        val sample2: SampleEntity2
+    )
+
+    data class Sample1AndMany(
+        @Embedded
+        val sample1: SampleEntity,
+        @Relation(parentColumn = "pk", entityColumn = "pk2")
+        val sample2s: List<SampleEntity2>
+    )
+
+    data class SampleManyAndMany(
+        @Embedded
+        val sample1: SampleEntity,
+        @Relation(
+            parentColumn = "pk",
+            entityColumn = "pk2",
+            associateBy = Junction(
+                value = Sample1Sample2XRef::class,
+                parentColumn = "sample1Key",
+                entityColumn = "sample2Key"
+            )
+        )
+        val sample2s: List<SampleEntity2>
+    )
 }
 
 @Database(
@@ -174,7 +234,8 @@
         SampleEntity::class,
         SampleEntity2::class,
         SampleEntity3::class,
-        SampleEntityCopy::class],
+        SampleEntityCopy::class,
+        Sample1Sample2XRef::class],
     version = 1,
     exportSchema = false
 )
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
index 99041b3..5c711a60 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
@@ -89,7 +89,7 @@
     }
 
     fun primaryKeyColumnDoesNotExist(columnName: String, allColumns: List<String>): String {
-        return "$columnName referenced in the primary key does not exists in the Entity." +
+        return "$columnName referenced in the primary key does not exist in the Entity." +
             " Available column names:${allColumns.joinToString(", ")}"
     }
 
@@ -389,7 +389,7 @@
     val INDEX_COLUMNS_CANNOT_BE_EMPTY = "List of columns in an index cannot be empty"
 
     fun indexColumnDoesNotExist(columnName: String, allColumns: List<String>): String {
-        return "$columnName referenced in the index does not exists in the Entity." +
+        return "$columnName referenced in the index does not exist in the Entity." +
             " Available column names:${allColumns.joinToString(", ")}"
     }
 
@@ -554,7 +554,7 @@
     val FOREIGN_KEY_CANNOT_FIND_PARENT = "Cannot find parent entity class."
 
     fun foreignKeyChildColumnDoesNotExist(columnName: String, allColumns: List<String>): String {
-        return "($columnName) referenced in the foreign key does not exists in the Entity." +
+        return "($columnName) referenced in the foreign key does not exist in the Entity." +
             " Available column names:${allColumns.joinToString(", ")}"
     }
 
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PojoRowAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PojoRowAdapter.kt
index e7ff05d..e245f35 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PojoRowAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PojoRowAdapter.kt
@@ -151,9 +151,7 @@
 
     override fun getDefaultIndexAdapter() = indexAdapter
 
-    override fun isMigratedToDriver(): Boolean {
-        return relationCollectors.isEmpty()
-    }
+    override fun isMigratedToDriver(): Boolean = relationCollectors.all { it.isMigratedToDriver() }
 
     data class PojoMapping(
         val pojo: Pojo,
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/vo/RelationCollector.kt b/room/room-compiler/src/main/kotlin/androidx/room/vo/RelationCollector.kt
index eceddad..e005bed 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/vo/RelationCollector.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/vo/RelationCollector.kt
@@ -22,8 +22,15 @@
 import androidx.room.compiler.codegen.XCodeBlock.Builder.Companion.addLocalVal
 import androidx.room.compiler.codegen.XTypeName
 import androidx.room.compiler.processing.XNullability
-import androidx.room.ext.CollectionTypeNames
+import androidx.room.ext.CollectionTypeNames.ARRAY_MAP
+import androidx.room.ext.CollectionTypeNames.LONG_SPARSE_ARRAY
 import androidx.room.ext.CommonTypeNames
+import androidx.room.ext.CommonTypeNames.ARRAY_LIST
+import androidx.room.ext.CommonTypeNames.HASH_MAP
+import androidx.room.ext.CommonTypeNames.HASH_SET
+import androidx.room.ext.KotlinCollectionMemberNames
+import androidx.room.ext.KotlinCollectionMemberNames.MUTABLE_LIST_OF
+import androidx.room.ext.KotlinCollectionMemberNames.MUTABLE_SET_OF
 import androidx.room.ext.capitalize
 import androidx.room.ext.stripNonJava
 import androidx.room.parser.ParsedQuery
@@ -43,6 +50,7 @@
 import androidx.room.verifier.DatabaseVerificationErrors
 import androidx.room.writer.QueryWriter
 import androidx.room.writer.RelationCollectorFunctionWriter
+import androidx.room.writer.RelationCollectorFunctionWriter.Companion.PARAM_CONNECTION_VARIABLE
 import java.util.Locale
 
 /**
@@ -72,20 +80,35 @@
     val relationTypeIsCollection: Boolean,
     val javaLambdaSyntaxAvailable: Boolean
 ) {
+    // TODO(b/319660042): Remove once migration to driver API is done.
+    fun isMigratedToDriver(): Boolean = rowAdapter.isMigratedToDriver()
+
     // variable name of map containing keys to relation collections, set when writing the code
     // generator in writeInitCode
-    lateinit var varName: String
+    private lateinit var varName: String
 
     fun writeInitCode(scope: CodeGenScope) {
         varName = scope.getTmpVar(
             "_collection${relation.field.getPath().stripNonJava().capitalize(Locale.US)}"
         )
         scope.builder.apply {
-            addLocalVariable(
-                name = varName,
-                typeName = mapTypeName,
-                assignExpr = XCodeBlock.ofNewInstance(language, mapTypeName)
-            )
+            if (language == CodeLanguage.JAVA ||
+                mapTypeName.rawTypeName == ARRAY_MAP ||
+                mapTypeName.rawTypeName == LONG_SPARSE_ARRAY
+            ) {
+                addLocalVariable(
+                    name = varName,
+                    typeName = mapTypeName,
+                    assignExpr = XCodeBlock.ofNewInstance(language, mapTypeName)
+                )
+            } else {
+                addLocalVal(
+                    name = varName,
+                    typeName = mapTypeName,
+                    "%M()",
+                    KotlinCollectionMemberNames.MUTABLE_MAP_OF
+                )
+            }
         }
     }
 
@@ -106,9 +129,25 @@
                 // for relation collection put an empty collections in the map, otherwise put nulls
                 if (relationTypeIsCollection) {
                     beginControlFlow("if (!%L.containsKey(%L))", varName, tmpVar).apply {
+                        val newEmptyCollection = when (language) {
+                            CodeLanguage.JAVA ->
+                                XCodeBlock.ofNewInstance(language, relationTypeName)
+                            CodeLanguage.KOTLIN ->
+                                XCodeBlock.of(
+                                    language = language,
+                                    "%M()",
+                                    if (relationTypeName == CommonTypeNames.MUTABLE_SET) {
+                                        MUTABLE_SET_OF
+                                    } else {
+                                        MUTABLE_LIST_OF
+                                    }
+                                )
+                        }
                         addStatement(
                             "%L.put(%L, %L)",
-                            varName, tmpVar, XCodeBlock.ofNewInstance(language, relationTypeName)
+                            varName,
+                            tmpVar,
+                            newEmptyCollection
                         )
                     }
                     endControlFlow()
@@ -152,7 +191,7 @@
                         // values for all keys, so this is safe. Special case for LongSParseArray
                         // since it does not have a getValue() from Kotlin.
                         val usingLongSparseArray =
-                            mapTypeName.rawTypeName == CollectionTypeNames.LONG_SPARSE_ARRAY
+                            mapTypeName.rawTypeName == LONG_SPARSE_ARRAY
                         when (language) {
                             CodeLanguage.JAVA -> addStatement(
                                 "%L = %L.get(%L)",
@@ -187,9 +226,23 @@
                 },
                 onKeyUnavailable = {
                     if (relationTypeIsCollection) {
+                        val newEmptyCollection = when (language) {
+                            CodeLanguage.JAVA ->
+                                XCodeBlock.ofNewInstance(language, relationTypeName)
+                            CodeLanguage.KOTLIN ->
+                                XCodeBlock.of(
+                                    language = language,
+                                    "%M()",
+                                    if (relationTypeName == CommonTypeNames.MUTABLE_SET) {
+                                        MUTABLE_SET_OF
+                                    } else {
+                                        MUTABLE_LIST_OF
+                                    }
+                                )
+                        }
                         addStatement(
                             "%L = %L",
-                            tmpRelationVar, XCodeBlock.ofNewInstance(language, relationTypeName)
+                            tmpRelationVar, newEmptyCollection
                         )
                     } else {
                         addStatement("%L = null", tmpRelationVar)
@@ -203,8 +256,14 @@
     // called to write the invocation to the fetch relationship method
     fun writeFetchRelationCall(scope: CodeGenScope) {
         val method = scope.writer
-            .getOrCreateFunction(RelationCollectorFunctionWriter(this))
-        scope.builder.addStatement("%L(%L)", method.name, varName)
+            .getOrCreateFunction(RelationCollectorFunctionWriter(this, scope.useDriverApi))
+        scope.builder.apply {
+            if (scope.useDriverApi) {
+                addStatement("%L(%L, %L)", method.name, PARAM_CONNECTION_VARIABLE, varName)
+            } else {
+                addStatement("%L(%L)", method.name, varName)
+            }
+        }
     }
 
     // called to read key and call `onKeyReady` to write code once it is successfully read
@@ -339,10 +398,10 @@
                 val resultInfo = parsedQuery.resultInfo
 
                 val usingLongSparseArray =
-                    tmpMapTypeName.rawTypeName == CollectionTypeNames.LONG_SPARSE_ARRAY
+                    tmpMapTypeName.rawTypeName == LONG_SPARSE_ARRAY
                 val queryParam = if (usingLongSparseArray) {
                     val longSparseArrayElement = context.processingEnv
-                        .requireTypeElement(CollectionTypeNames.LONG_SPARSE_ARRAY.canonicalName)
+                        .requireTypeElement(LONG_SPARSE_ARRAY.canonicalName)
                     QueryParameter(
                         name = RelationCollectorFunctionWriter.PARAM_MAP_VARIABLE,
                         sqlName = RelationCollectorFunctionWriter.PARAM_MAP_VARIABLE,
@@ -505,13 +564,20 @@
             if (fieldType.typeArguments.isNotEmpty()) {
                 val rawType = fieldType.rawType
                 val paramTypeName =
-                    if (context.COMMON_TYPES.LIST.rawType.isAssignableFrom(rawType)) {
-                        CommonTypeNames.ARRAY_LIST.parametrizedBy(relation.pojoTypeName)
-                    } else if (context.COMMON_TYPES.SET.rawType.isAssignableFrom(rawType)) {
-                        CommonTypeNames.HASH_SET.parametrizedBy(relation.pojoTypeName)
+                    if (context.COMMON_TYPES.SET.rawType.isAssignableFrom(rawType)) {
+                        when (context.codeLanguage) {
+                            CodeLanguage.KOTLIN ->
+                                CommonTypeNames.MUTABLE_SET.parametrizedBy(relation.pojoTypeName)
+                            CodeLanguage.JAVA ->
+                                HASH_SET.parametrizedBy(relation.pojoTypeName)
+                        }
                     } else {
-                        // Default to ArrayList and see how things go...
-                        CommonTypeNames.ARRAY_LIST.parametrizedBy(relation.pojoTypeName)
+                        when (context.codeLanguage) {
+                            CodeLanguage.KOTLIN ->
+                                CommonTypeNames.MUTABLE_LIST.parametrizedBy(relation.pojoTypeName)
+                            CodeLanguage.JAVA ->
+                                ARRAY_LIST.parametrizedBy(relation.pojoTypeName)
+                        }
                     }
                 paramTypeName to true
             } else {
@@ -526,24 +592,30 @@
             keyTypeName: XTypeName,
             valueTypeName: XTypeName,
         ): XTypeName {
-            val canUseLongSparseArray = context.processingEnv
-                .findTypeElement(CollectionTypeNames.LONG_SPARSE_ARRAY.canonicalName) != null
-            val canUseArrayMap = context.processingEnv
-                .findTypeElement(CollectionTypeNames.ARRAY_MAP.canonicalName) != null
+            val canUseLongSparseArray =
+                context.processingEnv
+                    .findTypeElement(LONG_SPARSE_ARRAY.canonicalName) != null
+            val canUseArrayMap =
+                context.processingEnv
+                    .findTypeElement(ARRAY_MAP.canonicalName) != null
             return when {
                 canUseLongSparseArray && affinity == SQLTypeAffinity.INTEGER ->
-                    CollectionTypeNames.LONG_SPARSE_ARRAY.parametrizedBy(valueTypeName)
+                    LONG_SPARSE_ARRAY.parametrizedBy(valueTypeName)
                 canUseArrayMap ->
-                    CollectionTypeNames.ARRAY_MAP.parametrizedBy(keyTypeName, valueTypeName)
-                else ->
-                    CommonTypeNames.HASH_MAP.parametrizedBy(keyTypeName, valueTypeName)
+                    ARRAY_MAP.parametrizedBy(keyTypeName, valueTypeName)
+                else -> when (context.codeLanguage) {
+                    CodeLanguage.JAVA ->
+                        HASH_MAP.parametrizedBy(keyTypeName, valueTypeName)
+                    CodeLanguage.KOTLIN ->
+                        CommonTypeNames.MUTABLE_MAP.parametrizedBy(keyTypeName, valueTypeName)
+                }
             }
         }
 
         // Gets the type name of the relationship key.
         private fun keyTypeFor(context: Context, affinity: SQLTypeAffinity): XTypeName {
             val canUseLongSparseArray = context.processingEnv
-                .findTypeElement(CollectionTypeNames.LONG_SPARSE_ARRAY.canonicalName) != null
+                .findTypeElement(LONG_SPARSE_ARRAY.canonicalName) != null
             return when (affinity) {
                 SQLTypeAffinity.INTEGER ->
                     if (canUseLongSparseArray) {
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/RelationCollectorFunctionWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/RelationCollectorFunctionWriter.kt
index 1ecfd81..3c63ece 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/RelationCollectorFunctionWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/RelationCollectorFunctionWriter.kt
@@ -32,6 +32,8 @@
 import androidx.room.ext.MapKeySetExprCode
 import androidx.room.ext.RoomMemberNames
 import androidx.room.ext.RoomTypeNames
+import androidx.room.ext.RoomTypeNames.RELATION_UTIL
+import androidx.room.ext.SQLiteDriverTypeNames
 import androidx.room.ext.stripNonJava
 import androidx.room.solver.CodeGenScope
 import androidx.room.solver.query.result.PojoRowAdapter
@@ -41,13 +43,20 @@
  * Writes the function that fetches the relations of a POJO and assigns them into the given map.
  */
 class RelationCollectorFunctionWriter(
-    private val collector: RelationCollector
+    private val collector: RelationCollector,
+    private val useDriverApi: Boolean
 ) : TypeWriter.SharedFunctionSpec(
-    "fetchRelationship${collector.relation.entity.tableName.stripNonJava()}" +
-        "As${collector.relation.pojoTypeName.toString(CodeLanguage.JAVA).stripNonJava()}"
+    baseName = if (useDriverApi) {
+        "fetchRelationship${collector.relation.entity.tableName.stripNonJava()}" +
+            "As${collector.relation.pojoTypeName.toString(CodeLanguage.JAVA).stripNonJava()}"
+    } else {
+        "fetchCompatRelationship${collector.relation.entity.tableName.stripNonJava()}" +
+            "As${collector.relation.pojoTypeName.toString(CodeLanguage.JAVA).stripNonJava()}"
+    },
 ) {
     companion object {
         const val PARAM_MAP_VARIABLE = "_map"
+        const val PARAM_CONNECTION_VARIABLE = "_connection"
         const val KEY_SET_VARIABLE = "__mapKeySet"
     }
 
@@ -63,11 +72,12 @@
             "-${relation.entity.typeName.toString(CodeLanguage.JAVA)}" +
             "-${relation.entityField.columnName}" +
             "-${relation.pojoTypeName}" +
-            "-${relation.createLoadAllSql()}"
+            "-${relation.createLoadAllSql()}" +
+            "-$useDriverApi"
     }
 
     override fun prepare(methodName: String, writer: TypeWriter, builder: XFunSpec.Builder) {
-        val scope = CodeGenScope(writer)
+        val scope = CodeGenScope(writer = writer, useDriverApi = useDriverApi)
         scope.builder.apply {
             // Check the input map key set for emptiness, returning early as no fetching is needed.
             addIsInputEmptyCheck()
@@ -75,25 +85,59 @@
             // Check if the input map key set exceeds MAX_BIND_PARAMETER_CNT, if so do a recursive
             // fetch.
             beginControlFlow(
-                "if (%L > %T.MAX_BIND_PARAMETER_CNT)",
+                "if (%L > %L)",
                 if (usingLongSparseArray) {
                     XCodeBlock.of(language, "%L.size()", PARAM_MAP_VARIABLE)
                 } else {
                     CollectionsSizeExprCode(language, PARAM_MAP_VARIABLE)
                 },
-                RoomTypeNames.ROOM_DB
+                if (useDriverApi) {
+                    "999"
+                } else {
+                    XCodeBlock.of(
+                        language,
+                        "%T.MAX_BIND_PARAMETER_CNT",
+                        RoomTypeNames.ROOM_DB
+                    )
+                }
             ).apply {
                 addRecursiveFetchCall(methodName)
                 addStatement("return")
             }.endControlFlow()
 
-            // Create SQL query, acquire statement and bind parameters.
-            val stmtVar = scope.getTmpVar("_stmt")
-            val sqlQueryVar = scope.getTmpVar("_sql")
-            collector.queryWriter.prepareReadAndBind(sqlQueryVar, stmtVar, scope)
+            createStmtAndReturn(scope)
+        }
+        builder.apply {
+            if (useDriverApi) {
+                addParameter(SQLiteDriverTypeNames.CONNECTION, PARAM_CONNECTION_VARIABLE)
+            }
+            addParameter(collector.mapTypeName, PARAM_MAP_VARIABLE)
+            addCode(scope.generate())
+        }
+    }
 
+    private fun XCodeBlock.Builder.createStmtAndReturn(
+        scope: CodeGenScope
+    ) {
+        // Create SQL query, acquire statement and bind parameters.
+        val stmtVar = scope.getTmpVar("_stmt")
+        val cursorVar = "_cursor"
+        val sqlQueryVar = scope.getTmpVar("_sql")
+
+        if (useDriverApi) {
+            val connectionVar = scope.getTmpVar(PARAM_CONNECTION_VARIABLE)
+            val listSizeVars = collector.queryWriter.prepareQuery(sqlQueryVar, scope)
+            addLocalVal(
+                stmtVar,
+                SQLiteDriverTypeNames.STATEMENT,
+                "%L.prepare(%L)",
+                connectionVar,
+                sqlQueryVar
+            )
+            collector.queryWriter.bindArgs(stmtVar, listSizeVars, scope)
+        } else {
+            collector.queryWriter.prepareReadAndBind(sqlQueryVar, stmtVar, scope)
             // Perform query and get a Cursor
-            val cursorVar = "_cursor"
             val shouldCopyCursor = collector.rowAdapter.let {
                 it is PojoRowAdapter && it.relationCollectors.isNotEmpty()
             }
@@ -110,86 +154,91 @@
                     "null"
                 )
             )
+        }
+        addRelationCollectorCode(scope, if (useDriverApi) stmtVar else cursorVar)
+    }
 
-            val relation = collector.relation
-            beginControlFlow("try").apply {
-                // Gets index of the column to be used as key
-                val itemKeyIndexVar = "_itemKeyIndex"
-                if (relation.junction != null) {
-                    // When using a junction table the relationship map is keyed on the parent
-                    // reference column of the junction table, the same column used in the WHERE IN
-                    // clause, this column is the rightmost column in the generated SELECT
-                    // clause.
-                    val junctionParentColumnIndex = relation.projection.size
-                    addStatement("// _junction.%L", relation.junction.parentField.columnName)
-                    addLocalVal(
-                        itemKeyIndexVar,
-                        XTypeName.PRIMITIVE_INT,
-                        "%L",
-                        junctionParentColumnIndex
-                    )
-                } else {
-                    addLocalVal(
-                        itemKeyIndexVar,
-                        XTypeName.PRIMITIVE_INT,
-                        "%M(%L, %S)",
-                        RoomMemberNames.CURSOR_UTIL_GET_COLUMN_INDEX,
-                        cursorVar,
-                        relation.entityField.columnName
-                    )
-                }
-                // Check if index of column is not -1, indicating the column for the key is not in
-                // the result, can happen if the user specified a bad projection in @Relation.
-                beginControlFlow("if (%L == -1)", itemKeyIndexVar).apply {
-                    addStatement("return")
-                }
-                endControlFlow()
+    private fun XCodeBlock.Builder.addRelationCollectorCode(
+        scope: CodeGenScope,
+        cursorVar: String
+    ) {
+        val relation = collector.relation
+        beginControlFlow("try").apply {
+            // Gets index of the column to be used as key
+            val itemKeyIndexVar = "_itemKeyIndex"
+            if (relation.junction != null) {
+                // When using a junction table the relationship map is keyed on the parent
+                // reference column of the junction table, the same column used in the WHERE IN
+                // clause, this column is the rightmost column in the generated SELECT
+                // clause.
+                val junctionParentColumnIndex = relation.projection.size
+                addStatement("// _junction.%L", relation.junction.parentField.columnName)
+                addLocalVal(
+                    itemKeyIndexVar,
+                    XTypeName.PRIMITIVE_INT,
+                    "%L",
+                    junctionParentColumnIndex
+                )
+            } else {
+                addLocalVal(
+                    name = itemKeyIndexVar,
+                    typeName = XTypeName.PRIMITIVE_INT,
+                    assignExprFormat = "%M(%L, %S)",
+                    if (useDriverApi) {
+                        RoomTypeNames.STATEMENT_UTIL.packageMember("getColumnIndex")
+                    } else { RoomMemberNames.CURSOR_UTIL_GET_COLUMN_INDEX },
+                    cursorVar,
+                    relation.entityField.columnName
+                )
+            }
 
-                // Prepare item column indices
-                collector.rowAdapter.onCursorReady(cursorVarName = cursorVar, scope = scope)
+            // Check if index of column is not -1, indicating the column for the key is not in
+            // the result, can happen if the user specified a bad projection in @Relation.
+            beginControlFlow("if (%L == -1)", itemKeyIndexVar).apply {
+                addStatement("return")
+            }
+            endControlFlow()
 
-                val tmpVarName = scope.getTmpVar("_item")
-                beginControlFlow("while (%L.moveToNext())", cursorVar).apply {
-                    // Read key from the cursor, convert row to item and place it on map
-                    collector.readKey(
-                        cursorVarName = cursorVar,
-                        indexVar = itemKeyIndexVar,
-                        keyReader = collector.entityKeyColumnReader,
-                        scope = scope
-                    ) { keyVar ->
-                        if (collector.relationTypeIsCollection) {
-                            val relationVar = scope.getTmpVar("_tmpRelation")
-                            addLocalVal(
-                                relationVar,
-                                collector.relationTypeName.copy(nullable = true),
-                                "%L.get(%L)",
-                                PARAM_MAP_VARIABLE, keyVar
-                            )
-                            beginControlFlow("if (%L != null)", relationVar)
-                            addLocalVariable(tmpVarName, relation.pojoTypeName)
-                            collector.rowAdapter.convert(tmpVarName, cursorVar, scope)
-                            addStatement("%L.add(%L)", relationVar, tmpVarName)
-                            endControlFlow()
-                        } else {
-                            beginControlFlow("if (%N.containsKey(%L))", PARAM_MAP_VARIABLE, keyVar)
-                            addLocalVariable(tmpVarName, relation.pojoTypeName)
-                            collector.rowAdapter.convert(tmpVarName, cursorVar, scope)
-                            addStatement("%N.put(%L, %L)", PARAM_MAP_VARIABLE, keyVar, tmpVarName)
-                            endControlFlow()
-                        }
+            // Prepare item column indices
+            collector.rowAdapter.onCursorReady(cursorVarName = cursorVar, scope = scope)
+            val tmpVarName = scope.getTmpVar("_item")
+            val stepName = if (scope.useDriverApi) "step" else "moveToNext"
+            beginControlFlow("while (%L.$stepName())", cursorVar).apply {
+                // Read key from the cursor, convert row to item and place it on map
+                collector.readKey(
+                    cursorVarName = cursorVar,
+                    indexVar = itemKeyIndexVar,
+                    keyReader = collector.entityKeyColumnReader,
+                    scope = scope
+                ) { keyVar ->
+                    if (collector.relationTypeIsCollection) {
+                        val relationVar = scope.getTmpVar("_tmpRelation")
+                        addLocalVal(
+                            relationVar,
+                            collector.relationTypeName.copy(nullable = true),
+                            "%L.get(%L)",
+                            PARAM_MAP_VARIABLE, keyVar
+                        )
+                        beginControlFlow("if (%L != null)", relationVar)
+                        addLocalVariable(tmpVarName, relation.pojoTypeName)
+                        collector.rowAdapter.convert(tmpVarName, cursorVar, scope)
+                        addStatement("%L.add(%L)", relationVar, tmpVarName)
+                        endControlFlow()
+                    } else {
+                        beginControlFlow("if (%N.containsKey(%L))", PARAM_MAP_VARIABLE, keyVar)
+                        addLocalVariable(tmpVarName, relation.pojoTypeName)
+                        collector.rowAdapter.convert(tmpVarName, cursorVar, scope)
+                        addStatement("%N.put(%L, %L)", PARAM_MAP_VARIABLE, keyVar, tmpVarName)
+                        endControlFlow()
                     }
                 }
-                endControlFlow()
-            }
-            nextControlFlow("finally").apply {
-                addStatement("%L.close()", cursorVar)
             }
             endControlFlow()
         }
-        builder.apply {
-            addParameter(collector.mapTypeName, PARAM_MAP_VARIABLE)
-            addCode(scope.generate())
+        nextControlFlow("finally").apply {
+            addStatement("%L.close()", cursorVar)
         }
+        endControlFlow()
     }
 
     private fun XCodeBlock.Builder.addIsInputEmptyCheck() {
@@ -211,20 +260,34 @@
 
     private fun XCodeBlock.Builder.addRecursiveFetchCall(methodName: String) {
         fun getRecursiveCall(itVarName: String) =
-            XCodeBlock.of(
-                language,
-                "%L(%L)",
-                methodName, itVarName
-            )
+            if (useDriverApi) {
+                XCodeBlock.of(
+                    language,
+                    "%L(%L, %L)",
+                    methodName, PARAM_CONNECTION_VARIABLE, itVarName
+                )
+            } else {
+                XCodeBlock.of(
+                    language,
+                    "%L(%L)",
+                    methodName, itVarName
+                )
+            }
         val utilFunction =
-            RoomTypeNames.RELATION_UTIL.let {
+            RELATION_UTIL.let {
                 when {
                     usingLongSparseArray ->
                         it.packageMember("recursiveFetchLongSparseArray")
                     usingArrayMap ->
                         it.packageMember("recursiveFetchArrayMap")
-                    else ->
+                    language == CodeLanguage.JAVA ->
                         it.packageMember("recursiveFetchHashMap")
+                    language == CodeLanguage.JAVA ->
+                        it.packageMember("recursiveFetchHashMap")
+                    else -> when (language) {
+                        CodeLanguage.JAVA -> it.packageMember("recursiveFetchHashMap")
+                        CodeLanguage.KOTLIN -> it.packageMember("recursiveFetchMap")
+                    }
                 }
             }
         when (language) {
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/relations.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/relations.kt
index 57c2e48..4daf31c 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/relations.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/relations.kt
@@ -1,21 +1,22 @@
-import android.database.Cursor
 import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
 import androidx.room.util.appendPlaceholders
 import androidx.room.util.getColumnIndex
 import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
-import androidx.room.util.recursiveFetchHashMap
-import java.util.ArrayList
-import java.util.HashMap
+import androidx.room.util.performBlocking
+import androidx.room.util.recursiveFetchMap
+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
 
@@ -31,130 +32,128 @@
 
   public override fun getSongsWithArtist(): SongWithArtist {
     val _sql: String = "SELECT * FROM Song"
-    val _statement: RoomSQLiteQuery = acquire(_sql, 0)
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, true, null)
-    try {
-      val _cursorIndexOfSongId: Int = getColumnIndexOrThrow(_cursor, "songId")
-      val _cursorIndexOfArtistKey: Int = getColumnIndexOrThrow(_cursor, "artistKey")
-      val _collectionArtist: HashMap<Long, Artist?> = HashMap<Long, Artist?>()
-      while (_cursor.moveToNext()) {
-        val _tmpKey: Long
-        _tmpKey = _cursor.getLong(_cursorIndexOfArtistKey)
-        _collectionArtist.put(_tmpKey, null)
-      }
-      _cursor.moveToPosition(-1)
-      __fetchRelationshipArtistAsArtist(_collectionArtist)
-      val _result: SongWithArtist
-      if (_cursor.moveToFirst()) {
-        val _tmpSong: Song
-        val _tmpSongId: Long
-        _tmpSongId = _cursor.getLong(_cursorIndexOfSongId)
-        val _tmpArtistKey: Long
-        _tmpArtistKey = _cursor.getLong(_cursorIndexOfArtistKey)
-        _tmpSong = Song(_tmpSongId,_tmpArtistKey)
-        val _tmpArtist: Artist?
-        val _tmpKey_1: Long
-        _tmpKey_1 = _cursor.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'.")
+    return performBlocking(__db, true, false) { _connection ->
+      val _stmt: SQLiteStatement = _connection.prepare(_sql)
+      try {
+        val _cursorIndexOfSongId: Int = getColumnIndexOrThrow(_stmt, "songId")
+        val _cursorIndexOfArtistKey: Int = getColumnIndexOrThrow(_stmt, "artistKey")
+        val _collectionArtist: MutableMap<Long, Artist?> = mutableMapOf()
+        while (_stmt.step()) {
+          val _tmpKey: Long
+          _tmpKey = _stmt.getLong(_cursorIndexOfArtistKey)
+          _collectionArtist.put(_tmpKey, null)
         }
-        _result = SongWithArtist(_tmpSong,_tmpArtist)
-      } else {
-        error("The query result was empty, but expected a single row to return a NON-NULL object of type <SongWithArtist>.")
+        _stmt.reset()
+        __fetchRelationshipArtistAsArtist(_connection, _collectionArtist)
+        val _result: SongWithArtist
+        if (_stmt.step()) {
+          val _tmpSong: Song
+          val _tmpSongId: Long
+          _tmpSongId = _stmt.getLong(_cursorIndexOfSongId)
+          val _tmpArtistKey: Long
+          _tmpArtistKey = _stmt.getLong(_cursorIndexOfArtistKey)
+          _tmpSong = Song(_tmpSongId,_tmpArtistKey)
+          val _tmpArtist: Artist?
+          val _tmpKey_1: Long
+          _tmpKey_1 = _stmt.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'.")
+          }
+          _result = SongWithArtist(_tmpSong,_tmpArtist)
+        } else {
+          error("The query result was empty, but expected a single row to return a NON-NULL object of type <SongWithArtist>.")
+        }
+        _result
+      } finally {
+        _stmt.close()
       }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
     }
   }
 
   public override fun getArtistAndSongs(): ArtistAndSongs {
     val _sql: String = "SELECT * FROM Artist"
-    val _statement: RoomSQLiteQuery = acquire(_sql, 0)
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, true, null)
-    try {
-      val _cursorIndexOfArtistId: Int = getColumnIndexOrThrow(_cursor, "artistId")
-      val _collectionSongs: HashMap<Long, ArrayList<Song>> = HashMap<Long, ArrayList<Song>>()
-      while (_cursor.moveToNext()) {
-        val _tmpKey: Long
-        _tmpKey = _cursor.getLong(_cursorIndexOfArtistId)
-        if (!_collectionSongs.containsKey(_tmpKey)) {
-          _collectionSongs.put(_tmpKey, ArrayList<Song>())
+    return performBlocking(__db, true, false) { _connection ->
+      val _stmt: SQLiteStatement = _connection.prepare(_sql)
+      try {
+        val _cursorIndexOfArtistId: Int = getColumnIndexOrThrow(_stmt, "artistId")
+        val _collectionSongs: MutableMap<Long, MutableList<Song>> = mutableMapOf()
+        while (_stmt.step()) {
+          val _tmpKey: Long
+          _tmpKey = _stmt.getLong(_cursorIndexOfArtistId)
+          if (!_collectionSongs.containsKey(_tmpKey)) {
+            _collectionSongs.put(_tmpKey, mutableListOf())
+          }
         }
+        _stmt.reset()
+        __fetchRelationshipSongAsSong(_connection, _collectionSongs)
+        val _result: ArtistAndSongs
+        if (_stmt.step()) {
+          val _tmpArtist: Artist
+          val _tmpArtistId: Long
+          _tmpArtistId = _stmt.getLong(_cursorIndexOfArtistId)
+          _tmpArtist = Artist(_tmpArtistId)
+          val _tmpSongsCollection: MutableList<Song>
+          val _tmpKey_1: Long
+          _tmpKey_1 = _stmt.getLong(_cursorIndexOfArtistId)
+          _tmpSongsCollection = _collectionSongs.getValue(_tmpKey_1)
+          _result = ArtistAndSongs(_tmpArtist,_tmpSongsCollection)
+        } else {
+          error("The query result was empty, but expected a single row to return a NON-NULL object of type <ArtistAndSongs>.")
+        }
+        _result
+      } finally {
+        _stmt.close()
       }
-      _cursor.moveToPosition(-1)
-      __fetchRelationshipSongAsSong(_collectionSongs)
-      val _result: ArtistAndSongs
-      if (_cursor.moveToFirst()) {
-        val _tmpArtist: Artist
-        val _tmpArtistId: Long
-        _tmpArtistId = _cursor.getLong(_cursorIndexOfArtistId)
-        _tmpArtist = Artist(_tmpArtistId)
-        val _tmpSongsCollection: ArrayList<Song>
-        val _tmpKey_1: Long
-        _tmpKey_1 = _cursor.getLong(_cursorIndexOfArtistId)
-        _tmpSongsCollection = _collectionSongs.getValue(_tmpKey_1)
-        _result = ArtistAndSongs(_tmpArtist,_tmpSongsCollection)
-      } else {
-        error("The query result was empty, but expected a single row to return a NON-NULL object of type <ArtistAndSongs>.")
-      }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
     }
   }
 
   public override fun getPlaylistAndSongs(): PlaylistAndSongs {
     val _sql: String = "SELECT * FROM Playlist"
-    val _statement: RoomSQLiteQuery = acquire(_sql, 0)
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, true, null)
-    try {
-      val _cursorIndexOfPlaylistId: Int = getColumnIndexOrThrow(_cursor, "playlistId")
-      val _collectionSongs: HashMap<Long, ArrayList<Song>> = HashMap<Long, ArrayList<Song>>()
-      while (_cursor.moveToNext()) {
-        val _tmpKey: Long
-        _tmpKey = _cursor.getLong(_cursorIndexOfPlaylistId)
-        if (!_collectionSongs.containsKey(_tmpKey)) {
-          _collectionSongs.put(_tmpKey, ArrayList<Song>())
+    return performBlocking(__db, true, false) { _connection ->
+      val _stmt: SQLiteStatement = _connection.prepare(_sql)
+      try {
+        val _cursorIndexOfPlaylistId: Int = getColumnIndexOrThrow(_stmt, "playlistId")
+        val _collectionSongs: MutableMap<Long, MutableList<Song>> = mutableMapOf()
+        while (_stmt.step()) {
+          val _tmpKey: Long
+          _tmpKey = _stmt.getLong(_cursorIndexOfPlaylistId)
+          if (!_collectionSongs.containsKey(_tmpKey)) {
+            _collectionSongs.put(_tmpKey, mutableListOf())
+          }
         }
+        _stmt.reset()
+        __fetchRelationshipSongAsSong_1(_connection, _collectionSongs)
+        val _result: PlaylistAndSongs
+        if (_stmt.step()) {
+          val _tmpPlaylist: Playlist
+          val _tmpPlaylistId: Long
+          _tmpPlaylistId = _stmt.getLong(_cursorIndexOfPlaylistId)
+          _tmpPlaylist = Playlist(_tmpPlaylistId)
+          val _tmpSongsCollection: MutableList<Song>
+          val _tmpKey_1: Long
+          _tmpKey_1 = _stmt.getLong(_cursorIndexOfPlaylistId)
+          _tmpSongsCollection = _collectionSongs.getValue(_tmpKey_1)
+          _result = PlaylistAndSongs(_tmpPlaylist,_tmpSongsCollection)
+        } else {
+          error("The query result was empty, but expected a single row to return a NON-NULL object of type <PlaylistAndSongs>.")
+        }
+        _result
+      } finally {
+        _stmt.close()
       }
-      _cursor.moveToPosition(-1)
-      __fetchRelationshipSongAsSong_1(_collectionSongs)
-      val _result: PlaylistAndSongs
-      if (_cursor.moveToFirst()) {
-        val _tmpPlaylist: Playlist
-        val _tmpPlaylistId: Long
-        _tmpPlaylistId = _cursor.getLong(_cursorIndexOfPlaylistId)
-        _tmpPlaylist = Playlist(_tmpPlaylistId)
-        val _tmpSongsCollection: ArrayList<Song>
-        val _tmpKey_1: Long
-        _tmpKey_1 = _cursor.getLong(_cursorIndexOfPlaylistId)
-        _tmpSongsCollection = _collectionSongs.getValue(_tmpKey_1)
-        _result = PlaylistAndSongs(_tmpPlaylist,_tmpSongsCollection)
-      } else {
-        error("The query result was empty, but expected a single row to return a NON-NULL object of type <PlaylistAndSongs>.")
-      }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
     }
   }
 
-  private fun __fetchRelationshipArtistAsArtist(_map: HashMap<Long, Artist?>) {
+  private fun __fetchRelationshipArtistAsArtist(_connection: SQLiteConnection,
+      _map: MutableMap<Long, Artist?>) {
     val __mapKeySet: Set<Long> = _map.keys
     if (__mapKeySet.isEmpty()) {
       return
     }
-    if (_map.size > RoomDatabase.MAX_BIND_PARAMETER_CNT) {
-      recursiveFetchHashMap(_map, false) {
-        __fetchRelationshipArtistAsArtist(it)
+    if (_map.size > 999) {
+      recursiveFetchMap(_map, false) {
+        __fetchRelationshipArtistAsArtist(_connection, it)
       }
       return
     }
@@ -164,44 +163,43 @@
     appendPlaceholders(_stringBuilder, _inputSize)
     _stringBuilder.append(")")
     val _sql: String = _stringBuilder.toString()
-    val _argCount: Int = 0 + _inputSize
-    val _stmt: RoomSQLiteQuery = acquire(_sql, _argCount)
+    val _stmt: SQLiteStatement = _connection.prepare(_sql)
     var _argIndex: Int = 1
     for (_item: Long in __mapKeySet) {
       _stmt.bindLong(_argIndex, _item)
       _argIndex++
     }
-    val _cursor: Cursor = query(__db, _stmt, false, null)
     try {
-      val _itemKeyIndex: Int = getColumnIndex(_cursor, "artistId")
+      val _itemKeyIndex: Int = getColumnIndex(_stmt, "artistId")
       if (_itemKeyIndex == -1) {
         return
       }
       val _cursorIndexOfArtistId: Int = 0
-      while (_cursor.moveToNext()) {
+      while (_stmt.step()) {
         val _tmpKey: Long
-        _tmpKey = _cursor.getLong(_itemKeyIndex)
+        _tmpKey = _stmt.getLong(_itemKeyIndex)
         if (_map.containsKey(_tmpKey)) {
           val _item_1: Artist
           val _tmpArtistId: Long
-          _tmpArtistId = _cursor.getLong(_cursorIndexOfArtistId)
+          _tmpArtistId = _stmt.getLong(_cursorIndexOfArtistId)
           _item_1 = Artist(_tmpArtistId)
           _map.put(_tmpKey, _item_1)
         }
       }
     } finally {
-      _cursor.close()
+      _stmt.close()
     }
   }
 
-  private fun __fetchRelationshipSongAsSong(_map: HashMap<Long, ArrayList<Song>>) {
+  private fun __fetchRelationshipSongAsSong(_connection: SQLiteConnection,
+      _map: MutableMap<Long, MutableList<Song>>) {
     val __mapKeySet: Set<Long> = _map.keys
     if (__mapKeySet.isEmpty()) {
       return
     }
-    if (_map.size > RoomDatabase.MAX_BIND_PARAMETER_CNT) {
-      recursiveFetchHashMap(_map, true) {
-        __fetchRelationshipSongAsSong(it)
+    if (_map.size > 999) {
+      recursiveFetchMap(_map, true) {
+        __fetchRelationshipSongAsSong(_connection, it)
       }
       return
     }
@@ -211,48 +209,47 @@
     appendPlaceholders(_stringBuilder, _inputSize)
     _stringBuilder.append(")")
     val _sql: String = _stringBuilder.toString()
-    val _argCount: Int = 0 + _inputSize
-    val _stmt: RoomSQLiteQuery = acquire(_sql, _argCount)
+    val _stmt: SQLiteStatement = _connection.prepare(_sql)
     var _argIndex: Int = 1
     for (_item: Long in __mapKeySet) {
       _stmt.bindLong(_argIndex, _item)
       _argIndex++
     }
-    val _cursor: Cursor = query(__db, _stmt, false, null)
     try {
-      val _itemKeyIndex: Int = getColumnIndex(_cursor, "artistKey")
+      val _itemKeyIndex: Int = getColumnIndex(_stmt, "artistKey")
       if (_itemKeyIndex == -1) {
         return
       }
       val _cursorIndexOfSongId: Int = 0
       val _cursorIndexOfArtistKey: Int = 1
-      while (_cursor.moveToNext()) {
+      while (_stmt.step()) {
         val _tmpKey: Long
-        _tmpKey = _cursor.getLong(_itemKeyIndex)
-        val _tmpRelation: ArrayList<Song>? = _map.get(_tmpKey)
+        _tmpKey = _stmt.getLong(_itemKeyIndex)
+        val _tmpRelation: MutableList<Song>? = _map.get(_tmpKey)
         if (_tmpRelation != null) {
           val _item_1: Song
           val _tmpSongId: Long
-          _tmpSongId = _cursor.getLong(_cursorIndexOfSongId)
+          _tmpSongId = _stmt.getLong(_cursorIndexOfSongId)
           val _tmpArtistKey: Long
-          _tmpArtistKey = _cursor.getLong(_cursorIndexOfArtistKey)
+          _tmpArtistKey = _stmt.getLong(_cursorIndexOfArtistKey)
           _item_1 = Song(_tmpSongId,_tmpArtistKey)
           _tmpRelation.add(_item_1)
         }
       }
     } finally {
-      _cursor.close()
+      _stmt.close()
     }
   }
 
-  private fun __fetchRelationshipSongAsSong_1(_map: HashMap<Long, ArrayList<Song>>) {
+  private fun __fetchRelationshipSongAsSong_1(_connection: SQLiteConnection,
+      _map: MutableMap<Long, MutableList<Song>>) {
     val __mapKeySet: Set<Long> = _map.keys
     if (__mapKeySet.isEmpty()) {
       return
     }
-    if (_map.size > RoomDatabase.MAX_BIND_PARAMETER_CNT) {
-      recursiveFetchHashMap(_map, true) {
-        __fetchRelationshipSongAsSong_1(it)
+    if (_map.size > 999) {
+      recursiveFetchMap(_map, true) {
+        __fetchRelationshipSongAsSong_1(_connection, it)
       }
       return
     }
@@ -262,14 +259,12 @@
     appendPlaceholders(_stringBuilder, _inputSize)
     _stringBuilder.append(")")
     val _sql: String = _stringBuilder.toString()
-    val _argCount: Int = 0 + _inputSize
-    val _stmt: RoomSQLiteQuery = acquire(_sql, _argCount)
+    val _stmt: SQLiteStatement = _connection.prepare(_sql)
     var _argIndex: Int = 1
     for (_item: Long in __mapKeySet) {
       _stmt.bindLong(_argIndex, _item)
       _argIndex++
     }
-    val _cursor: Cursor = query(__db, _stmt, false, null)
     try {
       // _junction.playlistKey
       val _itemKeyIndex: Int = 2
@@ -278,22 +273,22 @@
       }
       val _cursorIndexOfSongId: Int = 0
       val _cursorIndexOfArtistKey: Int = 1
-      while (_cursor.moveToNext()) {
+      while (_stmt.step()) {
         val _tmpKey: Long
-        _tmpKey = _cursor.getLong(_itemKeyIndex)
-        val _tmpRelation: ArrayList<Song>? = _map.get(_tmpKey)
+        _tmpKey = _stmt.getLong(_itemKeyIndex)
+        val _tmpRelation: MutableList<Song>? = _map.get(_tmpKey)
         if (_tmpRelation != null) {
           val _item_1: Song
           val _tmpSongId: Long
-          _tmpSongId = _cursor.getLong(_cursorIndexOfSongId)
+          _tmpSongId = _stmt.getLong(_cursorIndexOfSongId)
           val _tmpArtistKey: Long
-          _tmpArtistKey = _cursor.getLong(_cursorIndexOfArtistKey)
+          _tmpArtistKey = _stmt.getLong(_cursorIndexOfArtistKey)
           _item_1 = Song(_tmpSongId,_tmpArtistKey)
           _tmpRelation.add(_item_1)
         }
       }
     } finally {
-      _cursor.close()
+      _stmt.close()
     }
   }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/relations_arrayMap.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/relations_arrayMap.kt
index 0c5dcc4..4b43e32 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/relations_arrayMap.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/relations_arrayMap.kt
@@ -1,21 +1,21 @@
-import android.database.Cursor
 import androidx.collection.ArrayMap
 import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
 import androidx.room.util.appendPlaceholders
 import androidx.room.util.getColumnIndex
 import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performBlocking
 import androidx.room.util.recursiveFetchArrayMap
-import java.util.ArrayList
+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.Set
+import kotlin.collections.mutableListOf
 import kotlin.reflect.KClass
 import kotlin.text.StringBuilder
 
@@ -31,130 +31,130 @@
 
   public override fun getSongsWithArtist(): SongWithArtist {
     val _sql: String = "SELECT * FROM Song"
-    val _statement: RoomSQLiteQuery = acquire(_sql, 0)
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, true, null)
-    try {
-      val _cursorIndexOfSongId: Int = getColumnIndexOrThrow(_cursor, "songId")
-      val _cursorIndexOfArtistKey: Int = getColumnIndexOrThrow(_cursor, "artistKey")
-      val _collectionArtist: ArrayMap<Long, Artist?> = ArrayMap<Long, Artist?>()
-      while (_cursor.moveToNext()) {
-        val _tmpKey: Long
-        _tmpKey = _cursor.getLong(_cursorIndexOfArtistKey)
-        _collectionArtist.put(_tmpKey, null)
-      }
-      _cursor.moveToPosition(-1)
-      __fetchRelationshipArtistAsArtist(_collectionArtist)
-      val _result: SongWithArtist
-      if (_cursor.moveToFirst()) {
-        val _tmpSong: Song
-        val _tmpSongId: Long
-        _tmpSongId = _cursor.getLong(_cursorIndexOfSongId)
-        val _tmpArtistKey: Long
-        _tmpArtistKey = _cursor.getLong(_cursorIndexOfArtistKey)
-        _tmpSong = Song(_tmpSongId,_tmpArtistKey)
-        val _tmpArtist: Artist?
-        val _tmpKey_1: Long
-        _tmpKey_1 = _cursor.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'.")
+    return performBlocking(__db, true, false) { _connection ->
+      val _stmt: SQLiteStatement = _connection.prepare(_sql)
+      try {
+        val _cursorIndexOfSongId: Int = getColumnIndexOrThrow(_stmt, "songId")
+        val _cursorIndexOfArtistKey: Int = getColumnIndexOrThrow(_stmt, "artistKey")
+        val _collectionArtist: ArrayMap<Long, Artist?> = ArrayMap<Long, Artist?>()
+        while (_stmt.step()) {
+          val _tmpKey: Long
+          _tmpKey = _stmt.getLong(_cursorIndexOfArtistKey)
+          _collectionArtist.put(_tmpKey, null)
         }
-        _result = SongWithArtist(_tmpSong,_tmpArtist)
-      } else {
-        error("The query result was empty, but expected a single row to return a NON-NULL object of type <SongWithArtist>.")
+        _stmt.reset()
+        __fetchRelationshipArtistAsArtist(_connection, _collectionArtist)
+        val _result: SongWithArtist
+        if (_stmt.step()) {
+          val _tmpSong: Song
+          val _tmpSongId: Long
+          _tmpSongId = _stmt.getLong(_cursorIndexOfSongId)
+          val _tmpArtistKey: Long
+          _tmpArtistKey = _stmt.getLong(_cursorIndexOfArtistKey)
+          _tmpSong = Song(_tmpSongId,_tmpArtistKey)
+          val _tmpArtist: Artist?
+          val _tmpKey_1: Long
+          _tmpKey_1 = _stmt.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'.")
+          }
+          _result = SongWithArtist(_tmpSong,_tmpArtist)
+        } else {
+          error("The query result was empty, but expected a single row to return a NON-NULL object of type <SongWithArtist>.")
+        }
+        _result
+      } finally {
+        _stmt.close()
       }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
     }
   }
 
   public override fun getArtistAndSongs(): ArtistAndSongs {
     val _sql: String = "SELECT * FROM Artist"
-    val _statement: RoomSQLiteQuery = acquire(_sql, 0)
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, true, null)
-    try {
-      val _cursorIndexOfArtistId: Int = getColumnIndexOrThrow(_cursor, "artistId")
-      val _collectionSongs: ArrayMap<Long, ArrayList<Song>> = ArrayMap<Long, ArrayList<Song>>()
-      while (_cursor.moveToNext()) {
-        val _tmpKey: Long
-        _tmpKey = _cursor.getLong(_cursorIndexOfArtistId)
-        if (!_collectionSongs.containsKey(_tmpKey)) {
-          _collectionSongs.put(_tmpKey, ArrayList<Song>())
+    return performBlocking(__db, true, false) { _connection ->
+      val _stmt: SQLiteStatement = _connection.prepare(_sql)
+      try {
+        val _cursorIndexOfArtistId: Int = getColumnIndexOrThrow(_stmt, "artistId")
+        val _collectionSongs: ArrayMap<Long, MutableList<Song>> =
+            ArrayMap<Long, MutableList<Song>>()
+        while (_stmt.step()) {
+          val _tmpKey: Long
+          _tmpKey = _stmt.getLong(_cursorIndexOfArtistId)
+          if (!_collectionSongs.containsKey(_tmpKey)) {
+            _collectionSongs.put(_tmpKey, mutableListOf())
+          }
         }
+        _stmt.reset()
+        __fetchRelationshipSongAsSong(_connection, _collectionSongs)
+        val _result: ArtistAndSongs
+        if (_stmt.step()) {
+          val _tmpArtist: Artist
+          val _tmpArtistId: Long
+          _tmpArtistId = _stmt.getLong(_cursorIndexOfArtistId)
+          _tmpArtist = Artist(_tmpArtistId)
+          val _tmpSongsCollection: MutableList<Song>
+          val _tmpKey_1: Long
+          _tmpKey_1 = _stmt.getLong(_cursorIndexOfArtistId)
+          _tmpSongsCollection = _collectionSongs.getValue(_tmpKey_1)
+          _result = ArtistAndSongs(_tmpArtist,_tmpSongsCollection)
+        } else {
+          error("The query result was empty, but expected a single row to return a NON-NULL object of type <ArtistAndSongs>.")
+        }
+        _result
+      } finally {
+        _stmt.close()
       }
-      _cursor.moveToPosition(-1)
-      __fetchRelationshipSongAsSong(_collectionSongs)
-      val _result: ArtistAndSongs
-      if (_cursor.moveToFirst()) {
-        val _tmpArtist: Artist
-        val _tmpArtistId: Long
-        _tmpArtistId = _cursor.getLong(_cursorIndexOfArtistId)
-        _tmpArtist = Artist(_tmpArtistId)
-        val _tmpSongsCollection: ArrayList<Song>
-        val _tmpKey_1: Long
-        _tmpKey_1 = _cursor.getLong(_cursorIndexOfArtistId)
-        _tmpSongsCollection = _collectionSongs.getValue(_tmpKey_1)
-        _result = ArtistAndSongs(_tmpArtist,_tmpSongsCollection)
-      } else {
-        error("The query result was empty, but expected a single row to return a NON-NULL object of type <ArtistAndSongs>.")
-      }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
     }
   }
 
   public override fun getPlaylistAndSongs(): PlaylistAndSongs {
     val _sql: String = "SELECT * FROM Playlist"
-    val _statement: RoomSQLiteQuery = acquire(_sql, 0)
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, true, null)
-    try {
-      val _cursorIndexOfPlaylistId: Int = getColumnIndexOrThrow(_cursor, "playlistId")
-      val _collectionSongs: ArrayMap<Long, ArrayList<Song>> = ArrayMap<Long, ArrayList<Song>>()
-      while (_cursor.moveToNext()) {
-        val _tmpKey: Long
-        _tmpKey = _cursor.getLong(_cursorIndexOfPlaylistId)
-        if (!_collectionSongs.containsKey(_tmpKey)) {
-          _collectionSongs.put(_tmpKey, ArrayList<Song>())
+    return performBlocking(__db, true, false) { _connection ->
+      val _stmt: SQLiteStatement = _connection.prepare(_sql)
+      try {
+        val _cursorIndexOfPlaylistId: Int = getColumnIndexOrThrow(_stmt, "playlistId")
+        val _collectionSongs: ArrayMap<Long, MutableList<Song>> =
+            ArrayMap<Long, MutableList<Song>>()
+        while (_stmt.step()) {
+          val _tmpKey: Long
+          _tmpKey = _stmt.getLong(_cursorIndexOfPlaylistId)
+          if (!_collectionSongs.containsKey(_tmpKey)) {
+            _collectionSongs.put(_tmpKey, mutableListOf())
+          }
         }
+        _stmt.reset()
+        __fetchRelationshipSongAsSong_1(_connection, _collectionSongs)
+        val _result: PlaylistAndSongs
+        if (_stmt.step()) {
+          val _tmpPlaylist: Playlist
+          val _tmpPlaylistId: Long
+          _tmpPlaylistId = _stmt.getLong(_cursorIndexOfPlaylistId)
+          _tmpPlaylist = Playlist(_tmpPlaylistId)
+          val _tmpSongsCollection: MutableList<Song>
+          val _tmpKey_1: Long
+          _tmpKey_1 = _stmt.getLong(_cursorIndexOfPlaylistId)
+          _tmpSongsCollection = _collectionSongs.getValue(_tmpKey_1)
+          _result = PlaylistAndSongs(_tmpPlaylist,_tmpSongsCollection)
+        } else {
+          error("The query result was empty, but expected a single row to return a NON-NULL object of type <PlaylistAndSongs>.")
+        }
+        _result
+      } finally {
+        _stmt.close()
       }
-      _cursor.moveToPosition(-1)
-      __fetchRelationshipSongAsSong_1(_collectionSongs)
-      val _result: PlaylistAndSongs
-      if (_cursor.moveToFirst()) {
-        val _tmpPlaylist: Playlist
-        val _tmpPlaylistId: Long
-        _tmpPlaylistId = _cursor.getLong(_cursorIndexOfPlaylistId)
-        _tmpPlaylist = Playlist(_tmpPlaylistId)
-        val _tmpSongsCollection: ArrayList<Song>
-        val _tmpKey_1: Long
-        _tmpKey_1 = _cursor.getLong(_cursorIndexOfPlaylistId)
-        _tmpSongsCollection = _collectionSongs.getValue(_tmpKey_1)
-        _result = PlaylistAndSongs(_tmpPlaylist,_tmpSongsCollection)
-      } else {
-        error("The query result was empty, but expected a single row to return a NON-NULL object of type <PlaylistAndSongs>.")
-      }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
     }
   }
 
-  private fun __fetchRelationshipArtistAsArtist(_map: ArrayMap<Long, Artist?>) {
+  private fun __fetchRelationshipArtistAsArtist(_connection: SQLiteConnection,
+      _map: ArrayMap<Long, Artist?>) {
     val __mapKeySet: Set<Long> = _map.keys
     if (__mapKeySet.isEmpty()) {
       return
     }
-    if (_map.size > RoomDatabase.MAX_BIND_PARAMETER_CNT) {
+    if (_map.size > 999) {
       recursiveFetchArrayMap(_map, false) {
-        __fetchRelationshipArtistAsArtist(it)
+        __fetchRelationshipArtistAsArtist(_connection, it)
       }
       return
     }
@@ -164,44 +164,43 @@
     appendPlaceholders(_stringBuilder, _inputSize)
     _stringBuilder.append(")")
     val _sql: String = _stringBuilder.toString()
-    val _argCount: Int = 0 + _inputSize
-    val _stmt: RoomSQLiteQuery = acquire(_sql, _argCount)
+    val _stmt: SQLiteStatement = _connection.prepare(_sql)
     var _argIndex: Int = 1
     for (_item: Long in __mapKeySet) {
       _stmt.bindLong(_argIndex, _item)
       _argIndex++
     }
-    val _cursor: Cursor = query(__db, _stmt, false, null)
     try {
-      val _itemKeyIndex: Int = getColumnIndex(_cursor, "artistId")
+      val _itemKeyIndex: Int = getColumnIndex(_stmt, "artistId")
       if (_itemKeyIndex == -1) {
         return
       }
       val _cursorIndexOfArtistId: Int = 0
-      while (_cursor.moveToNext()) {
+      while (_stmt.step()) {
         val _tmpKey: Long
-        _tmpKey = _cursor.getLong(_itemKeyIndex)
+        _tmpKey = _stmt.getLong(_itemKeyIndex)
         if (_map.containsKey(_tmpKey)) {
           val _item_1: Artist
           val _tmpArtistId: Long
-          _tmpArtistId = _cursor.getLong(_cursorIndexOfArtistId)
+          _tmpArtistId = _stmt.getLong(_cursorIndexOfArtistId)
           _item_1 = Artist(_tmpArtistId)
           _map.put(_tmpKey, _item_1)
         }
       }
     } finally {
-      _cursor.close()
+      _stmt.close()
     }
   }
 
-  private fun __fetchRelationshipSongAsSong(_map: ArrayMap<Long, ArrayList<Song>>) {
+  private fun __fetchRelationshipSongAsSong(_connection: SQLiteConnection,
+      _map: ArrayMap<Long, MutableList<Song>>) {
     val __mapKeySet: Set<Long> = _map.keys
     if (__mapKeySet.isEmpty()) {
       return
     }
-    if (_map.size > RoomDatabase.MAX_BIND_PARAMETER_CNT) {
+    if (_map.size > 999) {
       recursiveFetchArrayMap(_map, true) {
-        __fetchRelationshipSongAsSong(it)
+        __fetchRelationshipSongAsSong(_connection, it)
       }
       return
     }
@@ -211,48 +210,47 @@
     appendPlaceholders(_stringBuilder, _inputSize)
     _stringBuilder.append(")")
     val _sql: String = _stringBuilder.toString()
-    val _argCount: Int = 0 + _inputSize
-    val _stmt: RoomSQLiteQuery = acquire(_sql, _argCount)
+    val _stmt: SQLiteStatement = _connection.prepare(_sql)
     var _argIndex: Int = 1
     for (_item: Long in __mapKeySet) {
       _stmt.bindLong(_argIndex, _item)
       _argIndex++
     }
-    val _cursor: Cursor = query(__db, _stmt, false, null)
     try {
-      val _itemKeyIndex: Int = getColumnIndex(_cursor, "artistKey")
+      val _itemKeyIndex: Int = getColumnIndex(_stmt, "artistKey")
       if (_itemKeyIndex == -1) {
         return
       }
       val _cursorIndexOfSongId: Int = 0
       val _cursorIndexOfArtistKey: Int = 1
-      while (_cursor.moveToNext()) {
+      while (_stmt.step()) {
         val _tmpKey: Long
-        _tmpKey = _cursor.getLong(_itemKeyIndex)
-        val _tmpRelation: ArrayList<Song>? = _map.get(_tmpKey)
+        _tmpKey = _stmt.getLong(_itemKeyIndex)
+        val _tmpRelation: MutableList<Song>? = _map.get(_tmpKey)
         if (_tmpRelation != null) {
           val _item_1: Song
           val _tmpSongId: Long
-          _tmpSongId = _cursor.getLong(_cursorIndexOfSongId)
+          _tmpSongId = _stmt.getLong(_cursorIndexOfSongId)
           val _tmpArtistKey: Long
-          _tmpArtistKey = _cursor.getLong(_cursorIndexOfArtistKey)
+          _tmpArtistKey = _stmt.getLong(_cursorIndexOfArtistKey)
           _item_1 = Song(_tmpSongId,_tmpArtistKey)
           _tmpRelation.add(_item_1)
         }
       }
     } finally {
-      _cursor.close()
+      _stmt.close()
     }
   }
 
-  private fun __fetchRelationshipSongAsSong_1(_map: ArrayMap<Long, ArrayList<Song>>) {
+  private fun __fetchRelationshipSongAsSong_1(_connection: SQLiteConnection,
+      _map: ArrayMap<Long, MutableList<Song>>) {
     val __mapKeySet: Set<Long> = _map.keys
     if (__mapKeySet.isEmpty()) {
       return
     }
-    if (_map.size > RoomDatabase.MAX_BIND_PARAMETER_CNT) {
+    if (_map.size > 999) {
       recursiveFetchArrayMap(_map, true) {
-        __fetchRelationshipSongAsSong_1(it)
+        __fetchRelationshipSongAsSong_1(_connection, it)
       }
       return
     }
@@ -262,14 +260,12 @@
     appendPlaceholders(_stringBuilder, _inputSize)
     _stringBuilder.append(")")
     val _sql: String = _stringBuilder.toString()
-    val _argCount: Int = 0 + _inputSize
-    val _stmt: RoomSQLiteQuery = acquire(_sql, _argCount)
+    val _stmt: SQLiteStatement = _connection.prepare(_sql)
     var _argIndex: Int = 1
     for (_item: Long in __mapKeySet) {
       _stmt.bindLong(_argIndex, _item)
       _argIndex++
     }
-    val _cursor: Cursor = query(__db, _stmt, false, null)
     try {
       // _junction.playlistKey
       val _itemKeyIndex: Int = 2
@@ -278,22 +274,22 @@
       }
       val _cursorIndexOfSongId: Int = 0
       val _cursorIndexOfArtistKey: Int = 1
-      while (_cursor.moveToNext()) {
+      while (_stmt.step()) {
         val _tmpKey: Long
-        _tmpKey = _cursor.getLong(_itemKeyIndex)
-        val _tmpRelation: ArrayList<Song>? = _map.get(_tmpKey)
+        _tmpKey = _stmt.getLong(_itemKeyIndex)
+        val _tmpRelation: MutableList<Song>? = _map.get(_tmpKey)
         if (_tmpRelation != null) {
           val _item_1: Song
           val _tmpSongId: Long
-          _tmpSongId = _cursor.getLong(_cursorIndexOfSongId)
+          _tmpSongId = _stmt.getLong(_cursorIndexOfSongId)
           val _tmpArtistKey: Long
-          _tmpArtistKey = _cursor.getLong(_cursorIndexOfArtistKey)
+          _tmpArtistKey = _stmt.getLong(_cursorIndexOfArtistKey)
           _item_1 = Song(_tmpSongId,_tmpArtistKey)
           _tmpRelation.add(_item_1)
         }
       }
     } finally {
-      _cursor.close()
+      _stmt.close()
     }
   }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/relations_byteBufferKey.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/relations_byteBufferKey.kt
index f6480e9c..136a991 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/relations_byteBufferKey.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/relations_byteBufferKey.kt
@@ -1,14 +1,12 @@
-import android.database.Cursor
 import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
 import androidx.room.util.appendPlaceholders
 import androidx.room.util.getColumnIndex
 import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
-import androidx.room.util.recursiveFetchHashMap
+import androidx.room.util.performBlocking
+import androidx.room.util.recursiveFetchMap
+import androidx.sqlite.SQLiteConnection
+import androidx.sqlite.SQLiteStatement
 import java.nio.ByteBuffer
-import java.util.HashMap
 import javax.`annotation`.processing.Generated
 import kotlin.ByteArray
 import kotlin.Int
@@ -16,7 +14,9 @@
 import kotlin.String
 import kotlin.Suppress
 import kotlin.collections.List
+import kotlin.collections.MutableMap
 import kotlin.collections.Set
+import kotlin.collections.mutableMapOf
 import kotlin.reflect.KClass
 import kotlin.text.StringBuilder
 
@@ -32,54 +32,54 @@
 
   public override fun getSongsWithArtist(): SongWithArtist {
     val _sql: String = "SELECT * FROM Song"
-    val _statement: RoomSQLiteQuery = acquire(_sql, 0)
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, true, null)
-    try {
-      val _cursorIndexOfSongId: Int = getColumnIndexOrThrow(_cursor, "songId")
-      val _cursorIndexOfArtistKey: Int = getColumnIndexOrThrow(_cursor, "artistKey")
-      val _collectionArtist: HashMap<ByteBuffer, Artist?> = HashMap<ByteBuffer, Artist?>()
-      while (_cursor.moveToNext()) {
-        val _tmpKey: ByteBuffer
-        _tmpKey = ByteBuffer.wrap(_cursor.getBlob(_cursorIndexOfArtistKey))
-        _collectionArtist.put(_tmpKey, null)
-      }
-      _cursor.moveToPosition(-1)
-      __fetchRelationshipArtistAsArtist(_collectionArtist)
-      val _result: SongWithArtist
-      if (_cursor.moveToFirst()) {
-        val _tmpSong: Song
-        val _tmpSongId: Long
-        _tmpSongId = _cursor.getLong(_cursorIndexOfSongId)
-        val _tmpArtistKey: ByteArray
-        _tmpArtistKey = _cursor.getBlob(_cursorIndexOfArtistKey)
-        _tmpSong = Song(_tmpSongId,_tmpArtistKey)
-        val _tmpArtist: Artist?
-        val _tmpKey_1: ByteBuffer
-        _tmpKey_1 = ByteBuffer.wrap(_cursor.getBlob(_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'.")
+    return performBlocking(__db, true, false) { _connection ->
+      val _stmt: SQLiteStatement = _connection.prepare(_sql)
+      try {
+        val _cursorIndexOfSongId: Int = getColumnIndexOrThrow(_stmt, "songId")
+        val _cursorIndexOfArtistKey: Int = getColumnIndexOrThrow(_stmt, "artistKey")
+        val _collectionArtist: MutableMap<ByteBuffer, Artist?> = mutableMapOf()
+        while (_stmt.step()) {
+          val _tmpKey: ByteBuffer
+          _tmpKey = ByteBuffer.wrap(_stmt.getBlob(_cursorIndexOfArtistKey))
+          _collectionArtist.put(_tmpKey, null)
         }
-        _result = SongWithArtist(_tmpSong,_tmpArtist)
-      } else {
-        error("The query result was empty, but expected a single row to return a NON-NULL object of type <SongWithArtist>.")
+        _stmt.reset()
+        __fetchRelationshipArtistAsArtist(_connection, _collectionArtist)
+        val _result: SongWithArtist
+        if (_stmt.step()) {
+          val _tmpSong: Song
+          val _tmpSongId: Long
+          _tmpSongId = _stmt.getLong(_cursorIndexOfSongId)
+          val _tmpArtistKey: ByteArray
+          _tmpArtistKey = _stmt.getBlob(_cursorIndexOfArtistKey)
+          _tmpSong = Song(_tmpSongId,_tmpArtistKey)
+          val _tmpArtist: Artist?
+          val _tmpKey_1: ByteBuffer
+          _tmpKey_1 = ByteBuffer.wrap(_stmt.getBlob(_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'.")
+          }
+          _result = SongWithArtist(_tmpSong,_tmpArtist)
+        } else {
+          error("The query result was empty, but expected a single row to return a NON-NULL object of type <SongWithArtist>.")
+        }
+        _result
+      } finally {
+        _stmt.close()
       }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
     }
   }
 
-  private fun __fetchRelationshipArtistAsArtist(_map: HashMap<ByteBuffer, Artist?>) {
+  private fun __fetchRelationshipArtistAsArtist(_connection: SQLiteConnection,
+      _map: MutableMap<ByteBuffer, Artist?>) {
     val __mapKeySet: Set<ByteBuffer> = _map.keys
     if (__mapKeySet.isEmpty()) {
       return
     }
-    if (_map.size > RoomDatabase.MAX_BIND_PARAMETER_CNT) {
-      recursiveFetchHashMap(_map, false) {
-        __fetchRelationshipArtistAsArtist(it)
+    if (_map.size > 999) {
+      recursiveFetchMap(_map, false) {
+        __fetchRelationshipArtistAsArtist(_connection, it)
       }
       return
     }
@@ -89,33 +89,31 @@
     appendPlaceholders(_stringBuilder, _inputSize)
     _stringBuilder.append(")")
     val _sql: String = _stringBuilder.toString()
-    val _argCount: Int = 0 + _inputSize
-    val _stmt: RoomSQLiteQuery = acquire(_sql, _argCount)
+    val _stmt: SQLiteStatement = _connection.prepare(_sql)
     var _argIndex: Int = 1
     for (_item: ByteBuffer in __mapKeySet) {
       _stmt.bindBlob(_argIndex, _item.array())
       _argIndex++
     }
-    val _cursor: Cursor = query(__db, _stmt, false, null)
     try {
-      val _itemKeyIndex: Int = getColumnIndex(_cursor, "artistId")
+      val _itemKeyIndex: Int = getColumnIndex(_stmt, "artistId")
       if (_itemKeyIndex == -1) {
         return
       }
       val _cursorIndexOfArtistId: Int = 0
-      while (_cursor.moveToNext()) {
+      while (_stmt.step()) {
         val _tmpKey: ByteBuffer
-        _tmpKey = ByteBuffer.wrap(_cursor.getBlob(_itemKeyIndex))
+        _tmpKey = ByteBuffer.wrap(_stmt.getBlob(_itemKeyIndex))
         if (_map.containsKey(_tmpKey)) {
           val _item_1: Artist
           val _tmpArtistId: ByteArray
-          _tmpArtistId = _cursor.getBlob(_cursorIndexOfArtistId)
+          _tmpArtistId = _stmt.getBlob(_cursorIndexOfArtistId)
           _item_1 = Artist(_tmpArtistId)
           _map.put(_tmpKey, _item_1)
         }
       }
     } finally {
-      _cursor.close()
+      _stmt.close()
     }
   }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/relations_longSparseArray.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/relations_longSparseArray.kt
index 2c0afeb..5760d95 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/relations_longSparseArray.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/relations_longSparseArray.kt
@@ -1,20 +1,20 @@
-import android.database.Cursor
 import androidx.collection.LongSparseArray
 import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
 import androidx.room.util.appendPlaceholders
 import androidx.room.util.getColumnIndex
 import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performBlocking
 import androidx.room.util.recursiveFetchLongSparseArray
-import java.util.ArrayList
+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.mutableListOf
 import kotlin.reflect.KClass
 import kotlin.text.StringBuilder
 
@@ -30,129 +30,129 @@
 
   public override fun getSongsWithArtist(): SongWithArtist {
     val _sql: String = "SELECT * FROM Song"
-    val _statement: RoomSQLiteQuery = acquire(_sql, 0)
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, true, null)
-    try {
-      val _cursorIndexOfSongId: Int = getColumnIndexOrThrow(_cursor, "songId")
-      val _cursorIndexOfArtistKey: Int = getColumnIndexOrThrow(_cursor, "artistKey")
-      val _collectionArtist: LongSparseArray<Artist?> = LongSparseArray<Artist?>()
-      while (_cursor.moveToNext()) {
-        val _tmpKey: Long
-        _tmpKey = _cursor.getLong(_cursorIndexOfArtistKey)
-        _collectionArtist.put(_tmpKey, null)
-      }
-      _cursor.moveToPosition(-1)
-      __fetchRelationshipArtistAsArtist(_collectionArtist)
-      val _result: SongWithArtist
-      if (_cursor.moveToFirst()) {
-        val _tmpSong: Song
-        val _tmpSongId: Long
-        _tmpSongId = _cursor.getLong(_cursorIndexOfSongId)
-        val _tmpArtistKey: Long
-        _tmpArtistKey = _cursor.getLong(_cursorIndexOfArtistKey)
-        _tmpSong = Song(_tmpSongId,_tmpArtistKey)
-        val _tmpArtist: Artist?
-        val _tmpKey_1: Long
-        _tmpKey_1 = _cursor.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'.")
+    return performBlocking(__db, true, false) { _connection ->
+      val _stmt: SQLiteStatement = _connection.prepare(_sql)
+      try {
+        val _cursorIndexOfSongId: Int = getColumnIndexOrThrow(_stmt, "songId")
+        val _cursorIndexOfArtistKey: Int = getColumnIndexOrThrow(_stmt, "artistKey")
+        val _collectionArtist: LongSparseArray<Artist?> = LongSparseArray<Artist?>()
+        while (_stmt.step()) {
+          val _tmpKey: Long
+          _tmpKey = _stmt.getLong(_cursorIndexOfArtistKey)
+          _collectionArtist.put(_tmpKey, null)
         }
-        _result = SongWithArtist(_tmpSong,_tmpArtist)
-      } else {
-        error("The query result was empty, but expected a single row to return a NON-NULL object of type <SongWithArtist>.")
+        _stmt.reset()
+        __fetchRelationshipArtistAsArtist(_connection, _collectionArtist)
+        val _result: SongWithArtist
+        if (_stmt.step()) {
+          val _tmpSong: Song
+          val _tmpSongId: Long
+          _tmpSongId = _stmt.getLong(_cursorIndexOfSongId)
+          val _tmpArtistKey: Long
+          _tmpArtistKey = _stmt.getLong(_cursorIndexOfArtistKey)
+          _tmpSong = Song(_tmpSongId,_tmpArtistKey)
+          val _tmpArtist: Artist?
+          val _tmpKey_1: Long
+          _tmpKey_1 = _stmt.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'.")
+          }
+          _result = SongWithArtist(_tmpSong,_tmpArtist)
+        } else {
+          error("The query result was empty, but expected a single row to return a NON-NULL object of type <SongWithArtist>.")
+        }
+        _result
+      } finally {
+        _stmt.close()
       }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
     }
   }
 
   public override fun getArtistAndSongs(): ArtistAndSongs {
     val _sql: String = "SELECT * FROM Artist"
-    val _statement: RoomSQLiteQuery = acquire(_sql, 0)
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, true, null)
-    try {
-      val _cursorIndexOfArtistId: Int = getColumnIndexOrThrow(_cursor, "artistId")
-      val _collectionSongs: LongSparseArray<ArrayList<Song>> = LongSparseArray<ArrayList<Song>>()
-      while (_cursor.moveToNext()) {
-        val _tmpKey: Long
-        _tmpKey = _cursor.getLong(_cursorIndexOfArtistId)
-        if (!_collectionSongs.containsKey(_tmpKey)) {
-          _collectionSongs.put(_tmpKey, ArrayList<Song>())
+    return performBlocking(__db, true, false) { _connection ->
+      val _stmt: SQLiteStatement = _connection.prepare(_sql)
+      try {
+        val _cursorIndexOfArtistId: Int = getColumnIndexOrThrow(_stmt, "artistId")
+        val _collectionSongs: LongSparseArray<MutableList<Song>> =
+            LongSparseArray<MutableList<Song>>()
+        while (_stmt.step()) {
+          val _tmpKey: Long
+          _tmpKey = _stmt.getLong(_cursorIndexOfArtistId)
+          if (!_collectionSongs.containsKey(_tmpKey)) {
+            _collectionSongs.put(_tmpKey, mutableListOf())
+          }
         }
+        _stmt.reset()
+        __fetchRelationshipSongAsSong(_connection, _collectionSongs)
+        val _result: ArtistAndSongs
+        if (_stmt.step()) {
+          val _tmpArtist: Artist
+          val _tmpArtistId: Long
+          _tmpArtistId = _stmt.getLong(_cursorIndexOfArtistId)
+          _tmpArtist = Artist(_tmpArtistId)
+          val _tmpSongsCollection: MutableList<Song>
+          val _tmpKey_1: Long
+          _tmpKey_1 = _stmt.getLong(_cursorIndexOfArtistId)
+          _tmpSongsCollection = checkNotNull(_collectionSongs.get(_tmpKey_1))
+          _result = ArtistAndSongs(_tmpArtist,_tmpSongsCollection)
+        } else {
+          error("The query result was empty, but expected a single row to return a NON-NULL object of type <ArtistAndSongs>.")
+        }
+        _result
+      } finally {
+        _stmt.close()
       }
-      _cursor.moveToPosition(-1)
-      __fetchRelationshipSongAsSong(_collectionSongs)
-      val _result: ArtistAndSongs
-      if (_cursor.moveToFirst()) {
-        val _tmpArtist: Artist
-        val _tmpArtistId: Long
-        _tmpArtistId = _cursor.getLong(_cursorIndexOfArtistId)
-        _tmpArtist = Artist(_tmpArtistId)
-        val _tmpSongsCollection: ArrayList<Song>
-        val _tmpKey_1: Long
-        _tmpKey_1 = _cursor.getLong(_cursorIndexOfArtistId)
-        _tmpSongsCollection = checkNotNull(_collectionSongs.get(_tmpKey_1))
-        _result = ArtistAndSongs(_tmpArtist,_tmpSongsCollection)
-      } else {
-        error("The query result was empty, but expected a single row to return a NON-NULL object of type <ArtistAndSongs>.")
-      }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
     }
   }
 
   public override fun getPlaylistAndSongs(): PlaylistAndSongs {
     val _sql: String = "SELECT * FROM Playlist"
-    val _statement: RoomSQLiteQuery = acquire(_sql, 0)
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, true, null)
-    try {
-      val _cursorIndexOfPlaylistId: Int = getColumnIndexOrThrow(_cursor, "playlistId")
-      val _collectionSongs: LongSparseArray<ArrayList<Song>> = LongSparseArray<ArrayList<Song>>()
-      while (_cursor.moveToNext()) {
-        val _tmpKey: Long
-        _tmpKey = _cursor.getLong(_cursorIndexOfPlaylistId)
-        if (!_collectionSongs.containsKey(_tmpKey)) {
-          _collectionSongs.put(_tmpKey, ArrayList<Song>())
+    return performBlocking(__db, true, false) { _connection ->
+      val _stmt: SQLiteStatement = _connection.prepare(_sql)
+      try {
+        val _cursorIndexOfPlaylistId: Int = getColumnIndexOrThrow(_stmt, "playlistId")
+        val _collectionSongs: LongSparseArray<MutableList<Song>> =
+            LongSparseArray<MutableList<Song>>()
+        while (_stmt.step()) {
+          val _tmpKey: Long
+          _tmpKey = _stmt.getLong(_cursorIndexOfPlaylistId)
+          if (!_collectionSongs.containsKey(_tmpKey)) {
+            _collectionSongs.put(_tmpKey, mutableListOf())
+          }
         }
+        _stmt.reset()
+        __fetchRelationshipSongAsSong_1(_connection, _collectionSongs)
+        val _result: PlaylistAndSongs
+        if (_stmt.step()) {
+          val _tmpPlaylist: Playlist
+          val _tmpPlaylistId: Long
+          _tmpPlaylistId = _stmt.getLong(_cursorIndexOfPlaylistId)
+          _tmpPlaylist = Playlist(_tmpPlaylistId)
+          val _tmpSongsCollection: MutableList<Song>
+          val _tmpKey_1: Long
+          _tmpKey_1 = _stmt.getLong(_cursorIndexOfPlaylistId)
+          _tmpSongsCollection = checkNotNull(_collectionSongs.get(_tmpKey_1))
+          _result = PlaylistAndSongs(_tmpPlaylist,_tmpSongsCollection)
+        } else {
+          error("The query result was empty, but expected a single row to return a NON-NULL object of type <PlaylistAndSongs>.")
+        }
+        _result
+      } finally {
+        _stmt.close()
       }
-      _cursor.moveToPosition(-1)
-      __fetchRelationshipSongAsSong_1(_collectionSongs)
-      val _result: PlaylistAndSongs
-      if (_cursor.moveToFirst()) {
-        val _tmpPlaylist: Playlist
-        val _tmpPlaylistId: Long
-        _tmpPlaylistId = _cursor.getLong(_cursorIndexOfPlaylistId)
-        _tmpPlaylist = Playlist(_tmpPlaylistId)
-        val _tmpSongsCollection: ArrayList<Song>
-        val _tmpKey_1: Long
-        _tmpKey_1 = _cursor.getLong(_cursorIndexOfPlaylistId)
-        _tmpSongsCollection = checkNotNull(_collectionSongs.get(_tmpKey_1))
-        _result = PlaylistAndSongs(_tmpPlaylist,_tmpSongsCollection)
-      } else {
-        error("The query result was empty, but expected a single row to return a NON-NULL object of type <PlaylistAndSongs>.")
-      }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
     }
   }
 
-  private fun __fetchRelationshipArtistAsArtist(_map: LongSparseArray<Artist?>) {
+  private fun __fetchRelationshipArtistAsArtist(_connection: SQLiteConnection,
+      _map: LongSparseArray<Artist?>) {
     if (_map.isEmpty()) {
       return
     }
-    if (_map.size() > RoomDatabase.MAX_BIND_PARAMETER_CNT) {
+    if (_map.size() > 999) {
       recursiveFetchLongSparseArray(_map, false) {
-        __fetchRelationshipArtistAsArtist(it)
+        __fetchRelationshipArtistAsArtist(_connection, it)
       }
       return
     }
@@ -162,44 +162,43 @@
     appendPlaceholders(_stringBuilder, _inputSize)
     _stringBuilder.append(")")
     val _sql: String = _stringBuilder.toString()
-    val _argCount: Int = 0 + _inputSize
-    val _stmt: RoomSQLiteQuery = acquire(_sql, _argCount)
+    val _stmt: SQLiteStatement = _connection.prepare(_sql)
     var _argIndex: Int = 1
     for (i in 0 until _map.size()) {
       val _item: Long = _map.keyAt(i)
       _stmt.bindLong(_argIndex, _item)
       _argIndex++
     }
-    val _cursor: Cursor = query(__db, _stmt, false, null)
     try {
-      val _itemKeyIndex: Int = getColumnIndex(_cursor, "artistId")
+      val _itemKeyIndex: Int = getColumnIndex(_stmt, "artistId")
       if (_itemKeyIndex == -1) {
         return
       }
       val _cursorIndexOfArtistId: Int = 0
-      while (_cursor.moveToNext()) {
+      while (_stmt.step()) {
         val _tmpKey: Long
-        _tmpKey = _cursor.getLong(_itemKeyIndex)
+        _tmpKey = _stmt.getLong(_itemKeyIndex)
         if (_map.containsKey(_tmpKey)) {
           val _item_1: Artist
           val _tmpArtistId: Long
-          _tmpArtistId = _cursor.getLong(_cursorIndexOfArtistId)
+          _tmpArtistId = _stmt.getLong(_cursorIndexOfArtistId)
           _item_1 = Artist(_tmpArtistId)
           _map.put(_tmpKey, _item_1)
         }
       }
     } finally {
-      _cursor.close()
+      _stmt.close()
     }
   }
 
-  private fun __fetchRelationshipSongAsSong(_map: LongSparseArray<ArrayList<Song>>) {
+  private fun __fetchRelationshipSongAsSong(_connection: SQLiteConnection,
+      _map: LongSparseArray<MutableList<Song>>) {
     if (_map.isEmpty()) {
       return
     }
-    if (_map.size() > RoomDatabase.MAX_BIND_PARAMETER_CNT) {
+    if (_map.size() > 999) {
       recursiveFetchLongSparseArray(_map, true) {
-        __fetchRelationshipSongAsSong(it)
+        __fetchRelationshipSongAsSong(_connection, it)
       }
       return
     }
@@ -209,48 +208,47 @@
     appendPlaceholders(_stringBuilder, _inputSize)
     _stringBuilder.append(")")
     val _sql: String = _stringBuilder.toString()
-    val _argCount: Int = 0 + _inputSize
-    val _stmt: RoomSQLiteQuery = acquire(_sql, _argCount)
+    val _stmt: SQLiteStatement = _connection.prepare(_sql)
     var _argIndex: Int = 1
     for (i in 0 until _map.size()) {
       val _item: Long = _map.keyAt(i)
       _stmt.bindLong(_argIndex, _item)
       _argIndex++
     }
-    val _cursor: Cursor = query(__db, _stmt, false, null)
     try {
-      val _itemKeyIndex: Int = getColumnIndex(_cursor, "artistKey")
+      val _itemKeyIndex: Int = getColumnIndex(_stmt, "artistKey")
       if (_itemKeyIndex == -1) {
         return
       }
       val _cursorIndexOfSongId: Int = 0
       val _cursorIndexOfArtistKey: Int = 1
-      while (_cursor.moveToNext()) {
+      while (_stmt.step()) {
         val _tmpKey: Long
-        _tmpKey = _cursor.getLong(_itemKeyIndex)
-        val _tmpRelation: ArrayList<Song>? = _map.get(_tmpKey)
+        _tmpKey = _stmt.getLong(_itemKeyIndex)
+        val _tmpRelation: MutableList<Song>? = _map.get(_tmpKey)
         if (_tmpRelation != null) {
           val _item_1: Song
           val _tmpSongId: Long
-          _tmpSongId = _cursor.getLong(_cursorIndexOfSongId)
+          _tmpSongId = _stmt.getLong(_cursorIndexOfSongId)
           val _tmpArtistKey: Long
-          _tmpArtistKey = _cursor.getLong(_cursorIndexOfArtistKey)
+          _tmpArtistKey = _stmt.getLong(_cursorIndexOfArtistKey)
           _item_1 = Song(_tmpSongId,_tmpArtistKey)
           _tmpRelation.add(_item_1)
         }
       }
     } finally {
-      _cursor.close()
+      _stmt.close()
     }
   }
 
-  private fun __fetchRelationshipSongAsSong_1(_map: LongSparseArray<ArrayList<Song>>) {
+  private fun __fetchRelationshipSongAsSong_1(_connection: SQLiteConnection,
+      _map: LongSparseArray<MutableList<Song>>) {
     if (_map.isEmpty()) {
       return
     }
-    if (_map.size() > RoomDatabase.MAX_BIND_PARAMETER_CNT) {
+    if (_map.size() > 999) {
       recursiveFetchLongSparseArray(_map, true) {
-        __fetchRelationshipSongAsSong_1(it)
+        __fetchRelationshipSongAsSong_1(_connection, it)
       }
       return
     }
@@ -260,15 +258,13 @@
     appendPlaceholders(_stringBuilder, _inputSize)
     _stringBuilder.append(")")
     val _sql: String = _stringBuilder.toString()
-    val _argCount: Int = 0 + _inputSize
-    val _stmt: RoomSQLiteQuery = acquire(_sql, _argCount)
+    val _stmt: SQLiteStatement = _connection.prepare(_sql)
     var _argIndex: Int = 1
     for (i in 0 until _map.size()) {
       val _item: Long = _map.keyAt(i)
       _stmt.bindLong(_argIndex, _item)
       _argIndex++
     }
-    val _cursor: Cursor = query(__db, _stmt, false, null)
     try {
       // _junction.playlistKey
       val _itemKeyIndex: Int = 2
@@ -277,22 +273,22 @@
       }
       val _cursorIndexOfSongId: Int = 0
       val _cursorIndexOfArtistKey: Int = 1
-      while (_cursor.moveToNext()) {
+      while (_stmt.step()) {
         val _tmpKey: Long
-        _tmpKey = _cursor.getLong(_itemKeyIndex)
-        val _tmpRelation: ArrayList<Song>? = _map.get(_tmpKey)
+        _tmpKey = _stmt.getLong(_itemKeyIndex)
+        val _tmpRelation: MutableList<Song>? = _map.get(_tmpKey)
         if (_tmpRelation != null) {
           val _item_1: Song
           val _tmpSongId: Long
-          _tmpSongId = _cursor.getLong(_cursorIndexOfSongId)
+          _tmpSongId = _stmt.getLong(_cursorIndexOfSongId)
           val _tmpArtistKey: Long
-          _tmpArtistKey = _cursor.getLong(_cursorIndexOfArtistKey)
+          _tmpArtistKey = _stmt.getLong(_cursorIndexOfArtistKey)
           _item_1 = Song(_tmpSongId,_tmpArtistKey)
           _tmpRelation.add(_item_1)
         }
       }
     } finally {
-      _cursor.close()
+      _stmt.close()
     }
   }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/relations_nullable.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/relations_nullable.kt
index 37123cf2..15d43af 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/relations_nullable.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/relations_nullable.kt
@@ -1,21 +1,22 @@
-import android.database.Cursor
 import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
 import androidx.room.util.appendPlaceholders
 import androidx.room.util.getColumnIndex
 import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
-import androidx.room.util.recursiveFetchHashMap
-import java.util.ArrayList
-import java.util.HashMap
+import androidx.room.util.performBlocking
+import androidx.room.util.recursiveFetchMap
+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
 
@@ -31,145 +32,143 @@
 
   public override fun getSongsWithArtist(): SongWithArtist {
     val _sql: String = "SELECT * FROM Song"
-    val _statement: RoomSQLiteQuery = acquire(_sql, 0)
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, true, null)
-    try {
-      val _cursorIndexOfSongId: Int = getColumnIndexOrThrow(_cursor, "songId")
-      val _cursorIndexOfArtistKey: Int = getColumnIndexOrThrow(_cursor, "artistKey")
-      val _collectionArtist: HashMap<Long, Artist?> = HashMap<Long, Artist?>()
-      while (_cursor.moveToNext()) {
-        val _tmpKey: Long?
-        if (_cursor.isNull(_cursorIndexOfArtistKey)) {
-          _tmpKey = null
+    return performBlocking(__db, true, false) { _connection ->
+      val _stmt: SQLiteStatement = _connection.prepare(_sql)
+      try {
+        val _cursorIndexOfSongId: Int = getColumnIndexOrThrow(_stmt, "songId")
+        val _cursorIndexOfArtistKey: Int = getColumnIndexOrThrow(_stmt, "artistKey")
+        val _collectionArtist: MutableMap<Long, Artist?> = mutableMapOf()
+        while (_stmt.step()) {
+          val _tmpKey: Long?
+          if (_stmt.isNull(_cursorIndexOfArtistKey)) {
+            _tmpKey = null
+          } else {
+            _tmpKey = _stmt.getLong(_cursorIndexOfArtistKey)
+          }
+          if (_tmpKey != null) {
+            _collectionArtist.put(_tmpKey, null)
+          }
+        }
+        _stmt.reset()
+        __fetchRelationshipArtistAsArtist(_connection, _collectionArtist)
+        val _result: SongWithArtist
+        if (_stmt.step()) {
+          val _tmpSong: Song
+          val _tmpSongId: Long
+          _tmpSongId = _stmt.getLong(_cursorIndexOfSongId)
+          val _tmpArtistKey: Long?
+          if (_stmt.isNull(_cursorIndexOfArtistKey)) {
+            _tmpArtistKey = null
+          } else {
+            _tmpArtistKey = _stmt.getLong(_cursorIndexOfArtistKey)
+          }
+          _tmpSong = Song(_tmpSongId,_tmpArtistKey)
+          val _tmpArtist: Artist?
+          val _tmpKey_1: Long?
+          if (_stmt.isNull(_cursorIndexOfArtistKey)) {
+            _tmpKey_1 = null
+          } else {
+            _tmpKey_1 = _stmt.getLong(_cursorIndexOfArtistKey)
+          }
+          if (_tmpKey_1 != null) {
+            _tmpArtist = _collectionArtist.get(_tmpKey_1)
+          } else {
+            _tmpArtist = null
+          }
+          _result = SongWithArtist(_tmpSong,_tmpArtist)
         } else {
-          _tmpKey = _cursor.getLong(_cursorIndexOfArtistKey)
+          error("The query result was empty, but expected a single row to return a NON-NULL object of type <SongWithArtist>.")
         }
-        if (_tmpKey != null) {
-          _collectionArtist.put(_tmpKey, null)
-        }
+        _result
+      } finally {
+        _stmt.close()
       }
-      _cursor.moveToPosition(-1)
-      __fetchRelationshipArtistAsArtist(_collectionArtist)
-      val _result: SongWithArtist
-      if (_cursor.moveToFirst()) {
-        val _tmpSong: Song
-        val _tmpSongId: Long
-        _tmpSongId = _cursor.getLong(_cursorIndexOfSongId)
-        val _tmpArtistKey: Long?
-        if (_cursor.isNull(_cursorIndexOfArtistKey)) {
-          _tmpArtistKey = null
-        } else {
-          _tmpArtistKey = _cursor.getLong(_cursorIndexOfArtistKey)
-        }
-        _tmpSong = Song(_tmpSongId,_tmpArtistKey)
-        val _tmpArtist: Artist?
-        val _tmpKey_1: Long?
-        if (_cursor.isNull(_cursorIndexOfArtistKey)) {
-          _tmpKey_1 = null
-        } else {
-          _tmpKey_1 = _cursor.getLong(_cursorIndexOfArtistKey)
-        }
-        if (_tmpKey_1 != null) {
-          _tmpArtist = _collectionArtist.get(_tmpKey_1)
-        } else {
-          _tmpArtist = null
-        }
-        _result = SongWithArtist(_tmpSong,_tmpArtist)
-      } else {
-        error("The query result was empty, but expected a single row to return a NON-NULL object of type <SongWithArtist>.")
-      }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
     }
   }
 
   public override fun getArtistAndSongs(): ArtistAndSongs {
     val _sql: String = "SELECT * FROM Artist"
-    val _statement: RoomSQLiteQuery = acquire(_sql, 0)
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, true, null)
-    try {
-      val _cursorIndexOfArtistId: Int = getColumnIndexOrThrow(_cursor, "artistId")
-      val _collectionSongs: HashMap<Long, ArrayList<Song>> = HashMap<Long, ArrayList<Song>>()
-      while (_cursor.moveToNext()) {
-        val _tmpKey: Long
-        _tmpKey = _cursor.getLong(_cursorIndexOfArtistId)
-        if (!_collectionSongs.containsKey(_tmpKey)) {
-          _collectionSongs.put(_tmpKey, ArrayList<Song>())
+    return performBlocking(__db, true, false) { _connection ->
+      val _stmt: SQLiteStatement = _connection.prepare(_sql)
+      try {
+        val _cursorIndexOfArtistId: Int = getColumnIndexOrThrow(_stmt, "artistId")
+        val _collectionSongs: MutableMap<Long, MutableList<Song>> = mutableMapOf()
+        while (_stmt.step()) {
+          val _tmpKey: Long
+          _tmpKey = _stmt.getLong(_cursorIndexOfArtistId)
+          if (!_collectionSongs.containsKey(_tmpKey)) {
+            _collectionSongs.put(_tmpKey, mutableListOf())
+          }
         }
+        _stmt.reset()
+        __fetchRelationshipSongAsSong(_connection, _collectionSongs)
+        val _result: ArtistAndSongs
+        if (_stmt.step()) {
+          val _tmpArtist: Artist
+          val _tmpArtistId: Long
+          _tmpArtistId = _stmt.getLong(_cursorIndexOfArtistId)
+          _tmpArtist = Artist(_tmpArtistId)
+          val _tmpSongsCollection: MutableList<Song>
+          val _tmpKey_1: Long
+          _tmpKey_1 = _stmt.getLong(_cursorIndexOfArtistId)
+          _tmpSongsCollection = _collectionSongs.getValue(_tmpKey_1)
+          _result = ArtistAndSongs(_tmpArtist,_tmpSongsCollection)
+        } else {
+          error("The query result was empty, but expected a single row to return a NON-NULL object of type <ArtistAndSongs>.")
+        }
+        _result
+      } finally {
+        _stmt.close()
       }
-      _cursor.moveToPosition(-1)
-      __fetchRelationshipSongAsSong(_collectionSongs)
-      val _result: ArtistAndSongs
-      if (_cursor.moveToFirst()) {
-        val _tmpArtist: Artist
-        val _tmpArtistId: Long
-        _tmpArtistId = _cursor.getLong(_cursorIndexOfArtistId)
-        _tmpArtist = Artist(_tmpArtistId)
-        val _tmpSongsCollection: ArrayList<Song>
-        val _tmpKey_1: Long
-        _tmpKey_1 = _cursor.getLong(_cursorIndexOfArtistId)
-        _tmpSongsCollection = _collectionSongs.getValue(_tmpKey_1)
-        _result = ArtistAndSongs(_tmpArtist,_tmpSongsCollection)
-      } else {
-        error("The query result was empty, but expected a single row to return a NON-NULL object of type <ArtistAndSongs>.")
-      }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
     }
   }
 
   public override fun getPlaylistAndSongs(): PlaylistAndSongs {
     val _sql: String = "SELECT * FROM Playlist"
-    val _statement: RoomSQLiteQuery = acquire(_sql, 0)
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, true, null)
-    try {
-      val _cursorIndexOfPlaylistId: Int = getColumnIndexOrThrow(_cursor, "playlistId")
-      val _collectionSongs: HashMap<Long, ArrayList<Song>> = HashMap<Long, ArrayList<Song>>()
-      while (_cursor.moveToNext()) {
-        val _tmpKey: Long
-        _tmpKey = _cursor.getLong(_cursorIndexOfPlaylistId)
-        if (!_collectionSongs.containsKey(_tmpKey)) {
-          _collectionSongs.put(_tmpKey, ArrayList<Song>())
+    return performBlocking(__db, true, false) { _connection ->
+      val _stmt: SQLiteStatement = _connection.prepare(_sql)
+      try {
+        val _cursorIndexOfPlaylistId: Int = getColumnIndexOrThrow(_stmt, "playlistId")
+        val _collectionSongs: MutableMap<Long, MutableList<Song>> = mutableMapOf()
+        while (_stmt.step()) {
+          val _tmpKey: Long
+          _tmpKey = _stmt.getLong(_cursorIndexOfPlaylistId)
+          if (!_collectionSongs.containsKey(_tmpKey)) {
+            _collectionSongs.put(_tmpKey, mutableListOf())
+          }
         }
+        _stmt.reset()
+        __fetchRelationshipSongAsSong_1(_connection, _collectionSongs)
+        val _result: PlaylistAndSongs
+        if (_stmt.step()) {
+          val _tmpPlaylist: Playlist
+          val _tmpPlaylistId: Long
+          _tmpPlaylistId = _stmt.getLong(_cursorIndexOfPlaylistId)
+          _tmpPlaylist = Playlist(_tmpPlaylistId)
+          val _tmpSongsCollection: MutableList<Song>
+          val _tmpKey_1: Long
+          _tmpKey_1 = _stmt.getLong(_cursorIndexOfPlaylistId)
+          _tmpSongsCollection = _collectionSongs.getValue(_tmpKey_1)
+          _result = PlaylistAndSongs(_tmpPlaylist,_tmpSongsCollection)
+        } else {
+          error("The query result was empty, but expected a single row to return a NON-NULL object of type <PlaylistAndSongs>.")
+        }
+        _result
+      } finally {
+        _stmt.close()
       }
-      _cursor.moveToPosition(-1)
-      __fetchRelationshipSongAsSong_1(_collectionSongs)
-      val _result: PlaylistAndSongs
-      if (_cursor.moveToFirst()) {
-        val _tmpPlaylist: Playlist
-        val _tmpPlaylistId: Long
-        _tmpPlaylistId = _cursor.getLong(_cursorIndexOfPlaylistId)
-        _tmpPlaylist = Playlist(_tmpPlaylistId)
-        val _tmpSongsCollection: ArrayList<Song>
-        val _tmpKey_1: Long
-        _tmpKey_1 = _cursor.getLong(_cursorIndexOfPlaylistId)
-        _tmpSongsCollection = _collectionSongs.getValue(_tmpKey_1)
-        _result = PlaylistAndSongs(_tmpPlaylist,_tmpSongsCollection)
-      } else {
-        error("The query result was empty, but expected a single row to return a NON-NULL object of type <PlaylistAndSongs>.")
-      }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
     }
   }
 
-  private fun __fetchRelationshipArtistAsArtist(_map: HashMap<Long, Artist?>) {
+  private fun __fetchRelationshipArtistAsArtist(_connection: SQLiteConnection,
+      _map: MutableMap<Long, Artist?>) {
     val __mapKeySet: Set<Long> = _map.keys
     if (__mapKeySet.isEmpty()) {
       return
     }
-    if (_map.size > RoomDatabase.MAX_BIND_PARAMETER_CNT) {
-      recursiveFetchHashMap(_map, false) {
-        __fetchRelationshipArtistAsArtist(it)
+    if (_map.size > 999) {
+      recursiveFetchMap(_map, false) {
+        __fetchRelationshipArtistAsArtist(_connection, it)
       }
       return
     }
@@ -179,44 +178,43 @@
     appendPlaceholders(_stringBuilder, _inputSize)
     _stringBuilder.append(")")
     val _sql: String = _stringBuilder.toString()
-    val _argCount: Int = 0 + _inputSize
-    val _stmt: RoomSQLiteQuery = acquire(_sql, _argCount)
+    val _stmt: SQLiteStatement = _connection.prepare(_sql)
     var _argIndex: Int = 1
     for (_item: Long in __mapKeySet) {
       _stmt.bindLong(_argIndex, _item)
       _argIndex++
     }
-    val _cursor: Cursor = query(__db, _stmt, false, null)
     try {
-      val _itemKeyIndex: Int = getColumnIndex(_cursor, "artistId")
+      val _itemKeyIndex: Int = getColumnIndex(_stmt, "artistId")
       if (_itemKeyIndex == -1) {
         return
       }
       val _cursorIndexOfArtistId: Int = 0
-      while (_cursor.moveToNext()) {
+      while (_stmt.step()) {
         val _tmpKey: Long
-        _tmpKey = _cursor.getLong(_itemKeyIndex)
+        _tmpKey = _stmt.getLong(_itemKeyIndex)
         if (_map.containsKey(_tmpKey)) {
           val _item_1: Artist?
           val _tmpArtistId: Long
-          _tmpArtistId = _cursor.getLong(_cursorIndexOfArtistId)
+          _tmpArtistId = _stmt.getLong(_cursorIndexOfArtistId)
           _item_1 = Artist(_tmpArtistId)
           _map.put(_tmpKey, _item_1)
         }
       }
     } finally {
-      _cursor.close()
+      _stmt.close()
     }
   }
 
-  private fun __fetchRelationshipSongAsSong(_map: HashMap<Long, ArrayList<Song>>) {
+  private fun __fetchRelationshipSongAsSong(_connection: SQLiteConnection,
+      _map: MutableMap<Long, MutableList<Song>>) {
     val __mapKeySet: Set<Long> = _map.keys
     if (__mapKeySet.isEmpty()) {
       return
     }
-    if (_map.size > RoomDatabase.MAX_BIND_PARAMETER_CNT) {
-      recursiveFetchHashMap(_map, true) {
-        __fetchRelationshipSongAsSong(it)
+    if (_map.size > 999) {
+      recursiveFetchMap(_map, true) {
+        __fetchRelationshipSongAsSong(_connection, it)
       }
       return
     }
@@ -226,39 +224,37 @@
     appendPlaceholders(_stringBuilder, _inputSize)
     _stringBuilder.append(")")
     val _sql: String = _stringBuilder.toString()
-    val _argCount: Int = 0 + _inputSize
-    val _stmt: RoomSQLiteQuery = acquire(_sql, _argCount)
+    val _stmt: SQLiteStatement = _connection.prepare(_sql)
     var _argIndex: Int = 1
     for (_item: Long in __mapKeySet) {
       _stmt.bindLong(_argIndex, _item)
       _argIndex++
     }
-    val _cursor: Cursor = query(__db, _stmt, false, null)
     try {
-      val _itemKeyIndex: Int = getColumnIndex(_cursor, "artistKey")
+      val _itemKeyIndex: Int = getColumnIndex(_stmt, "artistKey")
       if (_itemKeyIndex == -1) {
         return
       }
       val _cursorIndexOfSongId: Int = 0
       val _cursorIndexOfArtistKey: Int = 1
-      while (_cursor.moveToNext()) {
+      while (_stmt.step()) {
         val _tmpKey: Long?
-        if (_cursor.isNull(_itemKeyIndex)) {
+        if (_stmt.isNull(_itemKeyIndex)) {
           _tmpKey = null
         } else {
-          _tmpKey = _cursor.getLong(_itemKeyIndex)
+          _tmpKey = _stmt.getLong(_itemKeyIndex)
         }
         if (_tmpKey != null) {
-          val _tmpRelation: ArrayList<Song>? = _map.get(_tmpKey)
+          val _tmpRelation: MutableList<Song>? = _map.get(_tmpKey)
           if (_tmpRelation != null) {
             val _item_1: Song
             val _tmpSongId: Long
-            _tmpSongId = _cursor.getLong(_cursorIndexOfSongId)
+            _tmpSongId = _stmt.getLong(_cursorIndexOfSongId)
             val _tmpArtistKey: Long?
-            if (_cursor.isNull(_cursorIndexOfArtistKey)) {
+            if (_stmt.isNull(_cursorIndexOfArtistKey)) {
               _tmpArtistKey = null
             } else {
-              _tmpArtistKey = _cursor.getLong(_cursorIndexOfArtistKey)
+              _tmpArtistKey = _stmt.getLong(_cursorIndexOfArtistKey)
             }
             _item_1 = Song(_tmpSongId,_tmpArtistKey)
             _tmpRelation.add(_item_1)
@@ -266,18 +262,19 @@
         }
       }
     } finally {
-      _cursor.close()
+      _stmt.close()
     }
   }
 
-  private fun __fetchRelationshipSongAsSong_1(_map: HashMap<Long, ArrayList<Song>>) {
+  private fun __fetchRelationshipSongAsSong_1(_connection: SQLiteConnection,
+      _map: MutableMap<Long, MutableList<Song>>) {
     val __mapKeySet: Set<Long> = _map.keys
     if (__mapKeySet.isEmpty()) {
       return
     }
-    if (_map.size > RoomDatabase.MAX_BIND_PARAMETER_CNT) {
-      recursiveFetchHashMap(_map, true) {
-        __fetchRelationshipSongAsSong_1(it)
+    if (_map.size > 999) {
+      recursiveFetchMap(_map, true) {
+        __fetchRelationshipSongAsSong_1(_connection, it)
       }
       return
     }
@@ -287,14 +284,12 @@
     appendPlaceholders(_stringBuilder, _inputSize)
     _stringBuilder.append(")")
     val _sql: String = _stringBuilder.toString()
-    val _argCount: Int = 0 + _inputSize
-    val _stmt: RoomSQLiteQuery = acquire(_sql, _argCount)
+    val _stmt: SQLiteStatement = _connection.prepare(_sql)
     var _argIndex: Int = 1
     for (_item: Long in __mapKeySet) {
       _stmt.bindLong(_argIndex, _item)
       _argIndex++
     }
-    val _cursor: Cursor = query(__db, _stmt, false, null)
     try {
       // _junction.playlistKey
       val _itemKeyIndex: Int = 2
@@ -303,26 +298,26 @@
       }
       val _cursorIndexOfSongId: Int = 0
       val _cursorIndexOfArtistKey: Int = 1
-      while (_cursor.moveToNext()) {
+      while (_stmt.step()) {
         val _tmpKey: Long
-        _tmpKey = _cursor.getLong(_itemKeyIndex)
-        val _tmpRelation: ArrayList<Song>? = _map.get(_tmpKey)
+        _tmpKey = _stmt.getLong(_itemKeyIndex)
+        val _tmpRelation: MutableList<Song>? = _map.get(_tmpKey)
         if (_tmpRelation != null) {
           val _item_1: Song
           val _tmpSongId: Long
-          _tmpSongId = _cursor.getLong(_cursorIndexOfSongId)
+          _tmpSongId = _stmt.getLong(_cursorIndexOfSongId)
           val _tmpArtistKey: Long?
-          if (_cursor.isNull(_cursorIndexOfArtistKey)) {
+          if (_stmt.isNull(_cursorIndexOfArtistKey)) {
             _tmpArtistKey = null
           } else {
-            _tmpArtistKey = _cursor.getLong(_cursorIndexOfArtistKey)
+            _tmpArtistKey = _stmt.getLong(_cursorIndexOfArtistKey)
           }
           _item_1 = Song(_tmpSongId,_tmpArtistKey)
           _tmpRelation.add(_item_1)
         }
       }
     } finally {
-      _cursor.close()
+      _stmt.close()
     }
   }
 
diff --git a/room/room-runtime/api/restricted_current.txt b/room/room-runtime/api/restricted_current.txt
index 7039636..5634a6c 100644
--- a/room/room-runtime/api/restricted_current.txt
+++ b/room/room-runtime/api/restricted_current.txt
@@ -487,10 +487,11 @@
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static <T, C> T findAndInstantiateDatabaseImpl(Class<C> klass, optional String suffix);
   }
 
-  @RestrictTo({androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX}) public final class RelationUtil {
-    method public static <K, V> void recursiveFetchArrayMap(androidx.collection.ArrayMap<K,V> map, boolean isRelationCollection, kotlin.jvm.functions.Function1<? super androidx.collection.ArrayMap<K,V>,kotlin.Unit> fetchBlock);
-    method public static <K, V> void recursiveFetchHashMap(java.util.HashMap<K,V> map, boolean isRelationCollection, kotlin.jvm.functions.Function1<? super java.util.HashMap<K,V>,kotlin.Unit> fetchBlock);
-    method public static <V> void recursiveFetchLongSparseArray(androidx.collection.LongSparseArray<V> map, boolean isRelationCollection, kotlin.jvm.functions.Function1<? super androidx.collection.LongSparseArray<V>,kotlin.Unit> fetchBlock);
+  public final class RelationUtil {
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static <K, V> void recursiveFetchArrayMap(androidx.collection.ArrayMap<K,V> map, boolean isRelationCollection, kotlin.jvm.functions.Function1<? super androidx.collection.ArrayMap<K,V>,kotlin.Unit> fetchBlock);
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static <K, V> void recursiveFetchHashMap(java.util.HashMap<K,V> map, boolean isRelationCollection, kotlin.jvm.functions.Function1<? super java.util.HashMap<K,V>,kotlin.Unit> fetchBlock);
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static <V> void recursiveFetchLongSparseArray(androidx.collection.LongSparseArray<V> map, boolean isRelationCollection, kotlin.jvm.functions.Function1<? super androidx.collection.LongSparseArray<V>,kotlin.Unit> fetchBlock);
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static <K, V> void recursiveFetchMap(java.util.Map<K,V> map, boolean isRelationCollection, kotlin.jvm.functions.Function1<? super java.util.Map<K,V>,kotlin.Unit> fetchBlock);
   }
 
   public final class SQLiteConnectionUtil {
@@ -499,6 +500,7 @@
   }
 
   public final class SQLiteStatementUtil {
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static int getColumnIndex(androidx.sqlite.SQLiteStatement stmt, String name);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static int getColumnIndexOrThrow(androidx.sqlite.SQLiteStatement stmt, String name);
   }
 
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/util/RelationUtil.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/util/RelationUtil.android.kt
index cefa581..e4f7a9f 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/util/RelationUtil.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/util/RelationUtil.android.kt
@@ -14,8 +14,8 @@
  * limitations under the License.
  */
 
+@file:JvmMultifileClass
 @file:JvmName("RelationUtil")
-@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
 
 package androidx.room.util
 
@@ -32,6 +32,7 @@
  * @param isRelationCollection - True if [V] is a [Collection] which means it is non null.
  * @param fetchBlock - A lambda for calling the generated _fetchRelationship function.
  */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
 fun <K : Any, V> recursiveFetchHashMap(
     map: HashMap<K, V>,
     isRelationCollection: Boolean,
@@ -73,6 +74,7 @@
 /**
  * Same as [recursiveFetchHashMap] but for [LongSparseArray].
  */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
 fun <V> recursiveFetchLongSparseArray(
     map: LongSparseArray<V>,
     isRelationCollection: Boolean,
@@ -112,6 +114,7 @@
 /**
  * Same as [recursiveFetchHashMap] but for [ArrayMap].
  */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
 fun <K : Any, V> recursiveFetchArrayMap(
     map: ArrayMap<K, V>,
     isRelationCollection: Boolean,
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/util/StatementUtil.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/util/StatementUtil.android.kt
index 6b4d5d2..587b3fd 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/util/StatementUtil.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/util/StatementUtil.android.kt
@@ -27,12 +27,12 @@
  *
  * The implementation also contains Android-specific patches to workaround issues on older devices.
  */
-internal actual fun SQLiteStatement.getColumnIndex(name: String): Int {
-    var index = this.columnIndexOf(name)
+internal actual fun SQLiteStatement.columnIndexOf(name: String): Int {
+    var index = this.columnIndexOfCommon(name)
     if (index >= 0) {
         return index
     }
-    index = this.columnIndexOf("`$name`")
+    index = this.columnIndexOfCommon("`$name`")
     return if (index >= 0) {
         index
     } else {
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/util/RelationUtil.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/util/RelationUtil.kt
new file mode 100644
index 0000000..3a11b73
--- /dev/null
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/util/RelationUtil.kt
@@ -0,0 +1,76 @@
+/*
+ * 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.
+ */
+
+@file:JvmMultifileClass
+@file:JvmName("RelationUtil")
+
+package androidx.room.util
+
+import androidx.annotation.RestrictTo
+import kotlin.jvm.JvmMultifileClass
+import kotlin.jvm.JvmName
+
+/**
+ * Utility function used in generated code to recursively fetch relationships when the amount of
+ * keys exceed [MAX_BIND_PARAMETER_CNT].
+ *
+ * @param map - The map containing the relationship keys to fill-in.
+ * @param isRelationCollection - True if [V] is a [Collection] which means it is non null.
+ * @param fetchBlock - A lambda for calling the generated _fetchRelationship function.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+fun <K : Any, V> recursiveFetchMap(
+    map: MutableMap<K, V>,
+    isRelationCollection: Boolean,
+    fetchBlock: (MutableMap<K, V>) -> Unit
+) {
+    val tmpMap = mutableMapOf<K, V>()
+    var count = 0
+    for (key in map.keys) {
+        // Safe because `V` is a nullable type arg when isRelationCollection == false and vice versa
+        @Suppress("UNCHECKED_CAST")
+        if (isRelationCollection) {
+            tmpMap[key] = map[key] as V
+        } else {
+            tmpMap[key] = null as V
+        }
+        count++
+        if (count == MAX_BIND_PARAMETER_CNT) {
+            // recursively load that batch
+            fetchBlock(tmpMap)
+            // for non collection relation, put the loaded batch in the original map,
+            // not needed when dealing with collections since references are passed
+            if (!isRelationCollection) {
+                map.putAll(tmpMap)
+            }
+            tmpMap.clear()
+            count = 0
+        }
+    }
+    if (count > 0) {
+        // load the last batch
+        fetchBlock(tmpMap)
+        // for non collection relation, put the last batch in the original map
+        if (!isRelationCollection) {
+            map.putAll(tmpMap)
+        }
+    }
+}
+
+/**
+ * Unfortunately, we cannot read this value so we are only setting it to the SQLite default.
+ */
+internal const val MAX_BIND_PARAMETER_CNT = 999
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/util/SchemaInfoUtil.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/util/SchemaInfoUtil.kt
index d4ebdd5..2d68ae8 100644
--- a/room/room-runtime/src/commonMain/kotlin/androidx/room/util/SchemaInfoUtil.kt
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/util/SchemaInfoUtil.kt
@@ -69,11 +69,11 @@
 ): Set<TableInfo.ForeignKey> {
     // this seems to return everything in order but it is not documented so better be safe
     connection.prepare("PRAGMA foreign_key_list(`$tableName`)").use { stmt ->
-        val idColumnIndex = stmt.getColumnIndex("id")
-        val seqColumnIndex = stmt.getColumnIndex("seq")
-        val tableColumnIndex = stmt.getColumnIndex("table")
-        val onDeleteColumnIndex = stmt.getColumnIndex("on_delete")
-        val onUpdateColumnIndex = stmt.getColumnIndex("on_update")
+        val idColumnIndex = stmt.columnIndexOf("id")
+        val seqColumnIndex = stmt.columnIndexOf("seq")
+        val tableColumnIndex = stmt.columnIndexOf("table")
+        val onDeleteColumnIndex = stmt.columnIndexOf("on_delete")
+        val onUpdateColumnIndex = stmt.columnIndexOf("on_update")
         val ordered = readForeignKeyFieldMappings(stmt)
 
         // Reset cursor as readForeignKeyFieldMappings has moved it
@@ -132,10 +132,10 @@
 private fun readForeignKeyFieldMappings(
     stmt: SQLiteStatement
 ): List<ForeignKeyWithSequence> {
-    val idColumnIndex = stmt.getColumnIndex("id")
-    val seqColumnIndex = stmt.getColumnIndex("seq")
-    val fromColumnIndex = stmt.getColumnIndex("from")
-    val toColumnIndex = stmt.getColumnIndex("to")
+    val idColumnIndex = stmt.columnIndexOf("id")
+    val seqColumnIndex = stmt.columnIndexOf("seq")
+    val fromColumnIndex = stmt.columnIndexOf("from")
+    val toColumnIndex = stmt.columnIndexOf("to")
 
     return buildList {
         while (stmt.step()) {
@@ -160,11 +160,11 @@
             return emptyMap()
         }
 
-        val nameIndex = stmt.getColumnIndex("name")
-        val typeIndex = stmt.getColumnIndex("type")
-        val notNullIndex = stmt.getColumnIndex("notnull")
-        val pkIndex = stmt.getColumnIndex("pk")
-        val defaultValueIndex = stmt.getColumnIndex("dflt_value")
+        val nameIndex = stmt.columnIndexOf("name")
+        val typeIndex = stmt.columnIndexOf("type")
+        val notNullIndex = stmt.columnIndexOf("notnull")
+        val pkIndex = stmt.columnIndexOf("pk")
+        val defaultValueIndex = stmt.columnIndexOf("dflt_value")
 
         return buildMap {
             do {
@@ -195,9 +195,9 @@
  */
 private fun readIndices(connection: SQLiteConnection, tableName: String): Set<TableInfo.Index>? {
     connection.prepare("PRAGMA index_list(`$tableName`)").use { stmt ->
-        val nameColumnIndex = stmt.getColumnIndex("name")
-        val originColumnIndex = stmt.getColumnIndex("origin")
-        val uniqueIndex = stmt.getColumnIndex("unique")
+        val nameColumnIndex = stmt.columnIndexOf("name")
+        val originColumnIndex = stmt.columnIndexOf("origin")
+        val uniqueIndex = stmt.columnIndexOf("unique")
         if (nameColumnIndex == -1 || originColumnIndex == -1 || uniqueIndex == -1) {
             // we cannot read them so better not validate any index.
             return null
@@ -228,10 +228,10 @@
     unique: Boolean
 ): TableInfo.Index? {
     return connection.prepare("PRAGMA index_xinfo(`$name`)").use { stmt ->
-        val seqnoColumnIndex = stmt.getColumnIndex("seqno")
-        val cidColumnIndex = stmt.getColumnIndex("cid")
-        val nameColumnIndex = stmt.getColumnIndex("name")
-        val descColumnIndex = stmt.getColumnIndex("desc")
+        val seqnoColumnIndex = stmt.columnIndexOf("seqno")
+        val cidColumnIndex = stmt.columnIndexOf("cid")
+        val nameColumnIndex = stmt.columnIndexOf("name")
+        val descColumnIndex = stmt.columnIndexOf("desc")
         if (
             seqnoColumnIndex == -1 ||
             cidColumnIndex == -1 ||
@@ -265,7 +265,7 @@
     return buildSet {
         connection.prepare("PRAGMA table_info(`$tableName`)").use { stmt ->
             if (!stmt.step()) return@use
-            val nameIndex = stmt.getColumnIndex("name")
+            val nameIndex = stmt.columnIndexOf("name")
             do {
                 add(stmt.getText(nameIndex))
             } while (stmt.step())
@@ -278,7 +278,7 @@
         "SELECT * FROM sqlite_master WHERE `name` = '$tableName'"
     ).use { stmt ->
         if (stmt.step()) {
-            stmt.getText(stmt.getColumnIndex("sql"))
+            stmt.getText(stmt.columnIndexOf("sql"))
         } else {
             ""
         }
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/util/StatementUtil.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/util/StatementUtil.kt
index cebd289..d45b94f 100644
--- a/room/room-runtime/src/commonMain/kotlin/androidx/room/util/StatementUtil.kt
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/util/StatementUtil.kt
@@ -30,7 +30,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
 fun getColumnIndexOrThrow(stmt: SQLiteStatement, name: String): Int {
-    val index: Int = stmt.getColumnIndex(name)
+    val index: Int = stmt.columnIndexOf(name)
     if (index >= 0) {
         return index
     }
@@ -43,13 +43,21 @@
 /**
  * Returns the zero-based index for the given column name, or -1 if the column doesn't exist.
  */
-internal expect fun SQLiteStatement.getColumnIndex(name: String): Int
+internal expect fun SQLiteStatement.columnIndexOf(name: String): Int
 
 // TODO(b/322183292): Consider optimizing by creating a String->Int map, similar to Android
-internal fun SQLiteStatement.columnIndexOf(name: String): Int {
+internal fun SQLiteStatement.columnIndexOfCommon(name: String): Int {
     val columnCount = getColumnCount()
     for (i in 0 until columnCount) {
         if (name == getColumnName(i)) return i
     }
     return -1
 }
+
+/**
+ * Returns the zero-based index for the given column name, or -1 if the column doesn't exist.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+fun getColumnIndex(stmt: SQLiteStatement, name: String): Int {
+    return stmt.columnIndexOf(name)
+}
diff --git a/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/util/StatementUtil.jvmNative.kt b/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/util/StatementUtil.jvmNative.kt
index 437fe5c..96c4bae 100644
--- a/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/util/StatementUtil.jvmNative.kt
+++ b/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/util/StatementUtil.jvmNative.kt
@@ -26,4 +26,4 @@
 /**
  * Returns the zero-based index for the given column name, or -1 if the column doesn't exist.
  */
-internal actual fun SQLiteStatement.getColumnIndex(name: String): Int = columnIndexOf(name)
+internal actual fun SQLiteStatement.columnIndexOf(name: String): Int = columnIndexOfCommon(name)