Merge "Optimization for storing enums in databases." into androidx-master-dev
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XType.kt
index 8ba6067..db41d40 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XType.kt
@@ -134,6 +134,11 @@
     fun isType(): Boolean
 
     /**
+     * Returns true if this represented by an [Enum].
+     */
+    fun isEnum(): Boolean
+
+    /**
      * Returns `true` if this is the same raw type as [other]
      */
     fun isTypeOf(other: KClass<*>): Boolean
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacType.kt
index de5dc66..dc9a6be 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacType.kt
@@ -25,6 +25,7 @@
 import androidx.room.compiler.processing.safeTypeName
 import com.google.auto.common.MoreTypes
 import com.squareup.javapoet.TypeName
+import javax.lang.model.element.ElementKind
 import javax.lang.model.type.TypeKind
 import javax.lang.model.type.TypeMirror
 import kotlin.reflect.KClass
@@ -138,4 +139,7 @@
         private val BOXED_LONG = TypeName.LONG.box()
         private val BOXED_BYTE = TypeName.BYTE.box()
     }
+
+    override fun isEnum() = typeMirror.kind == TypeKind.DECLARED &&
+        MoreTypes.asElement(typeMirror).kind == ElementKind.ENUM
 }
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspArrayType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspArrayType.kt
index 65e2a2c..25e25cc 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspArrayType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspArrayType.kt
@@ -18,9 +18,9 @@
 
 import androidx.room.compiler.processing.XArrayType
 import androidx.room.compiler.processing.XType
+import com.google.devtools.ksp.symbol.KSType
 import com.squareup.javapoet.ArrayTypeName
 import com.squareup.javapoet.TypeName
-import com.google.devtools.ksp.symbol.KSType
 
 internal class KspArrayType(
     env: KspProcessingEnv,
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt
index 3b825dd..12d2be9 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt
@@ -20,6 +20,7 @@
 import androidx.room.compiler.processing.XNullability
 import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.XTypeElement
+import com.google.devtools.ksp.symbol.ClassKind
 import com.google.devtools.ksp.symbol.KSClassDeclaration
 import com.google.devtools.ksp.symbol.KSType
 import com.google.devtools.ksp.symbol.KSTypeReference
@@ -160,4 +161,8 @@
     override fun toString(): String {
         return ksType.toString()
     }
+
+    override fun isEnum(): Boolean {
+        return (ksType.declaration as? KSClassDeclaration)?.classKind == ClassKind.ENUM_CLASS
+    }
 }
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeArgumentType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeArgumentType.kt
index 011b54a..a65c976 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeArgumentType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeArgumentType.kt
@@ -16,9 +16,9 @@
 
 package androidx.room.compiler.processing.ksp
 
-import com.squareup.javapoet.TypeName
 import com.google.devtools.ksp.symbol.KSTypeArgument
 import com.google.devtools.ksp.symbol.KSTypeParameter
