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)));
+ }
+}