Merge "Add alternative TypeConverterStore for KSP" into androidx-main
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt b/buildSrc/private/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt
index b04ea44..074b07d 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt
@@ -185,6 +185,7 @@
     "lintVitalDebug",
     "lintWithExpandProjectionDebug",
     "lintWithoutExpandProjectionDebug",
+    "lintWithNullAwareTypeConverterDebug",
     "lintWithKaptDebug",
     "lintWithKspDebug",
 
diff --git a/room/integration-tests/kotlintestapp/src/androidTestWithKsp/java/androidx/room/integration/kotlintestapp/NullabilityAwareTypeConversionTest.kt b/room/integration-tests/kotlintestapp/src/androidTestWithKsp/java/androidx/room/integration/kotlintestapp/NullabilityAwareTypeConversionTest.kt
new file mode 100644
index 0000000..85c86ea
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTestWithKsp/java/androidx/room/integration/kotlintestapp/NullabilityAwareTypeConversionTest.kt
@@ -0,0 +1,357 @@
+/*
+ * 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 android.database.Cursor
+import androidx.room.Dao
+import androidx.room.Database
+import androidx.room.Entity
+import androidx.room.Insert
+import androidx.room.PrimaryKey
+import androidx.room.ProvidedTypeConverter
+import androidx.room.Query
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.TypeConverter
+import androidx.room.TypeConverters
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * This test can only pass in KSP with the new type converter store, which is why it is only in the
+ * KSP specific source set.
+ */
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class NullabilityAwareTypeConversionTest {
+    lateinit var dao: UserDao
+    private val nullableConvertors = NullableTypeConverters()
+
+    @Before
+    fun init() {
+        dao = Room.inMemoryDatabaseBuilder(
+            ApplicationProvider.getApplicationContext(),
+            NullAwareConverterDatabase::class.java
+        ).addTypeConverter(nullableConvertors).build().userDao
+    }
+
+    private fun assertNullableConverterIsNotUsed() {
+        assertWithMessage(
+            "should've not used nullable conversion since it is not available in this scope"
+        ).that(nullableConvertors.toStringInvocations).isEmpty()
+        assertWithMessage(
+            "should've not used nullable conversion since it is not available in this scope"
+        ).that(nullableConvertors.fromStringInvocations).isEmpty()
+    }
+
+    @Test
+    fun insert() {
+        val user = User(
+            id = 1,
+            nonNullCountry = Country.FRANCE,
+            nullableCountry = Country.UNITED_KINGDOM
+        )
+        dao.insert(user)
+        assertThat(
+            dao.getRawData()
+        ).isEqualTo("1-FR-UK")
+        assertNullableConverterIsNotUsed()
+    }
+
+    @Test
+    fun setNonNullColumn() {
+        val user = User(
+            id = 1,
+            nonNullCountry = Country.FRANCE,
+            nullableCountry = null
+        )
+        dao.insert(user)
+        assertThat(
+            dao.getRawData()
+        ).isEqualTo("1-FR-null")
+        dao.setNonNullCountry(id = 1, nonNullCountry = Country.UNITED_KINGDOM)
+        assertThat(
+            dao.getRawData()
+        ).isEqualTo("1-UK-null")
+        assertNullableConverterIsNotUsed()
+    }
+
+    @Test
+    fun setNullableColumn() {
+        val user = User(
+            id = 1,
+            nonNullCountry = Country.FRANCE,
+            nullableCountry = Country.UNITED_KINGDOM
+        )
+        dao.insert(user)
+        dao.setNullableCountry(id = 1, nullableCountry = null)
+        assertThat(
+            dao.getRawData()
+        ).isEqualTo("1-FR-null")
+        dao.setNullableCountry(id = 1, nullableCountry = Country.UNITED_KINGDOM)
+        assertThat(
+            dao.getRawData()
+        ).isEqualTo("1-FR-UK")
+        assertNullableConverterIsNotUsed()
+    }
+
+    @Test
+    fun load() {
+        val user1 = User(
+            id = 1,
+            nonNullCountry = Country.FRANCE,
+            nullableCountry = Country.UNITED_KINGDOM
+        )
+        dao.insert(user1)
+        assertThat(
+            dao.getById(1)
+        ).isEqualTo(user1)
+        val user2 = User(
+            id = 2,
+            nonNullCountry = Country.UNITED_KINGDOM,
+            nullableCountry = null
+        )
+        dao.insert(user2)
+        assertThat(
+            dao.getById(2)
+        ).isEqualTo(user2)
+        assertNullableConverterIsNotUsed()
+    }
+
+    @Test
+    fun useNullableConverter() {
+        val user = User(
+            id = 1,
+            nonNullCountry = Country.FRANCE,
+            nullableCountry = Country.UNITED_KINGDOM
+        )
+        dao.insert(user)
+        dao.setNullableCountryWithNullableTypeConverter(
+            id = 1,
+            nullableCountry = null
+        )
+        assertThat(
+            dao.getRawData()
+        ).isEqualTo("1-FR-null")
+        assertThat(
+            nullableConvertors.toStringInvocations
+        ).containsExactly(null)
+    }
+
+    @Test
+    fun loadNonNullColumn() {
+        val user = User(
+            id = 1,
+            nonNullCountry = Country.FRANCE,
+            nullableCountry = null
+        )
+        dao.insert(user)
+        val country = dao.getNonNullCountry(id = 1)
+        assertThat(country).isEqualTo(Country.FRANCE)
+        assertNullableConverterIsNotUsed()
+    }
+
+    @Test
+    fun loadNullableColumn() {
+        val user = User(
+            id = 1,
+            nonNullCountry = Country.FRANCE,
+            nullableCountry = null
+        )
+        dao.insert(user)
+        val country = dao.getNullableCountry(id = 1)
+        assertThat(country).isNull()
+        assertNullableConverterIsNotUsed()
+    }
+
+    @Test
+    fun loadNonNullColumn_withNullableConverter() {
+        val user = User(
+            id = 1,
+            nonNullCountry = Country.FRANCE,
+            nullableCountry = null
+        )
+        dao.insert(user)
+        val country = dao.getNonNullCountryWithNullableTypeConverter(id = 1)
+        assertThat(country).isEqualTo(Country.FRANCE)
+        // return value is non-null so it is better to use non-null converter and assume
+        // column is non-null, instead of using the nullable converter
+        assertNullableConverterIsNotUsed()
+    }
+
+    @Test
+    fun loadNonNullColumn_asNullable_withNullableConverter() {
+        val user = User(
+            id = 1,
+            nonNullCountry = Country.FRANCE,
+            nullableCountry = null
+        )
+        dao.insert(user)
+        val country = dao.getNonNullCountryAsNullableWithNullableTypeConverter(id = 1)
+        assertThat(country).isEqualTo(Country.FRANCE)
+        // return value is nullable so we are using the nullable converter because room does not
+        // know that the column is non-null.
+        // if one day Room understands it and this test fails, feel free to update it.
+        // We still want this test because right now Room does not know column is non-null hence
+        // it should prefer the nullable converter.
+        assertThat(
+            nullableConvertors.fromStringInvocations
+        ).containsExactly("FR")
+    }
+
+    @Test
+    fun loadNullableColumn_withNullableConverter() {
+        val user = User(
+            id = 1,
+            nonNullCountry = Country.FRANCE,
+            nullableCountry = null
+        )
+        dao.insert(user)
+        val country = dao.getNullableCountryWithNullableTypeConverter(id = 1)
+        assertThat(country).isNull()
+        assertThat(
+            nullableConvertors.fromStringInvocations
+        ).containsExactly(null)
+    }
+
+    @Database(
+        version = 1,
+        entities = [
+            User::class,
+        ],
+        exportSchema = false
+    )
+    @TypeConverters(NonNullTypeConverters::class)
+    abstract class NullAwareConverterDatabase : RoomDatabase() {
+        abstract val userDao: UserDao
+    }
+
+    @Dao
+    abstract class UserDao {
+
+        @Insert
+        abstract fun insert(user: User): Long
+
+        @Query("UPDATE user SET nonNullCountry = :nonNullCountry WHERE id = :id")
+        abstract fun setNonNullCountry(id: Long, nonNullCountry: Country)
+
+        @Query("UPDATE user SET nullableCountry = :nullableCountry WHERE id = :id")
+        abstract fun setNullableCountry(id: Long, nullableCountry: Country?)
+
+        @Query("SELECT * FROM user WHERE id = :id")
+        abstract fun getById(id: Long): User?
+
+        @Query("UPDATE user SET nullableCountry = :nullableCountry WHERE id = :id")
+        @TypeConverters(NullableTypeConverters::class)
+        abstract fun setNullableCountryWithNullableTypeConverter(
+            id: Long,
+            nullableCountry: Country?
+        )
+
+        @Query("SELECT nonNullCountry FROM user WHERE id = :id")
+        abstract fun getNonNullCountry(id: Long): Country
+
+        @Query("SELECT nullableCountry FROM user WHERE id = :id")
+        abstract fun getNullableCountry(id: Long): Country?
+
+        @Query("SELECT nullableCountry FROM user WHERE id = :id")
+        @TypeConverters(NullableTypeConverters::class)
+        abstract fun getNullableCountryWithNullableTypeConverter(id: Long): Country?
+
+        @Query("SELECT nonNullCountry FROM user WHERE id = :id")
+        @TypeConverters(NullableTypeConverters::class)
+        abstract fun getNonNullCountryWithNullableTypeConverter(id: Long): Country
+
+        @Query("SELECT nonNullCountry FROM user WHERE id = :id")
+        @TypeConverters(NullableTypeConverters::class)
+        abstract fun getNonNullCountryAsNullableWithNullableTypeConverter(id: Long): Country?
+
+        @Query("SELECT * FROM User ORDER BY id")
+        protected abstract fun getUsers(): Cursor
+
+        /**
+         * Return raw data in the database so that we can assert what is in the database
+         * without room's converters
+         */
+        fun getRawData(): String {
+            return buildString {
+                getUsers().use {
+                    if (it.moveToNext()) {
+                        append(it.getInt(0))
+                        append("-")
+                        append(it.getString(1))
+                        append("-")
+                        append(it.getString(2))
+                    }
+                }
+            }
+        }
+    }
+
+    @Entity(tableName = "user")
+    data class User(
+        @PrimaryKey
+        val id: Long,
+        val nonNullCountry: Country,
+        val nullableCountry: Country?,
+    )
+
+    enum class Country(val countryCode: String) {
+        UNITED_KINGDOM("UK"),
+        FRANCE("FR"),
+    }
+
+    object NonNullTypeConverters {
+        @TypeConverter
+        fun toString(country: Country): String {
+            return country.countryCode
+        }
+
+        @TypeConverter
+        fun toCountry(string: String): Country {
+            return Country.values().find { it.countryCode == string }
+                ?: throw IllegalArgumentException("Country code '$string' not found")
+        }
+    }
+
+    @ProvidedTypeConverter
+    class NullableTypeConverters {
+        val toStringInvocations = mutableListOf<Country?>()
+        val fromStringInvocations = mutableListOf<String?>()
+        @TypeConverter
+        fun toString(country: Country?): String? {
+            toStringInvocations.add(country)
+            return country?.countryCode
+        }
+
+        @TypeConverter
+        fun toCountry(string: String?): Country? {
+            fromStringInvocations.add(string)
+            if (string == null) {
+                return null
+            }
+            return Country.values().find { it.countryCode == string }
+                ?: throw IllegalArgumentException("Country code '$string' not found")
+        }
+    }
+}
diff --git a/room/integration-tests/testapp/build.gradle b/room/integration-tests/testapp/build.gradle
index 17cf821..358a6e5 100644
--- a/room/integration-tests/testapp/build.gradle
+++ b/room/integration-tests/testapp/build.gradle
@@ -68,6 +68,18 @@
                 }
             }
         }
