Preserve type in query result adapters

This CL fixes a bug that happens only in KSP pipeline.

When a dao method returns List<T> where T maps to a primitive in java
(e.g. List<Long>) and non null, Room would end up using a primitive
column row adapter for T. ListQueryResultAdapter previously read its
type argument from the row adapter, which is a problem because the row
adapter has primitive type.

I've updated ListQueryResultAdapter to explicitly receive the type on
construction instead of relying on the row adapter. Same case for
ImmutableList.

Both Java and Guava optional hits a similar issue as well. In both
cases, I changed the row adapter query to also use a nullable type when
querying to ensure that we'll prefer a row adapter that can return null
values. Otherwise, in a case like Optional<Int>, we would return
Optional.of(0) because we would look for a non-null row adapter even
though Optional semantically accepts nullable.

I've also added more tests for Binders. They were not failing without
the change but I decided to keep them.

Unfortunately, some of these tests fail with KAPT until b/146755796 is
fixed. To be able to disable them for kapt, I've added a BuildConfig
field to pass down whether ksp was used during compilation.

Fixes: 177515673
Bug: 146755796
Test: BoxedNonNullTypesTest
Change-Id: I7f5331f6b5b5566bdb17c715d76757e1b6436f82
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt b/room/compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
index 0a3c658..db7c397 100644
--- a/room/compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
@@ -427,21 +427,37 @@
                 // Handle Guava Optional by unpacking its generic type argument and adapting that.
                 // The Optional adapter will reappend the Optional type.
                 val typeArg = typeMirror.typeArguments.first()