+import com.squareup.javapoet.TypeName
 
 /**
  * The typeName for type arguments requires the type parameter, hence we have a special type
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt b/room/compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
index d79a71c..f4cf3a5 100644
--- a/room/compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
@@ -16,16 +16,16 @@
 
 package androidx.room.solver
 
-import androidx.room.ext.CommonTypeNames
-import androidx.room.ext.GuavaBaseTypeNames
-import androidx.room.ext.isEntityElement
-import androidx.room.parser.ParsedQuery
-import androidx.room.parser.SQLTypeAffinity
 import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.asDeclaredType
 import androidx.room.compiler.processing.isArray
 import androidx.room.compiler.processing.isDeclared
+import androidx.room.ext.CommonTypeNames
+import androidx.room.ext.GuavaBaseTypeNames
+import androidx.room.ext.isEntityElement
 import androidx.room.ext.isNotByte
+import androidx.room.parser.ParsedQuery
+import androidx.room.parser.SQLTypeAffinity
 import androidx.room.processor.Context
 import androidx.room.processor.EntityProcessor
 import androidx.room.processor.FieldProcessor
@@ -86,6 +86,7 @@
 import androidx.room.solver.types.CompositeAdapter
 import androidx.room.solver.types.CompositeTypeConverter
 import androidx.room.solver.types.CursorValueReader
+import androidx.room.solver.types.EnumColumnTypeAdapter
 import androidx.room.solver.types.NoOpConverter
 import androidx.room.solver.types.PrimitiveBooleanToIntConverter
 import androidx.room.solver.types.PrimitiveColumnTypeAdapter
@@ -218,13 +219,26 @@
         if (adapter != null) {
             return adapter
         }
-        val targetTypes = targetTypeMirrorsFor(affinity)
-        val binder = findTypeConverter(input, targetTypes) ?: return null
-        // columnAdapter should not be null but we are receiving errors on crash in `first()` so
-        // this safeguard allows us to dispatch the real problem to the user (e.g. why we couldn't
-        // find the right adapter)
-        val columnAdapter = getAllColumnAdapters(binder.to).firstOrNull() ?: return null
-        return CompositeAdapter(input, columnAdapter, binder, null)
+
+        fun findTypeConverterAdapter(): ColumnTypeAdapter? {
+            val targetTypes = targetTypeMirrorsFor(affinity)
+            val binder = findTypeConverter(input, targetTypes) ?: return null
+            // columnAdapter should not be null but we are receiving errors on crash in `first()` so
+            // this safeguard allows us to dispatch the real problem to the user (e.g. why we couldn't
+            // find the right adapter)
+            val columnAdapter = getAllColumnAdapters(binder.to).firstOrNull() ?: return null
+            return CompositeAdapter(input, columnAdapter, binder, null)
+        }
+
+        val adapterByTypeConverter = findTypeConverterAdapter()
+        if (adapterByTypeConverter != null) {
+            return adapterByTypeConverter
+        }
+        val enumAdapter = createEnumTypeAdapter(input)
+        if (enumAdapter != null) {
+            return enumAdapter
+        }
+        return null
     }
 
     /**
@@ -251,13 +265,28 @@
             // two way is better
             return adapter
         }
+
+        fun findTypeConverterAdapter(): ColumnTypeAdapter? {
+            val targetTypes = targetTypeMirrorsFor(affinity)
+            val converter = findTypeConverter(targetTypes, output) ?: return null
+            return CompositeAdapter(
+                output,
+                getAllColumnAdapters(converter.from).first(), null, converter
+            )
+        }
+
         // we could not find a two way version, search for anything
-        val targetTypes = targetTypeMirrorsFor(affinity)
-        val converter = findTypeConverter(targetTypes, output) ?: return null
-        return CompositeAdapter(
-            output,
-            getAllColumnAdapters(converter.from).first(), null, converter
-        )
+        val typeConverterAdapter = findTypeConverterAdapter()
+        if (typeConverterAdapter != null) {
+            return typeConverterAdapter
+        }
+
+        val enumAdapter = createEnumTypeAdapter(output)
+        if (enumAdapter != null) {
+            return enumAdapter
+        }
+
+        return null
     }
 
     /**
@@ -293,15 +322,34 @@
         if (adapter != null) {
             return adapter
         }
-        val targetTypes = targetTypeMirrorsFor(affinity)
-        val intoStatement = findTypeConverter(out, targetTypes) ?: return null
-        // ok found a converter, try the reverse now
-        val fromCursor = reverse(intoStatement) ?: findTypeConverter(intoStatement.to, out)
-            ?: return null
-        return CompositeAdapter(
-            out, getAllColumnAdapters(intoStatement.to).first(), intoStatement,
-            fromCursor
-        )
+
+        fun findTypeConverterAdapter(): ColumnTypeAdapter? {
+            val targetTypes = targetTypeMirrorsFor(affinity)
+            val intoStatement = findTypeConverter(out, targetTypes) ?: return null
+            // ok found a converter, try the reverse now
+            val fromCursor = reverse(intoStatement) ?: findTypeConverter(intoStatement.to, out)
+                ?: return null
+            return CompositeAdapter(
+                out, getAllColumnAdapters(intoStatement.to).first(), intoStatement, fromCursor
+            )
+        }
+
+        val adapterByTypeConverter = findTypeConverterAdapter()
+        if (adapterByTypeConverter != null) {
+            return adapterByTypeConverter
+        }
+        val enumAdapter = createEnumTypeAdapter(out)
+        if (enumAdapter != null) {
+            return enumAdapter
+        }
+        return null
+    }
+
+    private fun createEnumTypeAdapter(type: XType): ColumnTypeAdapter? {
+        if (type.isEnum()) {
+            return EnumColumnTypeAdapter(type)
+        }
+        return null
     }
 
     private fun findDirectAdapterFor(
@@ -513,17 +561,17 @@
         if (typeMirror.isType() &&
             context.COMMON_TYPES.COLLECTION.rawType.isAssignableFrom(typeMirror)
         ) {
+            val collectionBinder = findStatementValueBinder(typeMirror, null)
+            if (collectionBinder != null) {
+                // user has a converter for the collection itself
+                return BasicQueryParameterAdapter(collectionBinder)
+            }
+
             val declared = typeMirror.asDeclaredType()
             val binder = findStatementValueBinder(
                 declared.typeArguments.first().extendsBoundOrSelf(), null
-            )
-            if (binder != null) {
-                return CollectionQueryParameterAdapter(binder)
-            } else {
-                // maybe user wants to convert this collection themselves. look for a match
-                val collectionBinder = findStatementValueBinder(typeMirror, null) ?: return null
-                return BasicQueryParameterAdapter(collectionBinder)
-            }
+            ) ?: return null
+            return CollectionQueryParameterAdapter(binder)
         } else if (typeMirror.isArray() && typeMirror.componentType.isNotByte()) {
             val component = typeMirror.componentType
             val binder = findStatementValueBinder(component, null) ?: return null
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/types/EnumColumnTypeAdapter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/types/EnumColumnTypeAdapter.kt
new file mode 100644
index 0000000..1194b1d
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/types/EnumColumnTypeAdapter.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2020 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.ext.T
+import androidx.room.parser.SQLTypeAffinity.TEXT
+import androidx.room.solver.CodeGenScope
+
+/**
+ * Uses enum string representation.
+ */
+class EnumColumnTypeAdapter(out: XType) :
+    ColumnTypeAdapter(out, TEXT) {
+    private val enumTypeName = out.typeName
+    override fun readFromCursor(
+        outVarName: String,
+        cursorVarName: String,
+        indexVarName: String,
+        scope: CodeGenScope
+    ) {
+        scope.builder()
+            .addStatement(
+                "$L = $T.valueOf($L.getString($L))", outVarName, enumTypeName,
+                cursorVarName,
+                indexVarName
+            )
+    }
+
+    override fun bindToStmt(
+        stmtName: String,
+        indexVarName: String,
+        valueVarName: String,
+        scope: CodeGenScope
+    ) {
+        scope.builder().apply {
+            beginControlFlow("if ($L == null)", valueVarName)
+                .addStatement("$L.bindNull($L)", stmtName, indexVarName)
+            nextControlFlow("else")
+                .addStatement("$L.bindString($L, $L.name())", stmtName, indexVarName, valueVarName)
+            endControlFlow()
+        }
+    }
+}
\ No newline at end of file
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/DaoProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/DaoProcessorTest.kt
index 085b391..1f1d6cb 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/DaoProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/DaoProcessorTest.kt
@@ -50,6 +50,24 @@
     }
 
     @Test