+        withNullAwareTypeConverter {
+            dimension "processorConfiguration"
+            javaCompileOptions {
+                annotationProcessorOptions {
+                    arguments = [
+                            "room.schemaLocation"  : "$projectDir/schemas".toString(),
+                            "room.expandProjection"  : "false",
+                            "room.useNullAwareTypeAnalysis": "true"
+                    ]
+                }
+            }
+        }
     }
 }
 
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/RoomKspProcessor.kt b/room/room-compiler/src/main/kotlin/androidx/room/RoomKspProcessor.kt
index 7437810..0733517 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/RoomKspProcessor.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/RoomKspProcessor.kt
@@ -17,9 +17,12 @@
 package androidx.room
 
 import androidx.room.compiler.processing.ksp.KspBasicAnnotationProcessor
+import androidx.room.processor.Context.BooleanProcessorOptions.USE_NULL_AWARE_CONVERTER
+import androidx.room.processor.ProcessorErrors
 import com.google.devtools.ksp.processing.SymbolProcessor
 import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
 import com.google.devtools.ksp.processing.SymbolProcessorProvider
+import javax.tools.Diagnostic
 
 /**
  * Entry point for processing using KSP.
@@ -27,7 +30,22 @@
 class RoomKspProcessor(
     environment: SymbolProcessorEnvironment
 ) : KspBasicAnnotationProcessor(environment) {
-
+    init {
+        // print a warning if null aware converter is disabled because we'll remove that ability
+        // soon.
+        if (USE_NULL_AWARE_CONVERTER.getInputValue(xProcessingEnv) == false) {
+            xProcessingEnv.messager.printMessage(
+                kind = Diagnostic.Kind.WARNING,
+                msg = """
+                    Disabling null-aware type analysis in KSP is a temporary flag that will be
+                    removed in a future release.
+                    If the null-aware type analysis is causing a bug in your application,
+                    please file a bug at ${ProcessorErrors.ISSUE_TRACKER_LINK} with
+                    a sample app that reproduces your problem.
+                """.trimIndent()
+            )
+        }
+    }
     override fun processingSteps() = listOf(
         DatabaseProcessingStep()
     )
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/Context.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/Context.kt
index 537ceff..791a90e 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/Context.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/Context.kt
@@ -41,6 +41,15 @@
     val checker: Checks = Checks(logger)
     val COMMON_TYPES = CommonTypes(processingEnv)
 
+    /**
+     * Checks whether we should use the TypeConverter store that has a specific heuristic for
+     * nullability. Defaults to true in KSP, false in javac.
+     */
+    val useNullAwareConverter: Boolean by lazy {
+        BooleanProcessorOptions.USE_NULL_AWARE_CONVERTER.getInputValue(processingEnv)
+            ?: (processingEnv.backend == XProcessingEnv.Backend.KSP)
+    }
+
     val typeAdapterStore by lazy {
         if (inheritedAdapterStore != null) {
             TypeAdapterStore.copy(this, inheritedAdapterStore)
@@ -207,19 +216,21 @@
 
     enum class BooleanProcessorOptions(val argName: String, private val defaultValue: Boolean) {
         INCREMENTAL("room.incremental", defaultValue = true),
-        EXPAND_PROJECTION("room.expandProjection", defaultValue = false);
+        EXPAND_PROJECTION("room.expandProjection", defaultValue = false),
+        USE_NULL_AWARE_CONVERTER("room.useNullAwareTypeAnalysis", defaultValue = false);
 
         /**
          * Returns the value of this option passed through the [XProcessingEnv]. If the value
          * is null or blank, it returns the default value instead.
          */
         fun getValue(processingEnv: XProcessingEnv): Boolean {
-            val value = processingEnv.options[argName]
-            return if (value.isNullOrBlank()) {
-                defaultValue
-            } else {
-                value.toBoolean()
-            }
+            return getInputValue(processingEnv) ?: defaultValue
+        }
+
+        fun getInputValue(processingEnv: XProcessingEnv): Boolean? {
+            return processingEnv.options[argName]?.takeIf {
+                it.isNotBlank()
+            }?.toBoolean()
         }
     }
 }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/NullAwareTypeConverterStore.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/NullAwareTypeConverterStore.kt
new file mode 100644
index 0000000..95e4a8f
--- /dev/null
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/NullAwareTypeConverterStore.kt
@@ -0,0 +1,429 @@
+/*
+ * 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.solver
+
+import androidx.room.compiler.processing.XNullability
+import androidx.room.compiler.processing.XNullability.NONNULL
+import androidx.room.compiler.processing.XNullability.NULLABLE
+import androidx.room.compiler.processing.XNullability.UNKNOWN
+import androidx.room.compiler.processing.XProcessingEnv.Backend
+import androidx.room.compiler.processing.XType
+import androidx.room.processor.Context
+import androidx.room.solver.types.CompositeTypeConverter
+import androidx.room.solver.types.NoOpConverter
+import androidx.room.solver.types.NullSafeTypeConverter
+import androidx.room.solver.types.RequireNotNullTypeConverter
+import androidx.room.solver.types.TypeConverter
+import androidx.room.solver.types.UpCastTypeConverter
+import java.util.PriorityQueue
+
+/**
+ * A [TypeConverterStore] implementation that generates better code when we have the nullability
+ * information in types. It is enabled by default only in KSP backend but it can also be turned
+ * on via the [Context.BooleanProcessorOptions.USE_NULL_AWARE_CONVERTER] flag.
+ *
+ * This [TypeConverterStore] tries to maintain the nullability of the input/output type
+ * when writing into/reading from database. Even though nullability preservation is preferred, it is
+ * not strictly required such that it will fall back to the mismatched nullability.
+ */
+class NullAwareTypeConverterStore(
+    context: Context,
+    /**
+     * Available TypeConverters. Note that we might synthesize new type converters based on this
+     * list.
+     */
+    typeConverters: List<TypeConverter>,
+    /**
+     * List of types that can be saved into db/read from without a converter.
+     */
+    private val knownColumnTypes: List<XType>
+) : TypeConverterStore {
+    override val typeConverters = if (context.processingEnv.backend == Backend.KSP) {
+        val processedConverters = typeConverters.toMutableList()
+        // create copies for converters that receive non-null values
+        typeConverters.forEach { converter ->
+            if (converter.from.nullability == NONNULL) {
+                val candidate = NullSafeTypeConverter(delegate = converter)
+                // before we add this null safe converter, make sure there is no other converter
+                // that would already handle the same arguments.
+                val match = processedConverters.any { other ->
+                    other.from.isAssignableFrom(candidate.from) &&
+                        candidate.to.isAssignableFrom(other.to)
+                }
+                if (!match) {
+                    processedConverters.add(candidate)
+                }
+            }
+        }
+        processedConverters
+    } else {
+        typeConverters
+    }
+
+    // cache for type converter lookups to avoid traversing all of the list every time we need to
+    // find possible converters for a type. Unlike JAVAC, KSP supports equality in its objects so
+    // this tends to work rather well.
+    private val typeConvertersByFromCache = mutableMapOf<XType, List<TypeConverter>>()
+    private val typeConvertersByToCache = mutableMapOf<XType, List<TypeConverter>>()
+
+    /**
+     * Known column types that are nullable.
+     * Used in [getColumnTypesInPreferenceBuckets] to avoid re-partitioning known type lists.
+     */
+    private val knownNullableColumnTypes by lazy {
+        knownColumnTypes.filter { it.nullability == NULLABLE }
+    }
+
+    /**
+     * Known column types that are non-null or have unknown nullability.
+     * Used in [getColumnTypesInPreferenceBuckets] to avoid re-partitioning known type lists.
+     */
+    private val knownNonNullableColumnTypes by lazy {
+        knownColumnTypes.filter { it.nullability != NULLABLE }
+    }
+
+    /**
+     * Returns a list of lists for the given type, ordered by preference buckets for
+     * the given nullability.
+     */
+    private fun getColumnTypesInPreferenceBuckets(
+        nullability: XNullability,
+        explicitColumnTypes: List<XType>?
+    ): List<List<XType>> {
+        return if (explicitColumnTypes == null) {
+            when (nullability) {
+                NULLABLE -> {
+                    // prioritize nulls
+                    listOf(
+                        knownNullableColumnTypes,
+                        knownNonNullableColumnTypes
+                    )
+                }
+                NONNULL -> {
+                    // prioritize non-null
+                    listOf(
+                        knownNonNullableColumnTypes,
+                        knownNullableColumnTypes
+                    )
+                }
+                else -> {
+                    // we don't know, YOLO
+                    listOf(knownColumnTypes)
+                }
+            }
+        } else {
+            when (nullability) {
+                UNKNOWN -> listOf(explicitColumnTypes)
+                else -> listOf(
+                    explicitColumnTypes.filter { it.nullability == nullability },
+                    explicitColumnTypes.filter { it.nullability != nullability }
+                )
+            }
+        }
+    }
+
+    override fun findConverterIntoStatement(
+        input: XType,
+        columnTypes: List<XType>?
+    ): TypeConverter? {
+        getColumnTypesInPreferenceBuckets(
+            nullability = input.nullability,
+            explicitColumnTypes = columnTypes
+        ).forEach { types ->
+            findConverterIntoStatementInternal(
+                input = input,
+                columnTypes = types
+            )?.getOrCreateConverter()?.let {
+                return it
+            }
+        }
+        return null
+    }
+
+    private fun findConverterIntoStatementInternal(
+        input: XType,
+        columnTypes: List<XType>
+    ): TypeConverterEntry? {
+        if (columnTypes.isEmpty()) return null
+        val queue = TypeConverterQueue(
+            sourceType = input,
+            // each converter is keyed on which type they will take us to
+            keyType = TypeConverter::to
+        )
+
+        while (true) {
+            val current = queue.next() ?: break
+            val match = columnTypes.any { columnType ->
+                columnType.isSameType(current.type)
+            }
+            if (match) {
+                return current
+            }
+            // check for assignable matches but only enqueue them as there might be another shorter
+            // path
+            columnTypes.forEach { columnType ->
+                if (columnType.isAssignableFrom(current.type)) {
+                    queue.maybeEnqueue(
+                        current.appendConverter(
+                            UpCastTypeConverter(
+                                upCastFrom = current.type,
+                                upCastTo = columnType
+                            )
+                        )
+                    )
+                }
+            }
+            getAllTypeConvertersFrom(current.type).forEach {
+                queue.maybeEnqueue(current.appendConverter(it))
+            }
+        }
+        return null
+    }
+
+    override fun findConverterFromCursor(
+        columnTypes: List<XType>?,
+        output: XType
+    ): TypeConverter? {
+        @Suppress("NAME_SHADOWING") // intentional
+        val columnTypes = columnTypes ?: knownColumnTypes
+        // prefer nullable when reading from database, regardless of the output type
+        getColumnTypesInPreferenceBuckets(
+            nullability = NULLABLE,
+            explicitColumnTypes = columnTypes
+        ).forEach { types ->
+            findConverterFromCursorInternal(
+                columnTypes = types,
+                output = output
+            )?.let {
+                return it.getOrCreateConverter()
+            }
+        }
+
+        // if type is non-null, try to find nullable and add null check
+        return if (output.nullability == NONNULL) {
+            findConverterFromCursorInternal(
+                columnTypes = columnTypes,
+                output = output.makeNullable()
+            )?.appendConverter(
+                RequireNotNullTypeConverter(
+                    from = output.makeNullable()
+                )
+            )
+        } else {
+            null
+        }
+    }
+
+    private fun findConverterFromCursorInternal(
+        columnTypes: List<XType>,
+        output: XType
+    ): TypeConverterEntry? {
+        if (columnTypes.isEmpty()) return null
+        val queue = TypeConverterQueue(
+            sourceType = output,
+            // each converter is keyed on which type they receive as we are doing pathfinding
+            // reverse here
+            keyType = TypeConverter::from
+        )
+
+        while (true) {
+            val current = queue.next() ?: break
+            val match = columnTypes.any { columnType ->
+                columnType.isSameType(current.type)
+            }
+            if (match) {
+                return current
+            }
+            // check for assignable matches but only enqueue them as there might be another shorter
+            // path
+            columnTypes.forEach { columnType ->
+                if (current.type.isAssignableFrom(columnType)) {
+                    queue.maybeEnqueue(
+                        current.prependConverter(
+                            UpCastTypeConverter(
+                                upCastFrom = columnType,
+                                upCastTo = current.type
+                            )
+                        )
+                    )
+                }
+            }
+            getAllTypeConvertersTo(current.type).forEach {
+                queue.maybeEnqueue(current.prependConverter(it))
+            }
+        }
+        return null
+    }
+
+    override fun findTypeConverter(input: XType, output: XType): TypeConverter? {
+        return findConverterIntoStatementInternal(
+            input = input,
+            columnTypes = listOf(output)
+        )?.getOrCreateConverter()
+    }
+
+    /**
+     * Returns all type converters that can receive input type and return into another type.
+     */
+    private fun getAllTypeConvertersFrom(
+        input: XType
+    ): List<TypeConverter> {
+        // for input, check assignability because it defines whether we can use the method or not.
+        return typeConvertersByFromCache.getOrPut(input) {
+            // this cache avoids us many assignability checks.
+            typeConverters.mapNotNull { converter ->
+                when {
+                    converter.from.isSameType(input) -> converter
+                    converter.from.isAssignableFrom(input) -> CompositeTypeConverter(
+                        conv1 = UpCastTypeConverter(
+                            upCastFrom = input,
+                            upCastTo = converter.from
+                        ),
+                        conv2 = converter
+                    )
+                    else -> null
+                }
+            }
+        }
+    }
+
+    /**
+     * Returns all type converters that can return the output type.
+     */
+    private fun getAllTypeConvertersTo(
+        output: XType
+    ): List<TypeConverter> {
+        return typeConvertersByToCache.getOrPut(output) {
+            // this cache avoids us many assignability checks.
+            typeConverters.mapNotNull { converter ->
+                when {
+                    converter.to.isSameType(output) -> converter
+                    output.isAssignableFrom(converter.to) -> CompositeTypeConverter(
+                        conv1 = converter,
+                        conv2 = UpCastTypeConverter(
+                            upCastFrom = converter.to,
+                            upCastTo = output
+                        )
+                    )
+                    else -> null
+                }
+            }
+        }
+    }
+
+    /**
+     * Priority queue for the type converter search.
+     */
+    private class TypeConverterQueue(
+        sourceType: XType,
+        val keyType: TypeConverter.() -> XType
+    ) {
+        // using insertion order as the tie breaker for reproducible builds.
+        private var insertionOrder = 0
+
+        // map of XType to the converter that includes the path from the source type to the XType.
+        private val cheapestEntry = mutableMapOf<XType, TypeConverterEntry>()
+        private val queue = PriorityQueue<TypeConverterEntry>()
+
+        init {
+            val typeConverterEntry = TypeConverterEntry(
+                tieBreakerPriority = insertionOrder++,
+                type = sourceType,
+                converter = null
+            )
+            cheapestEntry[sourceType] = typeConverterEntry
+            queue.add(typeConverterEntry)
+        }
+
+        fun next(): TypeConverterEntry? {
+            while (queue.isNotEmpty()) {
+                val entry = queue.remove()
+                // check if we processed this type as there is no reason to process it again
+                if (cheapestEntry[entry.type] !== entry) {
+                    continue
+                }
+                return entry
+            }
+            return null
+        }
+
+        /**
+         * Enqueues the given [converter] if its target type (defined by [keyType]) is not visited
+         * or visited with a more expensive converter.
+         */
+        fun maybeEnqueue(
+            converter: TypeConverter
+        ): Boolean {
+            val keyType = converter.keyType()
+            val existing = cheapestEntry[keyType]
+            if (existing == null ||
+                (existing.converter != null && existing.converter.cost > converter.cost)
+            ) {
+                val entry = TypeConverterEntry(insertionOrder++, keyType, converter)
+                cheapestEntry[keyType] = entry
+                queue.add(entry)
+                return true
+            }
+            return false
+        }
+    }
+
+    private data class TypeConverterEntry(
+        // when costs are equal, tieBreakerPriority is used
+        val tieBreakerPriority: Int,
+        val type: XType,
+        val converter: TypeConverter?
+    ) : Comparable<TypeConverterEntry> {
+        override fun compareTo(other: TypeConverterEntry): Int {
+            if (converter == null) {
+                if (other.converter != null) {
+                    return -1
+                }
+            } else if (other.converter == null) {
+                return 1
+            } else {
+                val costCmp = converter.cost.compareTo(other.converter.cost)
+                if (costCmp != 0) {
+                    return costCmp
+                }
+            }
+            return tieBreakerPriority.compareTo(other.tieBreakerPriority)
+        }
+
+        fun getOrCreateConverter() = converter ?: NoOpConverter(type)
+
+        fun appendConverter(nextConverter: TypeConverter): TypeConverter {
+            if (converter == null) {
+                return nextConverter
+            }
+            return CompositeTypeConverter(
+                conv1 = converter,
+                conv2 = nextConverter
+            )
+        }
+
+        fun prependConverter(previous: TypeConverter): TypeConverter {
+            if (converter == null) {
+                return previous
+            }
+            return CompositeTypeConverter(
+                conv1 = previous,
+                conv2 = converter
+            )
+        }
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
index 937ec76..d67bc34 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
@@ -172,11 +172,14 @@
             ByteArrayColumnTypeAdapter.create(context.processingEnv).forEach(::addColumnAdapter)
             ByteBufferColumnTypeAdapter.create(context.processingEnv).forEach(::addColumnAdapter)
             PrimitiveBooleanToIntConverter.create(context.processingEnv).forEach(::addTypeConverter)
+            // null aware converter is able to automatically null wrap converters so we don't
+            // need this as long as we are running in KSP
             BoxedBooleanToBoxedIntConverter.create(context.processingEnv)
                 .forEach(::addTypeConverter)
             return TypeAdapterStore(
                 context = context, columnTypeAdapters = adapters,
-                typeConverterStore = TypeConverterStore(
+                typeConverterStore = TypeConverterStore.create(
+                    context = context,
                     typeConverters = converters,
                     knownColumnTypes = adapters.map { it.out }
                 ),
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeConverterStore.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeConverterStore.kt
index fbd8863..0b7d0af 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeConverterStore.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeConverterStore.kt
@@ -17,49 +17,34 @@
 package androidx.room.solver
 
 import androidx.room.compiler.processing.XType
+import androidx.room.processor.Context
 import androidx.room.solver.types.CompositeTypeConverter
 import androidx.room.solver.types.NoOpConverter
+import androidx.room.solver.types.RequireNotNullTypeConverter
 import androidx.room.solver.types.TypeConverter
-import java.util.LinkedList
+import androidx.room.solver.types.UpCastTypeConverter
 
-/**
- * Common logic that handles conversion between types either using built in converters or user
- * provided type converters.
- */
-class TypeConverterStore(
-    /**
-     * Available TypeConverters
-     */
-    private val typeConverters: List<TypeConverter>,
-    /**
-     * List of types that can be saved into db/read from without a converter.
-     */
-    private val knownColumnTypes: List<XType>
-) {
+interface TypeConverterStore {
+    val typeConverters: List<TypeConverter>
+
     /**
      * Finds a [TypeConverter] (might be composite) that can convert the given [input] type into
      * one of the given [columnTypes]. If [columnTypes] is not specified, targets all
-     * [knownColumnTypes].
+     * `knownColumnTypes`.
      */
     fun findConverterIntoStatement(
         input: XType,
-        columnTypes: List<XType>? = null
-    ) = findTypeConverter(
-        inputs = listOf(input),
-        outputs = columnTypes ?: knownColumnTypes
-    )
+        columnTypes: List<XType>?
+    ): TypeConverter?
 
     /**
      * Finds a [TypeConverter] (might be composite) that can convert the given [columnTypes] into
-     * the [output] type. If [columnTypes] is not specified, uses all [knownColumnTypes].
+     * the [output] type. If [columnTypes] is not specified, uses all `knownColumnTypes`.
      */
     fun findConverterFromCursor(
-        columnTypes: List<XType>? = null,
+        columnTypes: List<XType>?,
         output: XType
-    ) = findTypeConverter(
-        inputs = columnTypes ?: knownColumnTypes,
-        outputs = listOf(output)
-    )
+    ): TypeConverter?
 
     /**
      * Finds a [TypeConverter] from [input] to [output].
@@ -67,104 +52,8 @@
     fun findTypeConverter(
         input: XType,
         output: XType
-    ) = findTypeConverter(
-        inputs = listOf(input),
-        outputs = listOf(output)
-    )
+    ): TypeConverter?
 
-    /**
-     * Finds a type converter that can convert one of the input values to one of the output values.
-     *
-     * When multiple conversion paths are possible, shortest path (least amount of conversion) is
-     * preferred.
-     */
-    private fun findTypeConverter(
-        inputs: List<XType>,
-        outputs: List<XType>
-    ): TypeConverter? {
-        if (inputs.isEmpty()) {
-            return null
-        }
-        inputs.forEach { input ->
-            if (outputs.any { output -> input.isSameType(output) }) {
-                return NoOpConverter(input)
-            }
-        }
-
-        val excludes = arrayListOf<XType>()
-
-        val queue = LinkedList<TypeConverter>()
-        fun List<TypeConverter>.findMatchingConverter(): TypeConverter? {
-            // We prioritize exact match over assignable. To do that, this variable keeps any
-            // assignable match and if we cannot find exactly same type match, we'll return the
-            // assignable match.
-            var assignableMatchFallback: TypeConverter? = null
-            this.forEach { converter ->
-                outputs.forEach { output ->
-                    if (output.isSameType(converter.to)) {
-                        return converter
-                    } else if (assignableMatchFallback == null &&
-                        output.isAssignableFrom(converter.to)
-                    ) {
-                        // if we don't find exact match, we'll return this.
-                        assignableMatchFallback = converter
-                    }
-                }
-            }
-            return assignableMatchFallback
-        }
-        inputs.forEach { input ->
-            val candidates = getAllTypeConverters(input, excludes)
-            val match = candidates.findMatchingConverter()
-            if (match != null) {
-                return match
-            }
-            candidates.forEach {
-                excludes.add(it.to)
-                queue.add(it)
-            }
-        }
-        excludes.addAll(inputs)
-        while (queue.isNotEmpty()) {
-            val prev = queue.pop()
-            val from = prev.to
-            val candidates = getAllTypeConverters(from, excludes)
-            val match = candidates.findMatchingConverter()
-            if (match != null) {
-                return CompositeTypeConverter(prev, match)
-            }
-            candidates.forEach {
-                excludes.add(it.to)
-                queue.add(CompositeTypeConverter(prev, it))
-            }
-        }
-        return null
-    }
-
-    /**
-     * Returns all type converters that can receive input type and return into another type.
-     * The returned list is ordered by priority such that if we have an exact match, it is
-     * prioritized.
-     */
-    private fun getAllTypeConverters(input: XType, excludes: List<XType>): List<TypeConverter> {
-        // for input, check assignability because it defines whether we can use the method or not.
-        // for excludes, use exact match
-        return typeConverters.filter { converter ->
-            converter.from.isAssignableFrom(input) &&
-                !excludes.any { it.isSameType(converter.to) }
-        }.sortedByDescending {
-            // if it is the same, prioritize
-            if (it.from.isSameType(input)) {
-                2
-            } else {
-                1
-            }
-        }
-    }
-
-    /**
-     * Tries to reverse the converter going through the same nodes, if possible.
-     */
     fun reverse(converter: TypeConverter): TypeConverter? {
         return when (converter) {
             is NoOpConverter -> converter
@@ -173,6 +62,11 @@
                 val r2 = reverse(converter.conv2) ?: return null
                 CompositeTypeConverter(r2, r1)
             }
+            // reverse of require not null is upcast since not null can be converted into nullable
+            is RequireNotNullTypeConverter -> UpCastTypeConverter(
+                upCastFrom = converter.to,
+                upCastTo = converter.from
+            )
             else -> {
                 typeConverters.firstOrNull {
                     it.from.isSameType(converter.to) &&
@@ -181,4 +75,30 @@
             }
         }
     }
+
+    companion object {
+        /**
+         * @param context Processing context
+         * @param typeConverters Available TypeConverters, ordered by priority when they have the
+         *        same cost.
+         * @param knownColumnTypes List of types that can be saved into db/read from without a
+         *        converter.
+         */
+        fun create(
+            context: Context,
+            typeConverters: List<TypeConverter>,
+            knownColumnTypes: List<XType>
+        ) = if (context.useNullAwareConverter) {
+            NullAwareTypeConverterStore(
+                context = context,
+                typeConverters = typeConverters,
+                knownColumnTypes = knownColumnTypes
+            )
+        } else {
+            TypeConverterStoreImpl(
+                typeConverters = typeConverters,
+                knownColumnTypes = knownColumnTypes
+            )
+        }
+    }
 }
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeConverterStoreImpl.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeConverterStoreImpl.kt
new file mode 100644
index 0000000..848addf
--- /dev/null
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeConverterStoreImpl.kt
@@ -0,0 +1,152 @@
+/*
+ * 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.solver
+
+import androidx.room.compiler.processing.XType
+import androidx.room.solver.types.CompositeTypeConverter
+import androidx.room.solver.types.NoOpConverter
+import androidx.room.solver.types.TypeConverter
+import java.util.LinkedList
+
+/**
+ * Legacy [TypeConverterStore] implementation that does not assume we have type nullability
+ * information. It is kept around for backwards compatibility.
+ */
+class TypeConverterStoreImpl(
+    /**
+     * Available TypeConverters
+     */
+    override val typeConverters: List<TypeConverter>,
+    /**
+     * List of types that can be saved into db/read from without a converter.
+     */
+    private val knownColumnTypes: List<XType>
+) : TypeConverterStore {
+    override fun findConverterIntoStatement(
+        input: XType,
+        columnTypes: List<XType>?
+    ) = findTypeConverter(
+        inputs = listOf(input),
+        outputs = columnTypes ?: knownColumnTypes
+    )
+
+    override fun findConverterFromCursor(
+        columnTypes: List<XType>?,
+        output: XType
+    ) = findTypeConverter(
+        inputs = columnTypes ?: knownColumnTypes,
+        outputs = listOf(output)
+    )
+
+    override fun findTypeConverter(
+        input: XType,
+        output: XType
+    ) = findTypeConverter(
+        inputs = listOf(input),
+        outputs = listOf(output)
+    )
+
+    /**
+     * Finds a type converter that can convert one of the input values to one of the output values.
+     *
+     * When multiple conversion paths are possible, shortest path (least amount of conversion) is
+     * preferred.
+     */
+    private fun findTypeConverter(
+        inputs: List<XType>,
+        outputs: List<XType>
+    ): TypeConverter? {
+        if (inputs.isEmpty()) {
+            return null
+        }
+        inputs.forEach { input ->
+            if (outputs.any { output -> input.isSameType(output) }) {
+                return NoOpConverter(input)
+            }
+        }
+
+        val excludes = arrayListOf<XType>()
+
+        val queue = LinkedList<TypeConverter>()
+        fun List<TypeConverter>.findMatchingConverter(): TypeConverter? {
+            // We prioritize exact match over assignable. To do that, this variable keeps any
+            // assignable match and if we cannot find exactly same type match, we'll return the
+            // assignable match.
+            var assignableMatchFallback: TypeConverter? = null
+            this.forEach { converter ->
+                outputs.forEach { output ->
+                    if (output.isSameType(converter.to)) {
+                        return converter
+                    } else if (assignableMatchFallback == null &&
+                        output.isAssignableFrom(converter.to)
+                    ) {
+                        // if we don't find exact match, we'll return this.
+                        assignableMatchFallback = converter
+                    }
+                }
+            }
+            return assignableMatchFallback
+        }
+        inputs.forEach { input ->
+            val candidates = getAllTypeConverters(input, excludes)
+            val match = candidates.findMatchingConverter()
+            if (match != null) {
+                return match
+            }
+            candidates.forEach {
+                excludes.add(it.to)
+                queue.add(it)
+            }
+        }
+        excludes.addAll(inputs)
+        while (queue.isNotEmpty()) {
+            val prev = queue.pop()
+            val from = prev.to
+            val candidates = getAllTypeConverters(from, excludes)
+            val match = candidates.findMatchingConverter()
+            if (match != null) {
+                return CompositeTypeConverter(prev, match)
+            }
+            candidates.forEach {
+                excludes.add(it.to)
+                queue.add(CompositeTypeConverter(prev, it))
+            }
+        }
+        return null
+    }
+
+    /**
+     * Returns all type converters that can receive input type and return into another type.
+     * The returned list is ordered by priority such that if we have an exact match, it is
+     * prioritized.
+     */
+    private fun getAllTypeConverters(input: XType, excludes: List<XType>): List<TypeConverter> {
+        // for input, check assignability because it defines whether we can use the method or not.
+        // for excludes, use exact match
+        return typeConverters.filter { converter ->
+            converter.from.isAssignableFrom(input) &&
+                !excludes.any { it.isSameType(converter.to) }
+        }.sortedByDescending {
+            // if it is the same, prioritize
+            if (it.from.isSameType(input)) {
+                2
+            } else {
+                1
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/CompositeTypeConverter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/CompositeTypeConverter.kt
index b9c7cb9..2bb2c0c 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/CompositeTypeConverter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/CompositeTypeConverter.kt
@@ -22,7 +22,9 @@
  * combines 2 type converters
  */
 class CompositeTypeConverter(val conv1: TypeConverter, val conv2: TypeConverter) : TypeConverter(
-    conv1.from, conv2.to
+    from = conv1.from,
+    to = conv2.to,
+    cost = conv1.cost + conv2.cost
 ) {
     override fun doConvert(inputVarName: String, outputVarName: String, scope: CodeGenScope) {
         scope.builder().apply {
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/NullAwareTypeConverters.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/NullAwareTypeConverters.kt
new file mode 100644
index 0000000..288899f
--- /dev/null
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/NullAwareTypeConverters.kt
@@ -0,0 +1,98 @@
+/*
+ * 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.solver.types
+
+import androidx.annotation.VisibleForTesting
+import androidx.room.compiler.processing.XNullability
+import androidx.room.compiler.processing.XType
+import androidx.room.ext.L
+import androidx.room.ext.S
+import androidx.room.ext.T
+import androidx.room.solver.CodeGenScope
+import java.lang.IllegalStateException
+
+/**
+ * A type converter that checks if the input is null and returns null instead of calling the
+ * [delegate].
+ */
+class NullSafeTypeConverter(
+    @VisibleForTesting
+    internal val delegate: TypeConverter
+) : TypeConverter(
+    from = delegate.from.makeNullable(),
+    to = delegate.to.makeNullable(),
+    cost = delegate.cost + Cost.NULL_SAFE
+) {
+    init {
+        check(delegate.from.nullability == XNullability.NONNULL) {
+            "NullableWrapper can ony be used if the input type is non-nullable"
+        }
+    }
+
+    override fun doConvert(inputVarName: String, outputVarName: String, scope: CodeGenScope) {
+        scope.builder().apply {
+            beginControlFlow("if($L == null)", inputVarName)
+            addStatement("$L = null", outputVarName)
+            nextControlFlow("else")
+            delegate.convert(inputVarName, outputVarName, scope)
+            endControlFlow()
+        }
+    }
+}
+
+/**
+ * A [TypeConverter] that checks the value is `non-null` and throws if it is null.
+ */
+class RequireNotNullTypeConverter(
+    from: XType,
+) : TypeConverter(
+    from = from,
+    to = from.makeNonNullable(),
+    cost = Cost.REQUIRE_NOT_NULL
+) {
+    init {
+        check(from.nullability != XNullability.NONNULL) {
+            "No reason to null check a non-null input"
+        }
+    }
+
+    override fun doConvert(inputVarName: String, outputVarName: String, scope: CodeGenScope) {
+        scope.builder().apply {
+            beginControlFlow("if($L == null)", inputVarName)
+            addStatement(
+                "throw new $T($S)", IllegalStateException::class.java,
+                "Expected non-null ${from.typeName}, but it was null."
+            )
+            nextControlFlow("else").apply {
+                addStatement("$L = $L", outputVarName, inputVarName)
+            }
+            endControlFlow()
+        }
+    }
+
+    override fun doConvert(inputVarName: String, scope: CodeGenScope): String {
+        scope.builder().apply {
+            beginControlFlow("if($L == null)", inputVarName)
+            addStatement(
+                "throw new $T($S)", IllegalStateException::class.java,
+                "Expected non-null ${from.typeName}, but it was null."
+            )
+            endControlFlow()
+        }
+        return inputVarName
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/TypeConverter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/TypeConverter.kt
index fab9eb1..2f8ae17 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/TypeConverter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/TypeConverter.kt
@@ -16,6 +16,7 @@
 
 package androidx.room.solver.types
 
+import androidx.annotation.VisibleForTesting
 import androidx.room.compiler.processing.XType
 import androidx.room.ext.L
 import androidx.room.ext.T
@@ -24,7 +25,11 @@
 /**
  * A code generator that can convert from 1 type to another
  */
-abstract class TypeConverter(val from: XType, val to: XType) {
+abstract class TypeConverter(
+    val from: XType,
+    val to: XType,
+    val cost: Cost = Cost.CONVERTER
+) {
     /**
      * Should generate the code that will covert [inputVarName] of type [from] to [outputVarName]
      * of type [to]. This method *should not* declare the [outputVarName] as it is already
@@ -70,4 +75,123 @@
     ) {
         doConvert(inputVarName, outputVarName, scope)
     }
+
+    /**
+     * Represents the cost of a type converter.
+     *
+     * When calculating cost, we consider multiple types of conversions in ascending order, from
+     * cheapest to expensive:
+     * * `upcast`: Converts from a subtype to super type (e.g. Int to Number)
+     * * `nullSafeWrapper`: Adds a null check before calling the delegated converter or else
+     *    returns null.
+     * * `converter`: Unit converter
+     * * `requireNotNull`: Adds a null check before returning the delegated converter's value
+     *    or throws if the value is null.
+     *
+     * The comparison happens in buckets such that having 10 upcasts is still cheaper than having
+     * 1 nullSafeWrapper.
+     *
+     * Internally, this class uses an IntArray to keep its fields to optimize for readability in
+     * operators.
+     */
+    class Cost private constructor(
+        /**
+         * Values for each bucket, ordered from most expensive to least expensive.
+         */
+        private val values: IntArray
+    ) : Comparable<Cost> {
+        init {
+            require(values.size == Buckets.SIZE)
+        }
+
+        constructor(
+            converters: Int,
+            nullSafeWrapper: Int = 0,
+            upCasts: Int = 0,
+            requireNotNull: Int = 0
+        ) : this(
+            // NOTE: construction order here MUST match the [Buckets]
+            intArrayOf(
+                requireNotNull,
+                converters,
+                nullSafeWrapper,
+                upCasts
+            )
+        )
+
+        @VisibleForTesting
+        val upCasts: Int
+            get() = values[Buckets.UP_CAST]
+
+        @VisibleForTesting
+        val nullSafeWrapper: Int
+            get() = values[Buckets.NULL_SAFE]
+
+        @VisibleForTesting
+        val requireNotNull: Int
+            get() = values[Buckets.REQUIRE_NOT_NULL]
+
+        @VisibleForTesting
+        val converters: Int
+            get() = values[Buckets.CONVERTER]
+
+        operator fun plus(other: Cost) = Cost(
+            values = IntArray(Buckets.SIZE) { index ->
+                values[index] + other.values[index]
+            }
+        )
+
+        override operator fun compareTo(other: Cost): Int {
+            for (index in 0 until Buckets.SIZE) {
+                val cmp = values[index].compareTo(other.values[index])
+                if (cmp != 0) {
+                    return cmp
+                }
+            }
+            return 0
+        }
+
+        override fun toString() = buildString {
+            append("Cost[")
+            append("upcast:")
+            append(upCasts)
+            append(",nullsafe:")
+            append(nullSafeWrapper)
+            append(",converters:")
+            append(converters)
+            append(",requireNotNull:")
+            append(requireNotNull)
+            append("]")
+        }
+
+        override fun equals(other: Any?): Boolean {
+            if (other !is Cost) {
+                return false
+            }
+            return compareTo(other) == 0
+        }
+
+        override fun hashCode(): Int {
+            // we don't really use hash functions so this is good enough as a hash function.
+            return values[Buckets.CONVERTER]
+        }
+
+        companion object {
+            val UP_CAST = Cost(converters = 0, upCasts = 1)
+            val NULL_SAFE = Cost(converters = 0, nullSafeWrapper = 1)
+            val CONVERTER = Cost(converters = 1)
+            val REQUIRE_NOT_NULL = Cost(converters = 0, requireNotNull = 1)
+        }
+
+        /**
+         * Comparison buckets, ordered from the MOST expensive to LEAST expensive
+         */
+        private object Buckets {
+            const val REQUIRE_NOT_NULL = 0
+            const val CONVERTER = 1
+            const val NULL_SAFE = 2
+            const val UP_CAST = 3
+            const val SIZE = 4
+        }
+    }
 }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/UpCastTypeConverter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/UpCastTypeConverter.kt
new file mode 100644
index 0000000..ad07028
--- /dev/null
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/UpCastTypeConverter.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.solver.types
+
+import androidx.room.compiler.processing.XType
+import androidx.room.ext.L
+import androidx.room.solver.CodeGenScope
+
+/**
+ * We have a special class for upcasting types in type converters. (e.g. Int to Number)
+ * It is used in the pathfinding to be more expensive than exactly matching calls to prioritize
+ * exact matches.
+ */
+class UpCastTypeConverter(
+    upCastFrom: XType,
+    upCastTo: XType
+) : TypeConverter(
+    from = upCastFrom,
+    to = upCastTo,
+    cost = Cost.UP_CAST
+) {
+    override fun doConvert(inputVarName: String, outputVarName: String, scope: CodeGenScope) {
+        scope.builder().apply {
+            addStatement("$L = $L", outputVarName, inputVarName)
+        }
+    }
+
+    override fun doConvert(inputVarName: String, scope: CodeGenScope): String {
+        // normally, we don't need to generate any code here but if the upcast is converting from
+        // a primitive to boxed; we need to. Otherwise, output value won't become an object and
+        // that might break the rest of the code generation (e.g. checking nullable on primitive)
+        return if (to.typeName.isBoxedPrimitive && from.typeName.isPrimitive) {
+            super.doConvert(inputVarName, scope)
+        } else {
+            inputVarName
+        }
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/solver/NullabilityAwareTypeConverterStoreTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/solver/NullabilityAwareTypeConverterStoreTest.kt
new file mode 100644
index 0000000..99a0162
--- /dev/null
+++ b/room/room-compiler/src/test/kotlin/androidx/room/solver/NullabilityAwareTypeConverterStoreTest.kt
@@ -0,0 +1,743 @@
+/*
+ * 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.solver
+
+import androidx.room.RoomKspProcessor
+import androidx.room.compiler.processing.XType
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.XTestInvocation
+import androidx.room.compiler.processing.util.compiler.TestCompilationArguments
+import androidx.room.compiler.processing.util.compiler.compile
+import androidx.room.compiler.processing.util.runProcessorTest
+import androidx.room.processor.Context.BooleanProcessorOptions.USE_NULL_AWARE_CONVERTER
+import androidx.room.processor.CustomConverterProcessor
+import androidx.room.processor.DaoProcessor
+import androidx.room.solver.types.CustomTypeConverterWrapper
+import androidx.room.solver.types.TypeConverter
+import androidx.room.testing.context
+import androidx.room.vo.BuiltInConverterFlags
+import androidx.room.writer.DaoWriter
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import javax.tools.Diagnostic
+
+@RunWith(JUnit4::class)
+class NullabilityAwareTypeConverterStoreTest {
+    @get:Rule
+    val tmpFolder = TemporaryFolder()
+    val source = Source.kotlin(
+        "Foo.kt",
+        """
+            import androidx.room.*
+            class MyClass
+            class NonNullConverters {
+                @TypeConverter
+                fun myClassToString(myClass: MyClass): String {
+                    TODO()
+                }
+                @TypeConverter
+                fun stringToMyClass(input: String): MyClass {
+                    TODO()
+                }
+            }
+            class MyNullableReceivingConverters {
+                @TypeConverter
+                fun nullableMyClassToNonNullString(myClass: MyClass?): String {
+                    TODO()
+                }
+                @TypeConverter
+                fun nullableStringToNonNullMyClass(input: String?): MyClass {
+                    TODO()
+                }
+            }
+            class MyFullyNullableConverters {
+                @TypeConverter
+                fun nullableMyClassToNullableString(myClass: MyClass?): String? {
+                    TODO()
+                }
+                @TypeConverter
+                fun nullableStringToNullableMyClass(input: String?): MyClass? {
+                    TODO()
+                }
+            }
+        """.trimIndent()
+    )
+
+    private fun XTestInvocation.createStore(
+        vararg converters: String
+    ): TypeConverterStore {
+        val allConverters = converters.flatMap {
+            CustomConverterProcessor(
+                context = context,
+                element = processingEnv.requireTypeElement(it)
+            ).process()
+        }.map(::CustomTypeConverterWrapper)
+        return TypeAdapterStore.create(
+            context = context,
+            builtInConverterFlags = BuiltInConverterFlags.DEFAULT,
+            allConverters
+        ).typeConverterStore
+    }
+
+    @Test
+    fun withNonNullableConverters() {
+        val result = collectStringConversionResults("NonNullConverters")
+        assertResult(
+            result.trim(),
+            """
+            JAVAC
+            String? to MyClass?: stringToMyClass
+            MyClass? to String?: myClassToString
+            String? to MyClass!: stringToMyClass
+            MyClass! to String?: myClassToString
+            String! to MyClass?: stringToMyClass
+            MyClass? to String!: myClassToString
+            String! to MyClass!: stringToMyClass
+            MyClass! to String!: myClassToString
+            KSP
+            String? to MyClass?: (String? == null ? null : stringToMyClass)
+            MyClass? to String?: (MyClass? == null ? null : myClassToString)
+            String? to MyClass!: null
+            MyClass! to String?: myClassToString / (String! as String?)
+            String! to MyClass?: stringToMyClass / (MyClass! as MyClass?)
+            MyClass? to String!: null
+            String! to MyClass!: stringToMyClass
+            MyClass! to String!: myClassToString
+            """.trimIndent()
+        )
+    }
+
+    @Test
+    fun withNonNullableConverters_cursor() {
+        val result = collectCursorResults("NonNullConverters")
+        assertResult(
+            result.trim(),
+            """
+                JAVAC
+                Cursor to MyClass?: stringToMyClass
+                MyClass? to Cursor: myClassToString
+                Cursor to MyClass!: stringToMyClass
+                MyClass! to Cursor: myClassToString
+                KSP
+                Cursor to MyClass?: (String? == null ? null : stringToMyClass)
+                MyClass? to Cursor: (MyClass? == null ? null : myClassToString)
+                // when reading from cursor, we can assume non-null cursor value when
+                // we don't have a converter that would convert it from String?
+                Cursor to MyClass!: stringToMyClass
+                MyClass! to Cursor: myClassToString
+            """.trimIndent()
+        )
+    }
+
+    @Test
+    fun withNonNullAndNullableReceiving() {
+        val result = collectStringConversionResults(
+            "NonNullConverters",
+            "MyNullableReceivingConverters"
+        )
+        assertResult(
+            result.trim(),
+            """
+            JAVAC
+            String? to MyClass?: stringToMyClass
+            MyClass? to String?: myClassToString
+            String? to MyClass!: stringToMyClass
+            MyClass! to String?: myClassToString
+            String! to MyClass?: stringToMyClass
+            MyClass? to String!: myClassToString
+            String! to MyClass!: stringToMyClass
+            MyClass! to String!: myClassToString
+            KSP
+            String? to MyClass?: nullableStringToNonNullMyClass / (MyClass! as MyClass?)
+            MyClass? to String?: nullableMyClassToNonNullString / (String! as String?)
+            String? to MyClass!: nullableStringToNonNullMyClass
+            MyClass! to String?: myClassToString / (String! as String?)
+            String! to MyClass?: stringToMyClass / (MyClass! as MyClass?)
+            MyClass? to String!: nullableMyClassToNonNullString
+            String! to MyClass!: stringToMyClass
+            MyClass! to String!: myClassToString
+            """.trimIndent()
+        )
+    }
+
+    @Test
+    fun withNonNullAndNullableReceiving_cursor() {
+        val result = collectCursorResults(
+            "NonNullConverters",
+            "MyNullableReceivingConverters"
+        )
+        assertResult(
+            result.trim(),
+            """
+            JAVAC
+            Cursor to MyClass?: stringToMyClass
+            MyClass? to Cursor: myClassToString
+            Cursor to MyClass!: stringToMyClass
+            MyClass! to Cursor: myClassToString
+            KSP
+            // we start from nullable string because cursor values are assumed nullable when reading
+            Cursor to MyClass?: nullableStringToNonNullMyClass / (MyClass! as MyClass?)
+            // there is an additional upcast for String! to String? because when the written value
+            // is nullable, we prioritize a nullable column
+            MyClass? to Cursor: nullableMyClassToNonNullString / (String! as String?)
+            Cursor to MyClass!: nullableStringToNonNullMyClass
+            MyClass! to Cursor: myClassToString
+            """.trimIndent()
+        )
+    }
+
+    @Test
+    fun withFullyNullableConverters() {
+        val result = collectStringConversionResults(
+            "NonNullConverters",
+            "MyNullableReceivingConverters",
+            "MyFullyNullableConverters"
+        )
+        assertResult(
+            result.trim(),
+            """
+                JAVAC
+                String? to MyClass?: stringToMyClass
+                MyClass? to String?: myClassToString
+                String? to MyClass!: stringToMyClass
+                MyClass! to String?: myClassToString
+                String! to MyClass?: stringToMyClass
+                MyClass? to String!: myClassToString
+                String! to MyClass!: stringToMyClass
+                MyClass! to String!: myClassToString
+                KSP
+                String? to MyClass?: nullableStringToNullableMyClass
+                MyClass? to String?: nullableMyClassToNullableString
+                String? to MyClass!: nullableStringToNonNullMyClass
+                // another alternative is to use nonNullMyClassToNullableString and then upcast
+                // both are equal weight
+                MyClass! to String?: (MyClass! as MyClass?) / nullableMyClassToNullableString
+                String! to MyClass?: (String! as String?) / nullableStringToNullableMyClass
+                MyClass? to String!: nullableMyClassToNonNullString
+                String! to MyClass!: stringToMyClass
+                MyClass! to String!: myClassToString
+            """.trimIndent()
+        )
+    }
+
+    @Test
+    fun withFullyNullableConverters_cursor() {
+        val result = collectCursorResults(
+            "NonNullConverters",
+            "MyNullableReceivingConverters",
+            "MyFullyNullableConverters"
+        )
+        assertResult(
+            result.trim(),
+            """
+                JAVAC
+                Cursor to MyClass?: stringToMyClass
+                MyClass? to Cursor: myClassToString
+                Cursor to MyClass!: stringToMyClass
+                MyClass! to Cursor: myClassToString
+                KSP
+                Cursor to MyClass?: nullableStringToNullableMyClass
+                MyClass? to Cursor: nullableMyClassToNullableString
+                Cursor to MyClass!: nullableStringToNonNullMyClass
+                MyClass! to Cursor: myClassToString
+            """.trimIndent()
+        )
+    }
+
+    @Test
+    fun pojoProcess() {
+        // This is a repro case from trying to run TestApp with null aware converter.
+        // It reproduces the case where if we don't know nullability, we shouldn't try to
+        // prioritize nullable or non-null; instead YOLO and find whichever we can find first.
+        val user = Source.java(
+            "User", """
+            import androidx.room.*;
+            import java.util.*;
+            @TypeConverters({TestConverters.class})
+            @Entity
+            public class User {
+                @PrimaryKey
+                public int mId;
+                public Set<Day> mWorkDays = new HashSet<>();
+            }
+        """.trimIndent()
+        )
+        val converters = Source.java(
+            "TestConverters", """
+            import androidx.room.*;
+            import java.util.Date;
+            import java.util.HashSet;
+            import java.util.Set;
+            class TestConverters {
+                @TypeConverter
+                public static Set<Day> decomposeDays(int flags) {
+                    Set<Day> result = new HashSet<>();
+                    for (Day day : Day.values()) {
+                        if ((flags & (1 << day.ordinal())) != 0) {
+                            result.add(day);
+                        }
+                    }
+                    return result;
+                }
+
+                @TypeConverter
+                public static int composeDays(Set<Day> days) {
+                    int result = 0;
+                    for (Day day : days) {
+                        result |= 1 << day.ordinal();
+                    }
+                    return result;
+                }
+            }
+        """.trimIndent()
+        )
+        val day = Source.java(
+            "Day", """
+            public enum Day {
+                MONDAY,
+                TUESDAY,
+                WEDNESDAY,
+                THURSDAY,
+                FRIDAY,
+                SATURDAY,
+                SUNDAY
+            }
+        """.trimIndent()
+        )
+        val dao = Source.java(
+            "MyDao", """
+            import androidx.room.*;
+            @Dao
+            interface MyDao {
+                @Insert
+                void insert(User user);
+            }
+        """.trimIndent()
+        )
+        runProcessorTest(
+            sources = listOf(user, day, converters, dao),
+            options = mapOf(
+                USE_NULL_AWARE_CONVERTER.argName to "true"
+            )
+        ) { invocation ->
+            val daoProcessor = DaoProcessor(
+                baseContext = invocation.context,
+                element = invocation.processingEnv.requireTypeElement("MyDao"),
+                dbType = invocation.processingEnv.requireType("androidx.room.RoomDatabase"),
+                dbVerifier = null
+            )
+            DaoWriter(
+                dao = daoProcessor.process(),
+                dbElement = invocation.processingEnv
+                    .requireTypeElement("androidx.room.RoomDatabase"),
+                processingEnv = invocation.processingEnv
+            ).write(invocation.processingEnv)
+            invocation.assertCompilationResult {
+                generatedSourceFileWithPath("MyDao_Impl.java").let {
+                    // make sure it bounded w/o upcasting to Boolean
+                    it.contains("final int _tmp = TestConverters.composeDays(value.mWorkDays);")
+                    it.contains("stmt.bindLong(2, _tmp);")
+                }
+            }
+        }
+    }
+
+    @Test
+    fun checkSyntheticConverters() {
+        class MockTypeConverter(
+            from: XType,
+            to: XType,
+        ) : TypeConverter(
+            from = from,
+            to = to
+        ) {
+            override fun doConvert(
+                inputVarName: String,
+                outputVarName: String,
+                scope: CodeGenScope
+            ) {
+            }
+        }
+        runProcessorTest { invocation ->
+            val string = invocation.processingEnv.requireType(String::class)
+                .makeNonNullable()
+            val int = invocation.processingEnv.requireType(Int::class)
+                .makeNonNullable()
+            val long = invocation.processingEnv.requireType(Long::class)
+                .makeNonNullable()
+            val number = invocation.processingEnv.requireType(Number::class)
+                .makeNonNullable()
+            NullAwareTypeConverterStore(
+                context = invocation.context,
+                typeConverters = listOf(
+                    MockTypeConverter(
+                        from = string.makeNullable(),
+                        to = int.makeNullable()
+                    )
+                ),
+                knownColumnTypes = emptyList()
+            ).let { store ->
+                // nullable converter, don't duplicate anything
+                assertThat(
+                    store.typeConverters
+                ).hasSize(1)
+            }
+            NullAwareTypeConverterStore(
+                context = invocation.context,
+                typeConverters = listOf(
+                    MockTypeConverter(
+                        from = string,
+                        to = int
+                    )
+                ),
+                knownColumnTypes = emptyList()
+            ).let { store ->
+                if (invocation.isKsp) {
+                    // add a null wrapper version
+                    assertThat(store.typeConverters).hasSize(2)
+                } else {
+                    // do not duplicate unless we run in KSP
+                    assertThat(store.typeConverters).hasSize(1)
+                }
+            }
+            NullAwareTypeConverterStore(
+                context = invocation.context,
+                typeConverters = listOf(
+                    MockTypeConverter(
+                        from = string,
+                        to = int
+                    ),
+                    MockTypeConverter(
+                        from = string.makeNullable(),
+                        to = int
+                    )
+                ),
+                knownColumnTypes = emptyList()
+            ).let { store ->
+                // don't duplicate, we already have a null receiving version
+                assertThat(store.typeConverters).hasSize(2)
+            }
+            NullAwareTypeConverterStore(
+                context = invocation.context,
+                typeConverters = listOf(
+                    MockTypeConverter(
+                        from = string,
+                        to = int
+                    ),
+                    MockTypeConverter(
+                        from = string.makeNullable(),
+                        to = int.makeNullable()
+                    )
+                ),
+                knownColumnTypes = emptyList()
+            ).let { store ->
+                // don't duplicate, we already have a null receiving version
+                assertThat(store.typeConverters).hasSize(2)
+            }
+            NullAwareTypeConverterStore(
+                context = invocation.context,
+                typeConverters = listOf(
+                    MockTypeConverter(
+                        from = string,
+                        to = int
+                    ),
+                    MockTypeConverter(
+                        from = string,
+                        to = long
+                    ),
+                    MockTypeConverter(
+                        from = string.makeNullable(),
+                        to = int.makeNullable()
+                    )
+                ),
+                knownColumnTypes = emptyList()
+            ).let { store ->
+                // don't duplicate, we already have a null receiving version
+                if (invocation.isKsp) {
+                    // duplicate the long receiving one
+                    assertThat(store.typeConverters).hasSize(4)
+                } else {
+                    // don't duplicate in javac
+                    assertThat(store.typeConverters).hasSize(3)
+                }
+            }
+            NullAwareTypeConverterStore(
+                context = invocation.context,
+                typeConverters = listOf(
+                    MockTypeConverter(
+                        from = string,
+                        to = number
+                    ),
+                    MockTypeConverter(
+                        from = string.makeNullable(),
+                        to = int
+                    ),
+                ),
+                knownColumnTypes = emptyList()
+            ).let { store ->
+                // don't duplicate string number converter since we have string? to int
+                assertThat(store.typeConverters).hasSize(2)
+            }
+            NullAwareTypeConverterStore(
+                context = invocation.context,
+                typeConverters = listOf(
+                    MockTypeConverter(
+                        from = string,
+                        to = number.makeNullable()
+                    ),
+                    MockTypeConverter(
+                        from = string.makeNullable(),
+                        to = int
+                    ),
+                ),
+                knownColumnTypes = emptyList()
+            ).let { store ->
+                // don't duplicate string number converter since we have string? to int
+                assertThat(store.typeConverters).hasSize(2)
+            }
+        }
+    }
+
+    @Test
+    fun warnIfTurnedOffInKsp() {
+        val sources = Source.kotlin("Foo.kt", "")
+        arrayOf("", "true", "false").forEach { value ->
+            val result = compile(
+                workingDir = tmpFolder.newFolder(),
+                arguments = TestCompilationArguments(
+                    sources = listOf(sources),
+                    symbolProcessorProviders = listOf(
+                        RoomKspProcessor.Provider()
+                    ),
+                    processorOptions = mapOf(
+                        USE_NULL_AWARE_CONVERTER.argName to value
+                    )
+                )
+            )
+            val warnings = result.diagnostics[Diagnostic.Kind.WARNING]?.map {
+                it.msg
+            }?.filter {
+                it.contains("Disabling null-aware type analysis in KSP is a temporary flag")
+            } ?: emptyList()
+            val expected = if (value == "false") {
+                1
+            } else {
+                0
+            }
+            assertThat(
+                warnings
+            ).hasSize(expected)
+        }
+    }
+
+    /**
+     * Test converting a known column type into another type due to explicit affinity
+     */
+    @Test
+    fun knownColumnTypeToExplicitType() {
+        val source = Source.kotlin(
+            "Subject.kt", """
+            import androidx.room.*
+            object MyByteArrayConverter {
+                @TypeConverter
+                fun toByteArray(input:String): ByteArray { TODO() }
+                @TypeConverter
+                fun fromByteArray(input:ByteArray): String { TODO() }
+            }
+            class Subject(val arr:ByteArray)
+        """.trimIndent()
+        )
+        runProcessorTest(
+            sources = listOf(source),
+            options = mapOf(
+                USE_NULL_AWARE_CONVERTER.argName to "true"
+            )
+        ) { invocation ->
+            val byteArray = invocation.processingEnv.requireTypeElement("Subject")
+                .getDeclaredFields().first().type.makeNonNullable()
+            val string = invocation.processingEnv.requireType("java.lang.String")
+            invocation.createStore().let { storeWithoutConverter ->
+                val intoStatement = storeWithoutConverter.findConverterIntoStatement(
+                    input = byteArray,
+                    columnTypes = listOf(
+                        string.makeNullable(),
+                        string.makeNonNullable()
+                    )
+                )
+                assertThat(intoStatement).isNull()
+                val fromCursor = storeWithoutConverter.findConverterFromCursor(
+                    output = byteArray,
+                    columnTypes = listOf(
+                        string.makeNullable(),
+                        string.makeNonNullable()
+                    )
+                )
+                assertThat(fromCursor).isNull()
+            }
+
+            invocation.createStore(
+                "MyByteArrayConverter"
+            ).let { storeWithConverter ->
+                val intoStatement = storeWithConverter.findConverterIntoStatement(
+                    input = byteArray,
+                    columnTypes = listOf(
+                        string.makeNullable(),
+                        string.makeNonNullable()
+                    )
+                )
+                assertThat(intoStatement?.toSignature()).isEqualTo("fromByteArray")
+                assertThat(intoStatement?.to).isEqualTo(string.makeNonNullable())
+                assertThat(intoStatement?.from).isEqualTo(byteArray.makeNonNullable())
+                val fromCursor = storeWithConverter.findConverterFromCursor(
+                    output = byteArray,
+                    columnTypes = listOf(
+                        string.makeNullable(),
+                        string.makeNonNullable()
+                    )
+                )
+                assertThat(fromCursor?.toSignature()).isEqualTo("toByteArray")
+                assertThat(fromCursor?.to).isEqualTo(byteArray.makeNonNullable())
+                assertThat(fromCursor?.from).isEqualTo(string.makeNonNullable())
+            }
+        }
+    }
+
+    /**
+     * Collect results for conversion from String to our type
+     */
+    private fun collectStringConversionResults(
+        vararg selectedConverters: String
+    ): String {
+        val result = StringBuilder()
+        runProcessorTest(
+            sources = listOf(source),
+            options = mapOf(
+                USE_NULL_AWARE_CONVERTER.argName to "true"
+            )
+        ) { invocation ->
+            val store = invocation.createStore(*selectedConverters)
+            assertThat(store).isInstanceOf(NullAwareTypeConverterStore::class.java)
+            val myClassTypeElement = invocation.processingEnv.requireTypeElement(
+                "MyClass"
+            )
+            val stringTypeElement = invocation.processingEnv.requireTypeElement(
+                "java.lang.String"
+            )
+
+            result.appendLine(invocation.processingEnv.backend.name)
+            listOf(
+                stringTypeElement.type.makeNullable(),
+                stringTypeElement.type.makeNonNullable(),
+            ).forEach { stringType ->
+                listOf(
+                    myClassTypeElement.type.makeNullable(),
+                    myClassTypeElement.type.makeNonNullable()
+                ).forEach { myClassType ->
+                    val fromString = store.findTypeConverter(
+                        input = stringType,
+                        output = myClassType
+                    )
+                    val toString = store.findTypeConverter(
+                        input = myClassType,
+                        output = stringType
+                    )
+                    result.apply {
+                        append(stringType.toSignature())
+                        append(" to ")
+                        append(myClassType.toSignature())
+                        append(": ")
+                        appendLine(fromString?.toSignature() ?: "null")
+                    }
+
+                    result.apply {
+                        append(myClassType.toSignature())
+                        append(" to ")
+                        append(stringType.toSignature())
+                        append(": ")
+                        appendLine(toString?.toSignature() ?: "null")
+                    }
+                }
+            }
+        }
+        return result.toString()
+    }
+
+    /**
+     * Collect results for conversion from an unknown cursor type to our type
+     */
+    private fun collectCursorResults(
+        vararg selectedConverters: String
+    ): String {
+        val result = StringBuilder()
+        runProcessorTest(
+            sources = listOf(source),
+            options = mapOf(
+                USE_NULL_AWARE_CONVERTER.argName to "true"
+            )
+        ) { invocation ->
+            val store = invocation.createStore(*selectedConverters)
+            assertThat(store).isInstanceOf(NullAwareTypeConverterStore::class.java)
+            val myClassTypeElement = invocation.processingEnv.requireTypeElement(
+                "MyClass"
+            )
+
+            result.appendLine(invocation.processingEnv.backend.name)
+            listOf(
+                myClassTypeElement.type.makeNullable(),
+                myClassTypeElement.type.makeNonNullable()
+            ).forEach { myClassType ->
+                val toMyClass = store.findConverterFromCursor(
+                    columnTypes = null,
+                    output = myClassType
+                )
+                val fromMyClass = store.findConverterIntoStatement(
+                    input = myClassType,
+                    columnTypes = null
+                )
+
+                result.apply {
+                    append("Cursor to ")
+                    append(myClassType.toSignature())
+                    append(": ")
+                    appendLine(toMyClass?.toSignature() ?: "null")
+                }
+
+                result.apply {
+                    append(myClassType.toSignature())
+                    append(" to Cursor: ")
+                    appendLine(fromMyClass?.toSignature() ?: "null")
+                }
+            }
+        }
+        return result.toString()
+    }
+
+    private fun assertResult(result: String, expected: String) {
+        // remove commented lines from expected as they are used to explain cases for test's
+        // readability
+        assertThat(result).isEqualTo(
+            expected
+                .lines()
+                .filterNot { it.trim().startsWith("//") }
+                .joinToString("\n")
+        )
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/solver/Signatures.kt b/room/room-compiler/src/test/kotlin/androidx/room/solver/Signatures.kt
new file mode 100644
index 0000000..4ddee69
--- /dev/null
+++ b/room/room-compiler/src/test/kotlin/androidx/room/solver/Signatures.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.solver
+
+import androidx.room.compiler.processing.XNullability
+import androidx.room.compiler.processing.XType
+import androidx.room.solver.types.CompositeTypeConverter
+import androidx.room.solver.types.CustomTypeConverterWrapper
+import androidx.room.solver.types.NullSafeTypeConverter
+import androidx.room.solver.types.RequireNotNullTypeConverter
+import androidx.room.solver.types.TypeConverter
+import androidx.room.solver.types.UpCastTypeConverter
+
+// Shared signatures for objects that make testing more readable
+private fun XNullability.toSignature() = when (this) {
+    XNullability.NONNULL -> "!"
+    XNullability.NULLABLE -> "?"
+    XNullability.UNKNOWN -> ""
+}
+
+fun XType.toSignature() =
+    "$typeName${nullability.toSignature()}".substringAfter("java.lang.")
+
+fun TypeConverter.toSignature(): String {
+    return when (this) {
+        is CompositeTypeConverter -> "${conv1.toSignature()} / ${conv2.toSignature()}"
+        is CustomTypeConverterWrapper -> this.custom.methodName
+        is NullSafeTypeConverter ->
+            "(${this.from.toSignature()} == null " +
+                "? null : ${this.delegate.toSignature()})"
+        is RequireNotNullTypeConverter ->
+            "checkNotNull(${from.toSignature()})"
+        is UpCastTypeConverter ->
+            "(${from.toSignature()} as ${to.toSignature()})"
+        else -> "${from.toSignature()} -> ${to.toSignature()}"
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeConverterCostTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeConverterCostTest.kt
new file mode 100644
index 0000000..03ab939
--- /dev/null
+++ b/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeConverterCostTest.kt
@@ -0,0 +1,100 @@
+/*
+ * 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.solver
+
+import androidx.room.solver.types.TypeConverter.Cost
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+class TypeConverterCostTest {
+    @Test
+    fun sections() {
+        val cost = Cost(
+            upCasts = 7,
+            nullSafeWrapper = 3,
+            converters = 1,
+            requireNotNull = 2
+        )
+        assertThat(
+            cost.toString()
+        ).isEqualTo(
+            "Cost[upcast:7,nullsafe:3,converters:1,requireNotNull:2]"
+        )
+    }
+
+    @Test
+    fun sum() {
+        assertThat(
+            Cost(converters = 3) + Cost(converters = 3, nullSafeWrapper = 4)
+
+        ).isEqualTo(Cost(converters = 6, nullSafeWrapper = 4))
+    }
+
+    @Test
+    fun sum2() {
+        val sum = Cost(
+            converters = 1,
+            upCasts = 60,
+        ) + Cost(
+            converters = 1,
+            nullSafeWrapper = 2,
+            upCasts = 20
+        )
+        assertThat(
+            sum
+        ).isEqualTo(
+            Cost(
+               converters = 2,
+               nullSafeWrapper = 2,
+               upCasts = 80
+            )
+        )
+    }
+
+    @Test
+    fun compare() {
+        assertThat(
+            Cost(requireNotNull = 1, converters = 2, nullSafeWrapper = 3, upCasts = 4)
+        ).isEquivalentAccordingToCompareTo(
+            Cost(requireNotNull = 1, converters = 2, nullSafeWrapper = 3, upCasts = 4)
+        )
+        // require not null is the most expensive
+        assertThat(
+            Cost(requireNotNull = 2, converters = 1, nullSafeWrapper = 1, upCasts = 1)
+        ).isGreaterThan(
+            Cost(requireNotNull = 1, converters = 2, nullSafeWrapper = 2, upCasts = 2)
+        )
+        // converters are the second most expensive
+        assertThat(
+            Cost(requireNotNull = 1, converters = 2, nullSafeWrapper = 1, upCasts = 1)
+        ).isGreaterThan(
+            Cost(requireNotNull = 1, converters = 1, nullSafeWrapper = 2, upCasts = 2)
+        )
+        // null safe wrapper is the third most expensive
+        assertThat(
+            Cost(requireNotNull = 1, converters = 1, nullSafeWrapper = 2, upCasts = 1)
+        ).isGreaterThan(
+            Cost(requireNotNull = 1, converters = 1, nullSafeWrapper = 1, upCasts = 2)
+        )
+        // upcast is the least expensive
+        assertThat(
+            Cost(requireNotNull = 1, converters = 1, nullSafeWrapper = 1, upCasts = 2)
+        ).isGreaterThan(
+            Cost(requireNotNull = 1, converters = 1, nullSafeWrapper = 1, upCasts = 1)
+        )
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeConverterStoreTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeConverterStoreTest.kt
index 18c67d2..a6c6499 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeConverterStoreTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeConverterStoreTest.kt
@@ -98,6 +98,14 @@
                 findConverter("Type1_Super", "Type2_Super")
             ).isEqualTo(
                 "Type1_Super -> JumpType_2 : JumpType_2 -> JumpType_3 : JumpType_3 -> Type2_Sub"
+                    .let { tillSub ->
+                        if (invocation.context.useNullAwareConverter) {
+                            // new type converter will have a step for upcasts too
+                            "$tillSub : Type2_Sub -> Type2_Super"
+                        } else {
+                            tillSub
+                        }
+                    }
             )
             assertThat(
                 findConverter("Type1", "Type2_Sub")
@@ -107,7 +115,14 @@
             assertThat(
                 findConverter("Type1_Sub", "Type2_Sub")
             ).isEqualTo(
-                "Type1 -> JumpType_1 : JumpType_1 -> Type2_Sub"
+                "Type1 -> JumpType_1 : JumpType_1 -> Type2_Sub".let { end ->
+                    if (invocation.context.useNullAwareConverter) {
+                        // new type converter will have a step for upcasts too
+                        "Type1_Sub -> Type1 : $end"
+                    } else {
+                        end
+                    }
+                }
             )
             assertThat(
                 findConverter("Type2", "Type2_Sub")