-                val rowAdapter = findRowAdapter(typeArg, query) ?: return null
-                return GuavaOptionalQueryResultAdapter(SingleEntityQueryResultAdapter(rowAdapter))
+                // use nullable when finding row adapter as non-null adapters might return
+                // default values
+                val rowAdapter = findRowAdapter(typeArg.makeNullable(), query) ?: return null
+                return GuavaOptionalQueryResultAdapter(
+                    typeArg = typeArg,
+                    resultAdapter = SingleEntityQueryResultAdapter(rowAdapter)
+                )
             } else if (typeMirror.rawType.typeName == CommonTypeNames.OPTIONAL) {
                 // Handle java.util.Optional similarly.
                 val typeArg = typeMirror.typeArguments.first()
-                val rowAdapter = findRowAdapter(typeArg, query) ?: return null
-                return OptionalQueryResultAdapter(SingleEntityQueryResultAdapter(rowAdapter))
+                // use nullable when finding row adapter as non-null adapters might return
+                // default values
+                val rowAdapter = findRowAdapter(typeArg.makeNullable(), query) ?: return null
+                return OptionalQueryResultAdapter(
+                    typeArg = typeArg,
+                    resultAdapter = SingleEntityQueryResultAdapter(rowAdapter)
+                )
             } else if (typeMirror.isTypeOf(ImmutableList::class)) {
                 val typeArg = typeMirror.typeArguments.first().extendsBoundOrSelf()
                 val rowAdapter = findRowAdapter(typeArg, query) ?: return null
-                return ImmutableListQueryResultAdapter(rowAdapter)
+                return ImmutableListQueryResultAdapter(
+                    typeArg = typeArg,
+                    rowAdapter = rowAdapter
+                )
             } else if (typeMirror.isTypeOf(java.util.List::class)) {
                 val typeArg = typeMirror.typeArguments.first().extendsBoundOrSelf()
                 val rowAdapter = findRowAdapter(typeArg, query) ?: return null
-                return ListQueryResultAdapter(rowAdapter)
+                return ListQueryResultAdapter(
+                    typeArg = typeArg,
+                    rowAdapter = rowAdapter
+                )
             }
             return null
         }
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/binderprovider/DataSourceFactoryQueryResultBinderProvider.kt b/room/compiler/src/main/kotlin/androidx/room/solver/binderprovider/DataSourceFactoryQueryResultBinderProvider.kt
index 2e4a047..cf8fddc 100644
--- a/room/compiler/src/main/kotlin/androidx/room/solver/binderprovider/DataSourceFactoryQueryResultBinderProvider.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/binderprovider/DataSourceFactoryQueryResultBinderProvider.kt
@@ -39,7 +39,7 @@
         }
         val typeArg = declared.typeArguments[1]
         val adapter = context.typeAdapterStore.findRowAdapter(typeArg, query)?.let {
-            ListQueryResultAdapter(it)
+            ListQueryResultAdapter(typeArg, it)
         }
 
         val tableNames = (
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/binderprovider/DataSourceQueryResultBinderProvider.kt b/room/compiler/src/main/kotlin/androidx/room/solver/binderprovider/DataSourceQueryResultBinderProvider.kt
index a6803f2..e63b336 100644
--- a/room/compiler/src/main/kotlin/androidx/room/solver/binderprovider/DataSourceQueryResultBinderProvider.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/binderprovider/DataSourceQueryResultBinderProvider.kt
@@ -42,7 +42,7 @@
         }
         val typeArg = declared.typeArguments.last()
         val listAdapter = context.typeAdapterStore.findRowAdapter(typeArg, query)?.let {
-            ListQueryResultAdapter(it)
+            ListQueryResultAdapter(typeArg, it)
         }
         val tableNames = (
             (listAdapter?.accessedTableNames() ?: emptyList()) +
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/binderprovider/PagingSourceQueryResultBinderProvider.kt b/room/compiler/src/main/kotlin/androidx/room/solver/binderprovider/PagingSourceQueryResultBinderProvider.kt
index 18b1b26..d5f93c6 100644
--- a/room/compiler/src/main/kotlin/androidx/room/solver/binderprovider/PagingSourceQueryResultBinderProvider.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/binderprovider/PagingSourceQueryResultBinderProvider.kt
@@ -40,7 +40,7 @@
         }
         val typeArg = declared.typeArguments.last()
         val listAdapter = context.typeAdapterStore.findRowAdapter(typeArg, query)?.let {
-            ListQueryResultAdapter(it)
+            ListQueryResultAdapter(typeArg, it)
         }
         val tableNames = (
             (listAdapter?.accessedTableNames() ?: emptyList()) +
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaOptionalQueryResultAdapter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaOptionalQueryResultAdapter.kt
index 59d2cb2..a76b36e7 100644
--- a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaOptionalQueryResultAdapter.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaOptionalQueryResultAdapter.kt
@@ -16,6 +16,7 @@
 
 package androidx.room.solver.query.result
 
+import androidx.room.compiler.processing.XType
 import androidx.room.ext.GuavaBaseTypeNames
 import androidx.room.ext.L
 import androidx.room.ext.T
@@ -26,9 +27,10 @@
  * Wraps a row adapter when there is only 1 item in the result, and the result's outer type is
  * {@link com.google.common.base.Optional}.
  */
-class GuavaOptionalQueryResultAdapter(private val resultAdapter: SingleEntityQueryResultAdapter) :
-    QueryResultAdapter(resultAdapter.rowAdapter) {
-    val type = resultAdapter.rowAdapter?.out
+class GuavaOptionalQueryResultAdapter(
+    private val typeArg: XType,
+    private val resultAdapter: SingleEntityQueryResultAdapter
+) : QueryResultAdapter(resultAdapter.rowAdapter) {
     override fun convert(
         outVarName: String,
         cursorVarName: String,
@@ -39,7 +41,7 @@
             resultAdapter.convert(valueVarName, cursorVarName, scope)
             addStatement(
                 "final $T $L = $T.fromNullable($L)",
-                ParameterizedTypeName.get(GuavaBaseTypeNames.OPTIONAL, type?.typeName),
+                ParameterizedTypeName.get(GuavaBaseTypeNames.OPTIONAL, typeArg.typeName),
                 outVarName,
                 GuavaBaseTypeNames.OPTIONAL,
                 valueVarName
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/ImmutableListQueryResultAdapter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/ImmutableListQueryResultAdapter.kt
index 9cf42ed..7264e80 100644
--- a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/ImmutableListQueryResultAdapter.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/ImmutableListQueryResultAdapter.kt
@@ -16,6 +16,7 @@
 
 package androidx.room.solver.query.result
 
+import androidx.room.compiler.processing.XType
 import androidx.room.ext.L
 import androidx.room.ext.T
 import androidx.room.solver.CodeGenScope
@@ -23,24 +24,26 @@
 import com.squareup.javapoet.ClassName
 import com.squareup.javapoet.ParameterizedTypeName
 
-class ImmutableListQueryResultAdapter(rowAdapter: RowAdapter) : QueryResultAdapter(rowAdapter) {
-    val type = rowAdapter.out
+class ImmutableListQueryResultAdapter(
+    private val typeArg: XType,
+    rowAdapter: RowAdapter
+) : QueryResultAdapter(rowAdapter) {
     override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
         scope.builder().apply {
             rowAdapter?.onCursorReady(cursorVarName, scope)
             val collectionType = ParameterizedTypeName
-                .get(ClassName.get(ImmutableList::class.java), type.typeName)
+                .get(ClassName.get(ImmutableList::class.java), typeArg.typeName)
             val immutableListBuilderType = ParameterizedTypeName
-                .get(ClassName.get(ImmutableList.Builder::class.java), type.typeName)
+                .get(ClassName.get(ImmutableList.Builder::class.java), typeArg.typeName)
             val immutableListBuilderName = scope.getTmpVar("_immutableListBuilder")
             addStatement(
                 "final $T $L = $T.<$T>builder()",
                 immutableListBuilderType, immutableListBuilderName,
-                ClassName.get(ImmutableList::class.java), type.typeName
+                ClassName.get(ImmutableList::class.java), typeArg.typeName
             )
             val tmpVarName = scope.getTmpVar("_item")
             beginControlFlow("while($L.moveToNext())", cursorVarName).apply {
-                addStatement("final $T $L", type.typeName, tmpVarName)
+                addStatement("final $T $L", typeArg.typeName, tmpVarName)
                 rowAdapter?.convert(tmpVarName, cursorVarName, scope)
                 addStatement("$L.add($L)", immutableListBuilderName, tmpVarName)
             }
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/ListQueryResultAdapter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/ListQueryResultAdapter.kt
index 8767c2b..99e4fa6 100644
--- a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/ListQueryResultAdapter.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/ListQueryResultAdapter.kt
@@ -16,6 +16,7 @@
 
 package androidx.room.solver.query.result
 
+import androidx.room.compiler.processing.XType
 import androidx.room.ext.L
 import androidx.room.ext.T
 import androidx.room.solver.CodeGenScope
@@ -23,22 +24,24 @@
 import com.squareup.javapoet.ParameterizedTypeName
 import java.util.ArrayList
 
-class ListQueryResultAdapter(rowAdapter: RowAdapter) : QueryResultAdapter(rowAdapter) {
-    val type = rowAdapter.out
+class ListQueryResultAdapter(
+    private val typeArg: XType,
+    rowAdapter: RowAdapter
+) : QueryResultAdapter(rowAdapter) {
     override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
         scope.builder().apply {
             rowAdapter?.onCursorReady(cursorVarName, scope)
             val collectionType = ParameterizedTypeName
-                .get(ClassName.get(List::class.java), type.typeName)
+                .get(ClassName.get(List::class.java), typeArg.typeName)
             val arrayListType = ParameterizedTypeName
-                .get(ClassName.get(ArrayList::class.java), type.typeName)
+                .get(ClassName.get(ArrayList::class.java), typeArg.typeName)
             addStatement(
                 "final $T $L = new $T($L.getCount())",
                 collectionType, outVarName, arrayListType, cursorVarName
             )
             val tmpVarName = scope.getTmpVar("_item")
             beginControlFlow("while($L.moveToNext())", cursorVarName).apply {
-                addStatement("final $T $L", type.typeName, tmpVarName)
+                addStatement("final $T $L", typeArg.typeName, tmpVarName)
                 rowAdapter?.convert(tmpVarName, cursorVarName, scope)
                 addStatement("$L.add($L)", outVarName, tmpVarName)
             }
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/OptionalQueryResultAdapter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/OptionalQueryResultAdapter.kt
index ab6ba52..25d6ba2 100644
--- a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/OptionalQueryResultAdapter.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/OptionalQueryResultAdapter.kt
@@ -16,6 +16,7 @@
 
 package androidx.room.solver.query.result
 
+import androidx.room.compiler.processing.XType
 import androidx.room.ext.CommonTypeNames
 import androidx.room.ext.L
 import androidx.room.ext.T
@@ -28,9 +29,10 @@
  *
  * <p>n.b. this will only be useful if the project uses Java 8.
  */
-class OptionalQueryResultAdapter(private val resultAdapter: SingleEntityQueryResultAdapter) :
-    QueryResultAdapter(resultAdapter.rowAdapter) {
-    val type = resultAdapter.rowAdapter?.out
+class OptionalQueryResultAdapter(
+    private val typeArg: XType,
+    private val resultAdapter: SingleEntityQueryResultAdapter
+) : QueryResultAdapter(resultAdapter.rowAdapter) {
     override fun convert(
         outVarName: String,
         cursorVarName: String,
@@ -41,7 +43,7 @@
             resultAdapter.convert(valueVarName, cursorVarName, scope)
             addStatement(
                 "final $T $L = $T.ofNullable($L)",
-                ParameterizedTypeName.get(CommonTypeNames.OPTIONAL, type?.typeName),
+                ParameterizedTypeName.get(CommonTypeNames.OPTIONAL, typeArg.typeName),
                 outVarName,
                 CommonTypeNames.OPTIONAL,
                 valueVarName
diff --git a/room/integration-tests/kotlintestapp/build.gradle b/room/integration-tests/kotlintestapp/build.gradle
index c29c2cc..8b40312 100644
--- a/room/integration-tests/kotlintestapp/build.gradle
+++ b/room/integration-tests/kotlintestapp/build.gradle
@@ -39,6 +39,9 @@
 }
 
 android {
+    buildFeatures {
+        buildConfig = true
+    }
     defaultConfig {
         javaCompileOptions {
             annotationProcessorOptions {
@@ -48,6 +51,7 @@
                 ]
             }
         }
+        buildConfigField("boolean", "KSP", "$useKsp")
     }
 
     sourceSets {
@@ -87,6 +91,7 @@
     implementation(project(":room:room-runtime"))
     implementation(projectOrArtifact(":arch:core:core-runtime"))
     implementation(projectOrArtifact(":lifecycle:lifecycle-livedata"))
+    implementation(projectOrArtifact(":lifecycle:lifecycle-livedata-ktx"))
     implementation(KOTLIN_STDLIB)
     implementation(KOTLIN_COROUTINES_ANDROID)
     // depend on the shadowed version so that it tests with the shipped artifact
@@ -112,6 +117,7 @@
     androidTestImplementation project(':room:room-guava')
     androidTestImplementation project(':room:room-testing')
     androidTestImplementation project(':room:room-rxjava2')
+    androidTestImplementation(project(":room:room-rxjava3"))
     androidTestImplementation(project(":room:room-ktx"))
     androidTestImplementation("androidx.arch.core:core-testing:2.0.1")
     androidTestImplementation(GUAVA_ANDROID)
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/RoomTestConfig.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/RoomTestConfig.kt
new file mode 100644
index 0000000..7895e3c9
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/RoomTestConfig.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.integration.kotlintestapp
+
+import org.junit.AssumptionViolatedException
+
+/**
+ * Helper class to read test configuration via BuildConfig.
+ */
+object RoomTestConfig {
+    /**
+     * true if the test code is built by ksp
+     * This can help enable/disable certain tests for ksp
+     */
+    val isKsp
+        get() = BuildConfig.KSP
+}
+
+/**
+ * Helper method to check if the test was compiled with KSP, and if not, throw an assumption
+ * violation exception to skip the test.
+ */
+fun assumeKsp() {
+    if (!RoomTestConfig.isKsp) {
+        throw AssumptionViolatedException("test is supported only in KSP")
+    }
+}
\ No newline at end of file
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/BoxedNonNullTypesTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/BoxedNonNullTypesTest.kt
new file mode 100644
index 0000000..073e7e5
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/BoxedNonNullTypesTest.kt
@@ -0,0 +1,248 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.integration.kotlintestapp.test
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.asFlow
+import androidx.room.Dao
+import androidx.room.Database
+import androidx.room.Entity
+import androidx.room.Insert
+import androidx.room.PrimaryKey
+import androidx.room.Query
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.integration.kotlintestapp.assumeKsp
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.collect.ImmutableList
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.ListenableFuture
+import io.reactivex.Flowable
+import io.reactivex.Observable
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.Optional
+
+/**
+ * This test matters in KSP specifically where we might use primitive adapter for non-null java
+ * primitives.
+ */
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class BoxedNonNullTypesTest {
+    lateinit var db: MyDb
+
+    @Before
+    fun init() {
+        db = Room.inMemoryDatabaseBuilder(
+            ApplicationProvider.getApplicationContext(),
+            MyDb::class.java
+        ).build()
+    }
+
+    @Test
+    fun list() {
+        db.myDao().insert(MyEntity(3))
+        assertThat(db.myDao().getAsList()).containsExactly(3L)
+    }
+
+    @Test
+    fun list_nullable() {
+        assumeKsp()
+        db.myDao().insert(MyNullableEntity(null), MyNullableEntity(3L))
+        assertThat(db.myDao().getAsNullableList()).containsExactly(null, 3L)
+    }
+
+    @Test
+    fun immutableList() {
+        db.myDao().insert(MyEntity(4))
+        assertThat(db.myDao().getAsImmutableList()).containsExactly(4L)
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 24)
+    fun javaOptional() {
+        assertThat(db.myDao().getAsJavaOptional()).isEqualTo(
+            Optional.empty<Long>()
+        )
+        db.myDao().insert(MyEntity(5))
+        assertThat(db.myDao().getAsJavaOptional()).isEqualTo(
+            Optional.of(5L)
+        )
+    }
+
+    @Test
+    fun guavaOptional() {
+        assertThat(db.myDao().getAsGuavaOptional()).isEqualTo(
+            com.google.common.base.Optional.absent<Long>()
+        )
+        db.myDao().insert(MyEntity(6))
+        assertThat(db.myDao().getAsGuavaOptional()).isEqualTo(
+            com.google.common.base.Optional.of(6L)
+        )
+    }
+
+    @Test
+    fun getAsLiveData() = runBlocking<Unit> {
+        db.myDao().insert(MyEntity(7))
+        assertThat(db.myDao().getAsLiveData().asFlow().first()).isEqualTo(7L)
+    }
+
+    @Test
+    fun getAsLiveData_nullable() = runBlocking<Unit> {
+        assumeKsp()
+        db.myDao().insert(MyNullableEntity(null))
+        assertThat(db.myDao().getAsNullableLiveData().asFlow().first()).isNull()
+    }
+
+    @Test
+    fun getAsFlow() = runBlocking<Unit> {
+        db.myDao().insert(MyEntity(8))
+        assertThat(db.myDao().getAsFlow().first()).isEqualTo(8L)
+    }
+
+    @Test
+    fun getAsFlow_nullable() = runBlocking<Unit> {
+        assumeKsp()
+        db.myDao().insert(MyNullableEntity(null))
+        assertThat(db.myDao().getAsNullableFlow().first()).isNull()
+    }
+
+    @Test
+    fun getAsRx2Observable() {
+        db.myDao().insert(MyEntity(9))
+        assertThat(db.myDao().getAsRx2Observable().blockingFirst()).isEqualTo(9L)
+    }
+
+    @Test
+    fun getAsRx2Flowable() {
+        db.myDao().insert(MyEntity(10))
+        assertThat(db.myDao().getAsRx2Flowable().blockingFirst()).isEqualTo(10L)
+    }
+
+    @Test
+    fun getAsRx3Observable() {
+        db.myDao().insert(MyEntity(11))
+        assertThat(db.myDao().getAsRx3Observable().blockingFirst()).isEqualTo(11L)
+    }
+
+    @Test
+    fun getAsRx3Flowable() {
+        db.myDao().insert(MyEntity(12))
+        assertThat(db.myDao().getAsRx3Flowable().blockingFirst()).isEqualTo(12L)
+    }
+
+    @Test
+    fun getAsListenableFuture() {
+        db.myDao().insert(MyEntity(13))
+        assertThat(
+            db.myDao().getAsListenableFuture().get()
+        ).isEqualTo(13L)
+    }
+
+    @Test
+    fun getAsListenableFuture_nullable() {
+        assumeKsp()
+        db.myDao().insert(MyNullableEntity(null))
+        assertThat(
+            db.myDao().getAsNullableListenableFuture().get()
+        ).isEqualTo(null)
+    }
+
+    @Entity
+    data class MyEntity(
+        val value: Long,
+        @PrimaryKey(autoGenerate = true) val id: Int = 0,
+    )
+
+    @Entity
+    data class MyNullableEntity(
+        val value: Long?,
+        @PrimaryKey(autoGenerate = true) val id: Int = 0,
+    )
+
+    @Database(
+        entities = [MyEntity::class, MyNullableEntity::class],
+        version = 1,
+        exportSchema = false
+    )
+    abstract class MyDb : RoomDatabase() {
+        abstract fun myDao(): MyDao
+    }
+
+    @Dao
+    interface MyDao {
+        @Query("SELECT value FROM MyEntity")
+        fun getAsList(): List<Long>
+
+        @Query("SELECT value FROM MyNullableEntity")
+        fun getAsNullableList(): List<Long?>
+
+        // immutable list does not allow nulls, hence no nullable test for it
+        @Query("SELECT value FROM MyEntity")
+        fun getAsImmutableList(): ImmutableList<Long>
+
+        @Query("SELECT value FROM MyEntity LIMIT 1")
+        fun getAsJavaOptional(): Optional<Long>
+
+        @Query("SELECT value FROM MyEntity LIMIT 1")
+        fun getAsGuavaOptional(): com.google.common.base.Optional<Long>
+
+        @Query("SELECT value FROM MyEntity LIMIT 1")
+        fun getAsLiveData(): LiveData<Long>
+
+        @Query("SELECT value FROM MyNullableEntity LIMIT 1")
+        fun getAsNullableLiveData(): LiveData<Long?>
+
+        @Query("SELECT value FROM MyEntity LIMIT 1")
+        fun getAsFlow(): Flow<Long>
+
+        @Query("SELECT value FROM MyNullableEntity LIMIT 1")
+        fun getAsNullableFlow(): Flow<Long?>
+
+        @Query("SELECT value FROM MyEntity LIMIT 1")
+        fun getAsRx2Observable(): Observable<Long>
+
+        @Query("SELECT value FROM MyEntity LIMIT 1")
+        fun getAsRx2Flowable(): Flowable<Long>
+
+        @Query("SELECT value FROM MyEntity LIMIT 1")
+        fun getAsRx3Observable(): io.reactivex.rxjava3.core.Observable<Long>
+
+        @Query("SELECT value FROM MyEntity LIMIT 1")
+        fun getAsRx3Flowable(): io.reactivex.rxjava3.core.Flowable<Long>
+
+        @Query("SELECT value FROM MyEntity LIMIT 1")
+        fun getAsListenableFuture(): ListenableFuture<Long>
+
+        @Query("SELECT value FROM MyNullableEntity LIMIT 1")
+        fun getAsNullableListenableFuture(): ListenableFuture<Long?>
+
+        @Insert
+        fun insert(vararg entities: MyEntity)
+
+        @Insert
+        fun insert(vararg entities: MyNullableEntity)
+    }
+}
\ No newline at end of file