+    fun testUnusedEnumCompilesWithoutError() {
+        singleDao(
+            """
+                @Dao abstract class MyDao {
+                    @Query("SELECT uid FROM User")
+                    abstract int[] getIds();
+                    enum Fruit {
+                        APPLE,
+                        BANANA,
+                        STRAWBERRY
+                    }
+                }
+                """
+        ) { _, _ ->
+        }.compilesWithoutError()
+    }
+
+    @Test
     fun testNonAbstract() {
         singleDao("@Dao public class MyDao {}") { _, _ -> }
             .failsToCompile()
diff --git a/room/compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt b/room/compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
index e520c12..3d437fc 100644
--- a/room/compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
@@ -20,6 +20,8 @@
 import androidx.paging.DataSource
 import androidx.paging.PagingSource
 import androidx.room.Entity
+import androidx.room.compiler.processing.XProcessingEnv
+import androidx.room.compiler.processing.asDeclaredType
 import androidx.room.ext.GuavaUtilConcurrentTypeNames
 import androidx.room.ext.L
 import androidx.room.ext.LifecyclesTypeNames
@@ -30,8 +32,6 @@
 import androidx.room.ext.RxJava3TypeNames
 import androidx.room.ext.T
 import androidx.room.parser.SQLTypeAffinity
-import androidx.room.compiler.processing.XProcessingEnv
-import androidx.room.compiler.processing.asDeclaredType
 import androidx.room.processor.Context
 import androidx.room.processor.ProcessorErrors
 import androidx.room.solver.binderprovider.DataSourceFactoryQueryResultBinderProvider
@@ -44,6 +44,7 @@
 import androidx.room.solver.shortcut.binderprovider.RxCallableDeleteOrUpdateMethodBinderProvider
 import androidx.room.solver.shortcut.binderprovider.RxCallableInsertMethodBinderProvider
 import androidx.room.solver.types.CompositeAdapter
+import androidx.room.solver.types.EnumColumnTypeAdapter
 import androidx.room.solver.types.TypeConverter
 import androidx.room.testing.TestInvocation
 import androidx.room.testing.TestProcessor
@@ -103,6 +104,30 @@
     }
 
     @Test
