Supporting value classes in Room for KSP.

Value classes are now supported for KSP with Kotlin codegen in Room.

In KAPT stubs and in bytecode the places where the value class is used is replaced by the underlying type. This is the reason why we can't tell in KAPT or javac if the type is a value class without looking at the metadata. We have filed b/273592453 to further investigate this and figure out a solution for handling this case.

We are planning to follow-up and add a check such that, if user is on KAPT, straight out error, if user is on KSP gen Java, hint to @JvmInline and @JvmName requirements (if we can conclusively say they work this way) to use value classes with Java codegen and point to full-support with Kotlin codegen.

Bug: 124624218
Bug: 272820290
Test: TypeAdapterStoreTest.kt
Change-Id: I0a380ada951df776ed7bcb96757b86103743b0c5
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/Experiment.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/Experiment.kt
new file mode 100644
index 0000000..99a7600
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/Experiment.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2023 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.androidx.room.integration.kotlintestapp.vo
+
+@JvmInline
+value class Schrodinger(val experiment: Experiment)
+
+@JvmInline
+value class Cat(val catStatus: CatStatus)
+
+data class Experiment(val isCatAlive: String)
+
+data class CatStatus(val isCatAlive: String)
\ No newline at end of file
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/SchrodingerConverter.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/SchrodingerConverter.kt
new file mode 100755
index 0000000..edf961a
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/SchrodingerConverter.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2017, 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.androidx.room.integration.kotlintestapp.vo
+
+import androidx.room.TypeConverter
+
+object SchrodingerConverter {
+    @TypeConverter
+    fun schrodingerToCat(schrodinger: Schrodinger): Cat {
+        return Cat(CatStatus(schrodinger.experiment.isCatAlive))
+    }
+
+    @TypeConverter
+    fun catToIsCatAlive(cat: Cat): String {
+        return cat.catStatus.isCatAlive
+    }
+
+    @TypeConverter
+    fun isCatAliveToCat(isCatAlive: String): Cat {
+        return Cat(CatStatus(isCatAlive))
+    }
+
+    @TypeConverter
+    fun catToSchrodinger(cat: Cat): Schrodinger {
+        return Schrodinger(Experiment(cat.catStatus.isCatAlive))
+    }
+}
diff --git a/room/integration-tests/kotlintestapp/src/androidTestWithKsp/java/androidx/room/integration/kotlintestapp/NullabilityAwareTypeConversionTest.kt b/room/integration-tests/kotlintestapp/src/androidTestWithKspGenJava/java/androidx/room/integration/kotlintestapp/NullabilityAwareTypeConversionTest.kt
similarity index 100%
rename from room/integration-tests/kotlintestapp/src/androidTestWithKsp/java/androidx/room/integration/kotlintestapp/NullabilityAwareTypeConversionTest.kt
rename to room/integration-tests/kotlintestapp/src/androidTestWithKspGenJava/java/androidx/room/integration/kotlintestapp/NullabilityAwareTypeConversionTest.kt
diff --git a/room/integration-tests/kotlintestapp/src/androidTestWithKspGenKotlin/java/androidx/room/integration/kotlintestapp/test/ValueClassConverterWrapperTest.kt b/room/integration-tests/kotlintestapp/src/androidTestWithKspGenKotlin/java/androidx/room/integration/kotlintestapp/test/ValueClassConverterWrapperTest.kt
new file mode 100644
index 0000000..673ebd7
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTestWithKspGenKotlin/java/androidx/room/integration/kotlintestapp/test/ValueClassConverterWrapperTest.kt
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.integration.kotlintestapp.test
+
+import android.content.Context
+import androidx.room.Dao
+import androidx.room.Database
+import androidx.room.Entity
+import androidx.room.Insert
+import androidx.room.PrimaryKey
+import androidx.room.Query
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.TypeConverters
+import androidx.room.androidx.room.integration.kotlintestapp.vo.Experiment
+import androidx.room.androidx.room.integration.kotlintestapp.vo.Schrodinger
+import androidx.room.androidx.room.integration.kotlintestapp.vo.SchrodingerConverter
+import androidx.room.integration.kotlintestapp.vo.DateConverter
+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 java.util.Date
+import java.util.UUID
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ValueClassConverterWrapperTest {
+
+    @JvmInline
+    value class UserWithInt(val password: Int)
+
+    @JvmInline
+    value class UserWithString(val password: String)
+
+    @JvmInline
+    value class UserWithUUID(val password: UUID)
+
+    @JvmInline
+    value class UserWithByte(val password: Byte)
+
+    @JvmInline
+    value class UserWithDate(val password: Date)
+
+    @JvmInline
+    value class UserWithGeneric<T>(val password: T)
+
+    enum class Season {
+        WINTER, SUMMER, SPRING, FALL
+    }
+
+    @JvmInline
+    value class UserWithEnum(val password: Season)
+
+    @JvmInline
+    value class UserWithStringInternal(internal val password: String)
+
+    @JvmInline
+    value class UserWithByteArray(val password: ByteArray)
+
+    @Entity
+    @TypeConverters(DateConverter::class, SchrodingerConverter::class)
+    class UserInfo(
+        @PrimaryKey
+        val pk: Int,
+        val userIntPwd: UserWithInt,
+        val userStringPwd: UserWithString,
+        val userUUIDPwd: UserWithUUID,
+        val userBytePwd: UserWithByte,
+        val userEnumPwd: UserWithEnum,
+        val userDatePwd: UserWithDate,
+        val userStringInternalPwd: UserWithStringInternal,
+        val userGenericPwd: UserWithGeneric<String>,
+        val userByteArrayPwd: UserWithByteArray,
+        val schrodingerUser: Schrodinger
+    ) {
+        override fun equals(other: Any?): Boolean {
+            val otherEntity = other as UserInfo
+            return pk == otherEntity.pk &&
+                userIntPwd == otherEntity.userIntPwd &&
+                userStringPwd == otherEntity.userStringPwd &&
+                userBytePwd == otherEntity.userBytePwd &&
+                userUUIDPwd == otherEntity.userUUIDPwd &&
+                userEnumPwd == otherEntity.userEnumPwd &&
+                userDatePwd == otherEntity.userDatePwd &&
+                userStringInternalPwd == otherEntity.userStringInternalPwd &&
+                userGenericPwd == otherEntity.userGenericPwd &&
+                userByteArrayPwd.password.contentEquals(otherEntity.userByteArrayPwd.password) &&
+                schrodingerUser.experiment.isCatAlive ==
+                otherEntity.schrodingerUser.experiment.isCatAlive
+        }
+
+        override fun hashCode(): Int {
+            return 1
+        }
+    }
+
+    @Dao
+    interface SampleDao {
+        @Query("SELECT * FROM UserInfo")
+        fun getEntity(): UserInfo
+
+        @Insert
+        fun insert(item: UserInfo)
+    }
+
+    @Database(
+        entities = [UserInfo::class],
+        version = 1,
+        exportSchema = false
+    )
+    abstract class ValueClassConverterWrapperDatabase : RoomDatabase() {
+        abstract fun dao(): SampleDao
+    }
+
+    private lateinit var db: ValueClassConverterWrapperDatabase
+    private val pk = 0
+    private val intPwd = UserWithInt(123)
+    private val stringPwd = UserWithString("open_sesame")
+    private val uuidPwd = UserWithUUID(UUID.randomUUID())
+    private val bytePwd = UserWithByte(Byte.MIN_VALUE)
+    private val enumPwd = UserWithEnum(Season.SUMMER)
+    private val datePwd = UserWithDate(Date(2023L))
+    private val internalPwd = UserWithStringInternal("open_sesame")
+    private val genericPwd = UserWithGeneric("open_sesame")
+    private val byteArrayPwd = UserWithByteArray(byteArrayOf(Byte.MIN_VALUE))
+    private val shrodingerPwd = Schrodinger(Experiment("the cat is alive!"))
+
+    @Test
+    fun readAndWriteValueClassToDatabase() {
+        val customerInfo = UserInfo(
+            pk = pk,
+            userIntPwd = intPwd,
+            userStringPwd = stringPwd,
+            userUUIDPwd = uuidPwd,
+            userBytePwd = bytePwd,
+            userEnumPwd = enumPwd,
+            userDatePwd = datePwd,
+            userStringInternalPwd = internalPwd,
+            userGenericPwd = genericPwd,
+            userByteArrayPwd = byteArrayPwd,
+            schrodingerUser = shrodingerPwd
+        )
+
+        db.dao().insert(customerInfo)
+
+        val readEntity = db.dao().getEntity()
+
+        assertThat(readEntity).isEqualTo(customerInfo)
+    }
+
+    @Before
+    fun initDb() {
+        val context = ApplicationProvider.getApplicationContext<Context>()
+        db = Room.inMemoryDatabaseBuilder(
+            context,
+            ValueClassConverterWrapperDatabase::class.java
+        ).build()
+    }
+
+    @After
+    fun teardown() {
+        db.close()
+    }
+}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
index a9d786c..8fb91f6 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
@@ -248,7 +248,9 @@
     }
 
     override fun isValueClass(): Boolean {
-        return Modifier.INLINE in declaration.modifiers
+        // The inline modifier for inline classes is deprecated in Kotlin but we still include it
+        // in this check.
+        return Modifier.VALUE in declaration.modifiers || Modifier.INLINE in declaration.modifiers
     }
 
     override fun isFunctionalInterface(): Boolean {
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
index 330d1b4..24449d4 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
@@ -408,11 +408,7 @@
             assertThat(getModifiers("DataClass"))
                 .containsExactly("public", "final", "class", "data")
             assertThat(getModifiers("InlineClass")).apply {
-                if (isPreCompiled && invocation.isKsp) {
-                    containsExactly("public", "final", "class")
-                } else {
-                    containsExactly("public", "final", "class", "value")
-                }
+                containsExactly("public", "final", "class", "value")
             }
             assertThat(getModifiers("FunInterface"))
                 .containsExactly("public", "abstract", "interface", "fun")
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/DatabaseProcessingStep.kt b/room/room-compiler/src/main/kotlin/androidx/room/DatabaseProcessingStep.kt
index 2676100..472f46e 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/DatabaseProcessingStep.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/DatabaseProcessingStep.kt
@@ -23,6 +23,7 @@
 import androidx.room.compiler.processing.XTypeElement
 import androidx.room.log.RLog
 import androidx.room.processor.Context
+import androidx.room.processor.Context.BooleanProcessorOptions.GENERATE_KOTLIN
 import androidx.room.processor.DatabaseProcessor
 import androidx.room.processor.ProcessorErrors
 import androidx.room.util.SchemaFileResolver
@@ -45,9 +46,10 @@
         elementsByAnnotation: Map<String, Set<XElement>>,
         isLastRound: Boolean
     ): Set<XTypeElement> {
-        check(env.config == ENV_CONFIG) {
-            "Room Processor expected $ENV_CONFIG but was invoked with a different configuration:" +
-                "${env.config}"
+        check(env.config == getEnvConfig(env.options)) {
+            "Room Processor expected ${getEnvConfig(env.options)} " +
+                "but was invoked with a different " +
+                "configuration: ${env.config}"
         }
         val context = Context(env)
 
@@ -175,8 +177,9 @@
     }
 
     companion object {
-        internal val ENV_CONFIG = XProcessingEnvConfig.DEFAULT.copy(
-            excludeMethodsWithInvalidJvmSourceNames = true
-        )
+        internal fun getEnvConfig(options: Map<String, String>) =
+            XProcessingEnvConfig.DEFAULT.copy(
+                excludeMethodsWithInvalidJvmSourceNames = !GENERATE_KOTLIN.getValue(options)
+            )
     }
 }