+    fun testJavaLangEnumCompilesWithoutError() {
+        simpleRun(
+            JavaFileObjects.forSourceString(
+                "foo.bar.Fruit",
+                """ package foo.bar;
+                import androidx.room.*;
+                enum Fruit {
+                    APPLE,
+                    BANANA,
+                    STRAWBERRY}
+                """.trimMargin()
+            )
+        ) { invocation ->
+            val store = TypeAdapterStore.create(Context(invocation.processingEnv))
+            val enum = invocation
+                .processingEnv
+                .requireType("foo.bar.Fruit")
+            val adapter = store.findColumnTypeAdapter(enum, null)
+            assertThat(adapter, notNullValue())
+            assertThat(adapter, instanceOf(EnumColumnTypeAdapter::class.java))
+        }.compilesWithoutError()
+    }
+
+    @Test
     fun testVia1TypeAdapter() {
         singleRun { invocation ->
             val store = TypeAdapterStore.create(Context(invocation.processingEnv))
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/EnumColumnTypeAdapterTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/EnumColumnTypeAdapterTest.java
new file mode 100644
index 0000000..4c0df94
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/EnumColumnTypeAdapterTest.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2020 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.testapp.test;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+import android.content.Context;
+
+import androidx.room.Dao;
+import androidx.room.Database;
+import androidx.room.Entity;
+import androidx.room.PrimaryKey;
+import androidx.room.Query;
+import androidx.room.Room;
+import androidx.room.RoomDatabase;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class EnumColumnTypeAdapterTest {
+
+    private EnumColumnTypeAdapterDatabase mDb;
+
+    @Entity
+    public static class EntityWithEnum {
+        @PrimaryKey
+        public Long id;
+        public Fruit fruit;
+    }
+
+    public enum Fruit {
+        BANANA,
+        STRAWBERRY,
+        WILDBERRY
+    }
+
+    @Dao
+    public interface SampleDao {
+        @Query("INSERT INTO EntityWithEnum (id, fruit) VALUES (:id, :fruit)")
+        long insert(long id, Fruit fruit);
+
+        @Query("SELECT * FROM EntityWithEnum WHERE id = :id")
+        EntityWithEnum getValueWithId(long id);
+    }
+
+    @Database(entities = {EntityWithEnum.class}, version = 1, exportSchema = false)
+    public abstract static class EnumColumnTypeAdapterDatabase extends RoomDatabase {
+        public abstract EnumColumnTypeAdapterTest.SampleDao dao();
+    }
+
+    @Before
+    public void initDb() {
+        Context context = ApplicationProvider.getApplicationContext();
+        mDb = Room.inMemoryDatabaseBuilder(
+                context,
+                EnumColumnTypeAdapterDatabase.class)
+                .build();
+    }
+
+    @Test
+    public void readAndWriteEnumToDatabase() {
+        final long id1 = mDb.dao().insert(1, Fruit.BANANA);
+        final long id2 = mDb.dao().insert(2, Fruit.STRAWBERRY);
+
+        assertThat(mDb.dao().getValueWithId(1).fruit, is(equalTo(Fruit.BANANA)));
+        assertThat(mDb.dao().getValueWithId(2).fruit, is(equalTo(Fruit.STRAWBERRY)));
+    }
+}