\ No newline at end of file
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 0a37134..e2f9e3f 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/RoomKspProcessor.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/RoomKspProcessor.kt
@@ -16,7 +16,6 @@
 
 package androidx.room
 
-import androidx.room.DatabaseProcessingStep.Companion.ENV_CONFIG
 import androidx.room.compiler.processing.XProcessingEnv
 import androidx.room.compiler.processing.XRoundEnv
 import androidx.room.compiler.processing.ksp.KspBasicAnnotationProcessor
@@ -33,7 +32,10 @@
  */
 class RoomKspProcessor(
     environment: SymbolProcessorEnvironment
-) : KspBasicAnnotationProcessor(environment, ENV_CONFIG) {
+) : KspBasicAnnotationProcessor(
+    symbolProcessorEnvironment = environment,
+    config = DatabaseProcessingStep.getEnvConfig(environment.options)
+) {
     init {
         // print a warning if null aware converter is disabled because we'll remove that ability
         // soon.
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/RoomProcessor.kt b/room/room-compiler/src/main/kotlin/androidx/room/RoomProcessor.kt
index 5c7daee..00d1e5a 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/RoomProcessor.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/RoomProcessor.kt
@@ -16,7 +16,6 @@
 
 package androidx.room
 
-import androidx.room.DatabaseProcessingStep.Companion.ENV_CONFIG
 import androidx.room.compiler.processing.XProcessingEnv
 import androidx.room.compiler.processing.XRoundEnv
 import androidx.room.compiler.processing.javac.JavacBasicAnnotationProcessor
@@ -36,9 +35,11 @@
 /**
  * The annotation processor for Room.
  */
-class RoomProcessor : JavacBasicAnnotationProcessor({
-    ENV_CONFIG
-}) {
+class RoomProcessor : JavacBasicAnnotationProcessor(
+    configureEnv = { options ->
+        DatabaseProcessingStep.getEnvConfig(options)
+    }
+) {
 
     /** Helper variable to avoid reporting the warning twice. */
     private var jdkVersionHasBugReported = false
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/ext/xelement_ext.kt b/room/room-compiler/src/main/kotlin/androidx/room/ext/xelement_ext.kt
index 487bafc..5c36768 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/ext/xelement_ext.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/ext/xelement_ext.kt
@@ -18,6 +18,7 @@
 
 import kotlin.contracts.contract
 import androidx.room.compiler.processing.XElement
+import androidx.room.compiler.processing.XFieldElement
 import androidx.room.compiler.processing.XTypeElement
 
 fun XElement.isEntityElement(): Boolean {
@@ -27,6 +28,13 @@
     return this.hasAnnotation(androidx.room.Entity::class)
 }
 
+fun XTypeElement.getValueClassUnderlyingProperty(): XFieldElement {
+    check(this.isValueClass()) {
+        "Can't get value class property, type element '$this' is not a value class"
+    }
+    return this.getDeclaredFields().single()
+}
+
 /**
  * Suffix of the Kotlin synthetic class created interface method implementations.
  */
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 3221bd5..07ee499 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
@@ -274,8 +274,16 @@
             return getInputValue(processingEnv) ?: defaultValue
         }
 
+        fun getValue(options: Map<String, String>): Boolean {
+            return getInputValue(options) ?: defaultValue
+        }
+
         fun getInputValue(processingEnv: XProcessingEnv): Boolean? {
-            return processingEnv.options[argName]?.takeIf {
+            return getInputValue(processingEnv.options)
+        }
+
+        private fun getInputValue(options: Map<String, String>): Boolean? {
+            return options[argName]?.takeIf {
                 it.isNotBlank()
             }?.toBoolean()
         }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/FieldProcessor.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/FieldProcessor.kt
index 1645d65..5c72f35 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/FieldProcessor.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/FieldProcessor.kt
@@ -17,6 +17,7 @@
 package androidx.room.processor
 
 import androidx.room.ColumnInfo
+import androidx.room.compiler.codegen.CodeLanguage
 import androidx.room.compiler.processing.XFieldElement
 import androidx.room.compiler.processing.XType
 import androidx.room.parser.Collate
@@ -82,6 +83,13 @@
             nonNull = nonNull
         )
 
+        // TODO(b/273592453): Figure out a way to detect value classes in KAPT and guard against it.
+        if (member.typeElement?.isValueClass() == true &&
+            context.codeLanguage != CodeLanguage.KOTLIN
+        ) {
+            onBindingError(field, ProcessorErrors.VALUE_CLASS_ONLY_SUPPORTED_IN_KSP)
+        }
+
         when (bindingScope) {
             BindingScope.TWO_WAY -> {
                 field.statementBinder = adapter
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
index 414e98f..0367eeb 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
@@ -255,6 +255,9 @@
     val CANNOT_FIND_COLUMN_TYPE_ADAPTER = "Cannot figure out how to save this field into" +
         " database. You can consider adding a type converter for it."
 
+    val VALUE_CLASS_ONLY_SUPPORTED_IN_KSP = "Kotlin value classes are only supported " +
+        "in Room using KSP and generating Kotlin (room.generateKotlin=true)."
+
     val CANNOT_FIND_STMT_BINDER = "Cannot figure out how to bind this field into a statement."
 
     val CANNOT_FIND_CURSOR_READER = "Cannot figure out how to read this field from a cursor."
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 329e023..030d092 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
@@ -28,6 +28,7 @@
 import androidx.room.ext.CollectionTypeNames.LONG_SPARSE_ARRAY
 import androidx.room.ext.CommonTypeNames
 import androidx.room.ext.GuavaTypeNames
+import androidx.room.ext.getValueClassUnderlyingProperty
 import androidx.room.ext.isByteBuffer
 import androidx.room.ext.isEntityElement
 import androidx.room.ext.isNotByte
@@ -115,6 +116,7 @@
 import androidx.room.solver.types.StringColumnTypeAdapter
 import androidx.room.solver.types.TypeConverter
 import androidx.room.solver.types.UuidColumnTypeAdapter
+import androidx.room.solver.types.ValueClassConverterWrapper
 import androidx.room.vo.BuiltInConverterFlags
 import androidx.room.vo.MapInfo
 import androidx.room.vo.ShortcutQueryParameter
@@ -278,7 +280,7 @@
         if (adapterByTypeConverter != null) {
             return adapterByTypeConverter
         }
-        val defaultAdapter = createDefaultTypeAdapter(input)
+        val defaultAdapter = createDefaultTypeAdapter(input, affinity)
         if (defaultAdapter != null) {
             return defaultAdapter
         }
@@ -316,7 +318,7 @@
             return typeConverterAdapter
         }
 
-        val defaultAdapter = createDefaultTypeAdapter(output)
+        val defaultAdapter = createDefaultTypeAdapter(output, affinity)
         if (defaultAdapter != null) {
             return defaultAdapter
         }
@@ -361,7 +363,7 @@
         }
 
         if (!skipDefaultConverter) {
-            val defaultAdapter = createDefaultTypeAdapter(out)
+            val defaultAdapter = createDefaultTypeAdapter(out, affinity)
             if (defaultAdapter != null) {
                 return defaultAdapter
             }
@@ -369,8 +371,27 @@
         return null
     }
 
-    private fun createDefaultTypeAdapter(type: XType): ColumnTypeAdapter? {
+    private fun createDefaultTypeAdapter(
+        type: XType,
+        affinity: SQLTypeAffinity?
+    ): ColumnTypeAdapter? {
         val typeElement = type.typeElement
+        if (typeElement?.isValueClass() == true) {
+            // Extract the type value of the Value class element
+            val underlyingProperty = typeElement.getValueClassUnderlyingProperty()
+            val underlyingTypeColumnAdapter = findColumnTypeAdapter(
+                out = underlyingProperty.asMemberOf(type),
+                affinity = affinity,
+                skipDefaultConverter = false
+            ) ?: return null
+
+            return ValueClassConverterWrapper(
+                valueTypeColumnAdapter = underlyingTypeColumnAdapter,
+                affinity = underlyingTypeColumnAdapter.typeAffinity,
+                out = type,
+                valuePropertyName = underlyingProperty.name
+            )
+        }
         return when {
             builtInConverterFlags.enums.isEnabled() &&
                 typeElement?.isEnum() == true -> EnumColumnTypeAdapter(typeElement, type)
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/ValueClassConverterWrapper.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/ValueClassConverterWrapper.kt
new file mode 100644
index 0000000..57a2247
--- /dev/null
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/ValueClassConverterWrapper.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2017 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.codegen.XCodeBlock
+import androidx.room.compiler.processing.XNullability
+import androidx.room.compiler.processing.XType
+import androidx.room.parser.SQLTypeAffinity
+import androidx.room.solver.CodeGenScope
+
+/**
+ * ColumnTypeAdapter for Kotlin value classes that simply wraps and forwards calls to a found
+ * adapter for the underlying type.
+ */
+class ValueClassConverterWrapper(
+    val valueTypeColumnAdapter: ColumnTypeAdapter,
+    val affinity: SQLTypeAffinity,
+    out: XType,
+    val valuePropertyName: String
+) : ColumnTypeAdapter(out, affinity) {
+    override fun readFromCursor(
+        outVarName: String,
+        cursorVarName: String,
+        indexVarName: String,
+        scope: CodeGenScope
+    ) {
+        scope.builder.apply {
+            fun XCodeBlock.Builder.addTypeToValueClassStatement() {
+                val propertyValueVarName = scope.getTmpVar("_$valuePropertyName")
+                addLocalVariable(propertyValueVarName, valueTypeColumnAdapter.outTypeName)
+                valueTypeColumnAdapter.readFromCursor(
+                    propertyValueVarName,
+                    cursorVarName,
+                    indexVarName,
+                    scope
+                )
+                addStatement(
+                    format = "%L = %L",
+                    outVarName,
+                    XCodeBlock.ofNewInstance(
+                        language,
+                        out.asTypeName(),
+                        "%N",
+                        propertyValueVarName
+                    )
+                )
+            }
+            if (out.nullability == XNullability.NONNULL) {
+                addTypeToValueClassStatement()
+            } else {
+                beginControlFlow("if (%L.isNull(%L))", cursorVarName, indexVarName)
+                    .addStatement("%L = null", outVarName)
+                nextControlFlow("else")
+                    .addTypeToValueClassStatement()
+                endControlFlow()
+            }
+        }
+    }
+
+    override fun bindToStmt(
+        stmtName: String,
+        indexVarName: String,
+        valueVarName: String,
+        scope: CodeGenScope
+    ) {
+        scope.builder.apply {
+            val propertyName = scope.getTmpVar("_$valuePropertyName")
+            addLocalVariable(
+                name = propertyName,
+                typeName = valueTypeColumnAdapter.outTypeName,
+                assignExpr = XCodeBlock.of(
+                    scope.language,
+                    "%L.%L",
+                    valueVarName,
+                    valuePropertyName
+                )
+            )
+
+            if (out.nullability == XNullability.NONNULL) {
+                valueTypeColumnAdapter.bindToStmt(
+                    stmtName,
+                    indexVarName,
+                    propertyName,
+                    scope
+                )
+            } else {
+                beginControlFlow(
+                    "if (%L == null)",
+                    propertyName
+                ).addStatement("%L.bindNull(%L)", stmtName, indexVarName)
+                nextControlFlow("else")
+                valueTypeColumnAdapter.bindToStmt(
+                    stmtName,
+                    indexVarName,
+                    propertyName,
+                    scope
+                )
+                endControlFlow()
+            }
+        }
+    }
+}
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/RoomTestEnvConfigProvider.kt b/room/room-compiler/src/test/kotlin/androidx/room/RoomTestEnvConfigProvider.kt
index addefbc..761159f 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/RoomTestEnvConfigProvider.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/RoomTestEnvConfigProvider.kt
@@ -21,6 +21,6 @@
 
 class RoomTestEnvConfigProvider : XProcessingEnvironmentTestConfigProvider {
     override fun configure(options: Map<String, String>): XProcessingEnvConfig {
-        return DatabaseProcessingStep.ENV_CONFIG
+        return DatabaseProcessingStep.getEnvConfig(options)
     }
 }
\ No newline at end of file
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
index 558e6c0..f5c3ac3 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
@@ -22,6 +22,7 @@
 import androidx.room.Dao
 import androidx.room.compiler.codegen.XCodeBlock
 import androidx.room.compiler.codegen.XTypeName
+import androidx.room.compiler.codegen.XTypeName.Companion.PRIMITIVE_INT
 import androidx.room.compiler.processing.XProcessingEnv
 import androidx.room.compiler.processing.XRawType
 import androidx.room.compiler.processing.isTypeElement
@@ -62,18 +63,20 @@
 import androidx.room.solver.shortcut.binderprovider.RxCallableUpsertMethodBinderProvider
 import androidx.room.solver.types.BoxedPrimitiveColumnTypeAdapter
 import androidx.room.solver.types.ByteBufferColumnTypeAdapter
+import androidx.room.solver.types.ColumnTypeAdapter
 import androidx.room.solver.types.CompositeAdapter
 import androidx.room.solver.types.CustomTypeConverterWrapper
 import androidx.room.solver.types.EnumColumnTypeAdapter
 import androidx.room.solver.types.PrimitiveColumnTypeAdapter
 import androidx.room.solver.types.SingleStatementTypeConverter
+import androidx.room.solver.types.StringColumnTypeAdapter
 import androidx.room.solver.types.TypeConverter
 import androidx.room.solver.types.UuidColumnTypeAdapter
+import androidx.room.solver.types.ValueClassConverterWrapper
 import androidx.room.testing.context
 import androidx.room.vo.BuiltInConverterFlags
 import androidx.room.vo.ReadQueryMethod
 import com.google.common.truth.Truth.assertThat
-import java.util.UUID
 import org.hamcrest.CoreMatchers.instanceOf
 import org.hamcrest.CoreMatchers.`is`
 import org.hamcrest.CoreMatchers.notNullValue
@@ -221,6 +224,99 @@
     }
 
     @Test
+    fun testKotlinLangValueClassCompilesWithoutError() {
+        val source = Source.kotlin(
+            "Foo.kt",
+            """
+            import androidx.room.*
+            @JvmInline
+            value class IntValueClass(val data: Int)
+            @JvmInline
+            value class StringValueClass(val data: String)
+            class EntityWithValueClass {
+                val intData = IntValueClass(123)
+                val stringData = StringValueClass("bla")
+            }
+            """.trimIndent()
+        )
+        var results: Map<String, String?> = mutableMapOf()
+
+        runProcessorTest(
+            sources = listOf(source)
+        ) { invocation ->
+            val typeAdapterStore = TypeAdapterStore.create(
+                context = invocation.context,
+                builtInConverterFlags = BuiltInConverterFlags.DEFAULT
+            )
+            val subject = invocation.processingEnv.requireTypeElement("EntityWithValueClass")
+            results = subject.getAllFieldsIncludingPrivateSupers().associate { field ->
+                val columnAdapter = typeAdapterStore.findColumnTypeAdapter(
+                    out = field.type,
+                    affinity = null,
+                    false
+                )
+
+                val typeElementColumnAdapter: ColumnTypeAdapter? =
+                    if (columnAdapter is ValueClassConverterWrapper) {
+                        columnAdapter.valueTypeColumnAdapter
+                    } else {
+                        columnAdapter
+                    }
+
+                when (typeElementColumnAdapter) {
+                    is PrimitiveColumnTypeAdapter -> {
+                        field.name to "primitive"
+                    }
+
+                    is StringColumnTypeAdapter -> {
+                        field.name to "string"
+                    }
+
+                    else -> {
+                        field.name to null
+                    }
+                }
+            }
+        }
+        assertThat(results).containsExactlyEntriesIn(
+            mapOf(
+                "intData" to "primitive",
+                "stringData" to "string"
+            )
+        )
+    }
+
+    @Test
+    fun testValueClassWithDifferentTypeVal() {
+        val source = Source.kotlin(
+            "Foo.kt",
+            """
+            import androidx.room.*
+            @JvmInline
+            value class Foo(val value : Int) {
+                val double
+                    get() = value * 2
+            }
+            """.trimIndent()
+        )
+
+        runProcessorTest(
+            sources = listOf(source)
+        ) { invocation ->
+            TypeAdapterStore.create(
+                context = invocation.context,
+                builtInConverterFlags = BuiltInConverterFlags.DEFAULT
+            )
+            val typeElement = invocation
+                .processingEnv
+                .requireTypeElement("Foo")
+            assertThat(typeElement.getDeclaredFields()).hasSize(1)
+            assertThat(typeElement.getDeclaredFields().single().type.asTypeName())
+                .isEqualTo(PRIMITIVE_INT)
+        }
+    }
+
+    @Test
     fun testJavaLangByteBufferCompilesWithoutError() {
         runProcessorTest { invocation ->
             val store = TypeAdapterStore.create(
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt
index 15f4065..7cf8c7f 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt
@@ -2145,4 +2145,46 @@
             expectedFilePath = getTestGoldenPath(testName)
         )
     }
+
+    @Test
+    fun valueClassConverter() {
+        val testName = object {}.javaClass.enclosingMethod!!.name
+        val src = Source.kotlin(
+            "MyDao.kt",
+            """
+            import androidx.room.*
+            import java.util.UUID
+
+            @Dao
+            interface MyDao {
+              @Query("SELECT * FROM MyEntity")
+              fun getEntity(): MyEntity
+
+              @Insert
+              fun addEntity(item: MyEntity)
+            }
+
+            @JvmInline
+            value class LongValueClass(val data: Long)
+
+            @JvmInline
+            value class UUIDValueClass(val data: UUID)
+
+            @JvmInline
+            value class GenericValueClass<T>(val password: T)
+
+            @Entity
+            data class MyEntity (
+                @PrimaryKey
+                val pk: LongValueClass,
+                val uuidData: UUIDValueClass,
+                val genericData: GenericValueClass<String>
+            )
+            """.trimIndent()
+        )
+        runTest(
+            sources = listOf(src, databaseSrc),
+            expectedFilePath = getTestGoldenPath(testName)
+        )
+    }
 }
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/valueClassConverter.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/valueClassConverter.kt
new file mode 100644
index 0000000..0d3b34d
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/valueClassConverter.kt
@@ -0,0 +1,96 @@
+import android.database.Cursor
+import androidx.room.EntityInsertionAdapter
+import androidx.room.RoomDatabase
+import androidx.room.RoomSQLiteQuery
+import androidx.room.RoomSQLiteQuery.Companion.acquire
+import androidx.room.util.convertByteToUUID
+import androidx.room.util.convertUUIDToByte
+import androidx.room.util.getColumnIndexOrThrow
+import androidx.room.util.query
+import androidx.sqlite.db.SupportSQLiteStatement
+import java.lang.Class
+import java.util.UUID
+import javax.`annotation`.processing.Generated
+import kotlin.Int
+import kotlin.Long
+import kotlin.String
+import kotlin.Suppress
+import kotlin.Unit
+import kotlin.collections.List
+import kotlin.jvm.JvmStatic
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["UNCHECKED_CAST", "DEPRECATION", "REDUNDANT_PROJECTION"])
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
+    private val __db: RoomDatabase
+
+    private val __insertionAdapterOfMyEntity: EntityInsertionAdapter<MyEntity>
+    init {
+        this.__db = __db
+        this.__insertionAdapterOfMyEntity = object : EntityInsertionAdapter<MyEntity>(__db) {
+            public override fun createQuery(): String =
+                "INSERT OR ABORT INTO `MyEntity` (`pk`,`uuidData`,`genericData`) VALUES (?,?,?)"
+
+            public override fun bind(statement: SupportSQLiteStatement, entity: MyEntity): Unit {
+                val _data: Long = entity.pk.data
+                statement.bindLong(1, _data)
+                val _data_1: UUID = entity.uuidData.data
+                statement.bindBlob(2, convertUUIDToByte(_data_1))
+                val _password: String = entity.genericData.password
+                statement.bindString(3, _password)
+            }
+        }
+    }
+
+    public override fun addEntity(item: MyEntity): Unit {
+        __db.assertNotSuspendingTransaction()
+        __db.beginTransaction()
+        try {
+            __insertionAdapterOfMyEntity.insert(item)
+            __db.setTransactionSuccessful()
+        } finally {
+            __db.endTransaction()
+        }
+    }
+
+    public override fun getEntity(): MyEntity {
+        val _sql: String = "SELECT * FROM MyEntity"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, false, null)
+        try {
+            val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
+            val _cursorIndexOfUuidData: Int = getColumnIndexOrThrow(_cursor, "uuidData")
+            val _cursorIndexOfGenericData: Int = getColumnIndexOrThrow(_cursor, "genericData")
+            val _result: MyEntity
+            if (_cursor.moveToFirst()) {
+                val _tmpPk: LongValueClass
+                val _data: Long
+                _data = _cursor.getLong(_cursorIndexOfPk)
+                _tmpPk = LongValueClass(_data)
+                val _tmpUuidData: UUIDValueClass
+                val _data_1: UUID
+                _data_1 = convertByteToUUID(_cursor.getBlob(_cursorIndexOfUuidData))
+                _tmpUuidData = UUIDValueClass(_data_1)
+                val _tmpGenericData: GenericValueClass<String>
+                val _password: String
+                _password = _cursor.getString(_cursorIndexOfGenericData)
+                _tmpGenericData = GenericValueClass<String>(_password)
+                _result = MyEntity(_tmpPk,_tmpUuidData,_tmpGenericData)
+            } else {
+                error("Cursor was empty, but expected a single item.")
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    public companion object {
+        @JvmStatic
+        public fun getRequiredConverters(): List<Class<*>> = emptyList()
+    }
+}
\ No newline at end of file