Allow collection type converters in query parameters

If a query parameter is a collection AND we cannot find a converter for the type parameter of it,
we will look for a converter that converts the whole thing.

Collection Type converters were broken in kotlin data classes because
kotlin creates constructor args with variance which cannot be assigned
to the fields. This CL flexes the constructor and setter check to allow
variances.

Bug: 69164099
Test: SimpleEntityReadWriteTest, CustomTypeConverterResolutionTest, BookDaoTest,
TypeAssignmentTest
Change-Id: I20eae401ca7f19a7acba2958f3601744aef1f7be
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/ext/element_ext.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/ext/element_ext.kt
index 5f61e21..e9245cc 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/ext/element_ext.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/ext/element_ext.kt
@@ -30,7 +30,10 @@
 import javax.lang.model.element.VariableElement
 import javax.lang.model.type.TypeKind
 import javax.lang.model.type.TypeMirror
+import javax.lang.model.type.WildcardType
 import javax.lang.model.util.SimpleAnnotationValueVisitor6
+import javax.lang.model.util.SimpleTypeVisitor7
+import javax.lang.model.util.Types
 import kotlin.reflect.KClass
 
 fun Element.hasAnyOf(vararg modifiers: Modifier): Boolean {
@@ -161,3 +164,50 @@
 fun AnnotationValue.getAsStringList(): List<String> {
     return ANNOTATION_VALUE_STRING_ARR_VISITOR.visit(this)
 }
+
+// a variant of Types.isAssignable that ignores variance.
+fun Types.isAssignableWithoutVariance(from: TypeMirror, to: TypeMirror): Boolean {
+    val assignable = isAssignable(from, to)
+    if (assignable) {
+        return true
+    }
+    if (from.kind != TypeKind.DECLARED || to.kind != TypeKind.DECLARED) {
+        return false
+    }
+    val declaredFrom = MoreTypes.asDeclared(from)
+    val declaredTo = MoreTypes.asDeclared(to)
+    val fromTypeArgs = declaredFrom.typeArguments
+    val toTypeArgs = declaredTo.typeArguments
+    // no type arguments, we don't need extra checks
+    if (fromTypeArgs.isEmpty() || fromTypeArgs.size != toTypeArgs.size) {
+        return false
+    }
+    // check erasure version first, if it does not match, no reason to proceed
+    if (!isAssignable(erasure(from), erasure(to))) {
+        return false
+    }
+    // convert from args to their upper bounds if it exists
+    val fromUpperBounds = fromTypeArgs.map {
+        it.getUpperBound()
+    }
+    // if there are no upper bound conversions, return.
+    if (fromUpperBounds.all { it == null }) {
+        return false
+    }
+    // try to move the types of the from to their upper bounds. It does not matter for the "to"
+    // because Types.isAssignable handles it as it is valid java
+    return (0 until fromTypeArgs.size).all { index ->
+        isAssignableWithoutVariance(
+                from = fromUpperBounds[index] ?: fromTypeArgs[index],
+                to = toTypeArgs[index])
+    }
+}
+
+// converts ? in Set< ? extends Foo> to Foo
+private fun TypeMirror.getUpperBound(): TypeMirror? {
+    return this.accept(object : SimpleTypeVisitor7<TypeMirror, Void?>() {
+        override fun visitWildcard(type: WildcardType, ignored: Void?): TypeMirror {
+            return type.extendsBound
+        }
+    }, null)
+}
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/PojoProcessor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/PojoProcessor.kt
index cb63b6d..f3b505d 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/PojoProcessor.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/PojoProcessor.kt
@@ -26,6 +26,7 @@
 import android.arch.persistence.room.ext.getAsStringList
 import android.arch.persistence.room.ext.hasAnnotation
 import android.arch.persistence.room.ext.hasAnyOf
+import android.arch.persistence.room.ext.isAssignableWithoutVariance
 import android.arch.persistence.room.ext.isCollection
 import android.arch.persistence.room.ext.toClassType
 import android.arch.persistence.room.processor.ProcessorErrors.CANNOT_FIND_GETTER_FOR_FIELD
@@ -216,7 +217,8 @@
                     } else if (!field.nameWithVariations.contains(paramName)) {
                         false
                     } else {
-                        typeUtils.isAssignable(paramType, field.type)
+                        // see: b/69164099
+                        typeUtils.isAssignableWithoutVariance(paramType, field.type)
                     }
                 }
 
@@ -555,7 +557,8 @@
 
         val matching = candidates
                 .filter {
-                    types.isAssignable(getType(it), field.element.asType())
+                    // b/69164099
+                    types.isAssignableWithoutVariance(getType(it), field.element.asType())
                             && (field.nameWithVariations.contains(it.simpleName.toString())
                             || nameVariations.contains(it.simpleName.toString()))
                 }
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/TypeAdapterStore.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/TypeAdapterStore.kt
index dcd5ee6..d64300c 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/TypeAdapterStore.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/TypeAdapterStore.kt
@@ -362,8 +362,14 @@
                 || MoreTypes.isTypeOf(java.util.Set::class.java, typeMirror))) {
             val declared = MoreTypes.asDeclared(typeMirror)
             val binder = findStatementValueBinder(declared.typeArguments.first(),
-                    null) ?: return null
-            return CollectionQueryParameterAdapter(binder)
+                    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)
+            }
         } else if (typeMirror is ArrayType && typeMirror.componentType.kind != TypeKind.BYTE) {
             val component = typeMirror.componentType
             val binder = findStatementValueBinder(component, null) ?: return null
@@ -439,8 +445,10 @@
     private fun getAllTypeConverters(input: TypeMirror, excludes: List<TypeMirror>):
             List<TypeConverter> {
         val types = context.processingEnv.typeUtils
+        // for input, check assignability because it defines whether we can use the method or not.
+        // for excludes, use exact match
         return typeConverters.filter { converter ->
-            types.isSameType(input, converter.from) &&
+            types.isAssignable(input, converter.from) &&
                     !excludes.any { types.isSameType(it, converter.to) }
         }
     }
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/solver/CustomTypeConverterResolutionTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/solver/CustomTypeConverterResolutionTest.kt
index d9b4997..ae635b2 100644
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/solver/CustomTypeConverterResolutionTest.kt
+++ b/room/compiler/src/test/kotlin/android/arch/persistence/room/solver/CustomTypeConverterResolutionTest.kt
@@ -39,6 +39,7 @@
 import com.squareup.javapoet.FieldSpec
 import com.squareup.javapoet.MethodSpec
 import com.squareup.javapoet.ParameterSpec
+import com.squareup.javapoet.ParameterizedTypeName
 import com.squareup.javapoet.TypeName
 import com.squareup.javapoet.TypeSpec
 import org.junit.Test
@@ -83,6 +84,26 @@
                     }
                 }
                 """)
+        val CUSTOM_TYPE_SET = ParameterizedTypeName.get(
+                ClassName.get(Set::class.java), CUSTOM_TYPE)
+        val CUSTOM_TYPE_SET_CONVERTER = ClassName.get("foo.bar", "MySetConverter")
+        val CUSTOM_TYPE_SET_CONVERTER_JFO = JavaFileObjects.forSourceLines(
+                CUSTOM_TYPE_SET_CONVERTER.toString(),
+                """
+                package ${CUSTOM_TYPE_SET_CONVERTER.packageName()};
+                import java.util.HashSet;
+                import java.util.Set;
+                public class ${CUSTOM_TYPE_SET_CONVERTER.simpleName()} {
+                    @${TypeConverter::class.java.canonicalName}
+                    public static $CUSTOM_TYPE_SET toCustom(int value) {
+                        return null;
+                    }
+                    @${TypeConverter::class.java.canonicalName}
+                    public static int fromCustom($CUSTOM_TYPE_SET input) {
+                        return 0;
+                    }
+                }
+                """)
     }
 
     @Test
@@ -94,6 +115,36 @@
     }
 
     @Test
+    fun collection_forEntity() {
+        val entity = createEntity(
+                hasCustomField = true,
+                useCollection = true)
+        val database = createDatabase(
+                hasConverters = true,
+                hasDao = true,
+                useCollection = true)
+        val dao = createDao(
+                hasQueryWithCustomParam = false,
+                useCollection = true)
+        run(entity.toJFO(), dao.toJFO(), database.toJFO()).compilesWithoutError()
+    }
+
+    @Test
+    fun collection_forDao() {
+        val entity = createEntity(
+                hasCustomField = true,
+                useCollection = true)
+        val database = createDatabase(
+                hasConverters = true,
+                hasDao = true,
+                useCollection = true)
+        val dao = createDao(
+                hasQueryWithCustomParam = true,
+                useCollection = true)
+        run(entity.toJFO(), dao.toJFO(), database.toJFO()).compilesWithoutError()
+    }
+
+    @Test
     fun useFromDatabase_forQueryParameter() {
         val entity = createEntity()
         val database = createDatabase(hasConverters = true, hasDao = true)
@@ -170,21 +221,29 @@
 
     fun run(vararg jfos: JavaFileObject): CompileTester {
         return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
-                .that(jfos.toList() + CUSTOM_TYPE_JFO + CUSTOM_TYPE_CONVERTER_JFO)
+                .that(jfos.toList() + CUSTOM_TYPE_JFO + CUSTOM_TYPE_CONVERTER_JFO
+                        + CUSTOM_TYPE_SET_CONVERTER_JFO)
                 .processedWith(RoomProcessor())
     }
 
-    private fun createEntity(hasCustomField: Boolean = false,
-                             hasConverters: Boolean = false,
-                             hasConverterOnField: Boolean = false): TypeSpec {
+    private fun createEntity(
+            hasCustomField: Boolean = false,
+            hasConverters: Boolean = false,
+            hasConverterOnField: Boolean = false,
+            useCollection: Boolean = false): TypeSpec {
         if (hasConverterOnField && hasConverters) {
             throw IllegalArgumentException("cannot have both converters")
         }
+        val type = if (useCollection) {
+            CUSTOM_TYPE_SET
+        } else {
+            CUSTOM_TYPE
+        }
         return TypeSpec.classBuilder(ENTITY).apply {
             addAnnotation(Entity::class.java)
             addModifiers(Modifier.PUBLIC)
             if (hasCustomField) {
-                addField(FieldSpec.builder(CUSTOM_TYPE, "myCustomField", Modifier.PUBLIC).apply {
+                addField(FieldSpec.builder(type, "myCustomField", Modifier.PUBLIC).apply {
                     if (hasConverterOnField) {
                         addAnnotation(createConvertersAnnotation())
                     }
@@ -199,13 +258,15 @@
         }.build()
     }
 
-    private fun createDatabase(hasConverters: Boolean = false,
-                               hasDao: Boolean = false): TypeSpec {
+    private fun createDatabase(
+            hasConverters: Boolean = false,
+            hasDao: Boolean = false,
+            useCollection: Boolean = false): TypeSpec {
         return TypeSpec.classBuilder(DB).apply {
             addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC)
             superclass(RoomTypeNames.ROOM_DB)
             if (hasConverters) {
-                addAnnotation(createConvertersAnnotation())
+                addAnnotation(createConvertersAnnotation(useCollection = useCollection))
             }
             addField(FieldSpec.builder(TypeName.INT, "id", Modifier.PUBLIC).apply {
                 addAnnotation(PrimaryKey::class.java)
@@ -225,11 +286,13 @@
         }.build()
     }
 
-    private fun createDao(hasConverters: Boolean = false,
-                          hasQueryReturningEntity: Boolean = false,
-                          hasQueryWithCustomParam: Boolean = false,
-                          hasMethodConverters: Boolean = false,
-                          hasParameterConverters: Boolean = false): TypeSpec {
+    private fun createDao(
+            hasConverters: Boolean = false,
+            hasQueryReturningEntity: Boolean = false,
+            hasQueryWithCustomParam: Boolean = false,
+            hasMethodConverters: Boolean = false,
+            hasParameterConverters: Boolean = false,
+            useCollection: Boolean = false): TypeSpec {
         val annotationCount = listOf(hasMethodConverters, hasConverters, hasParameterConverters)
                 .map { if (it) 1 else 0 }.sum()
         if (annotationCount > 1) {
@@ -242,7 +305,7 @@
             addAnnotation(Dao::class.java)
             addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC)
             if (hasConverters) {
-                addAnnotation(createConvertersAnnotation())
+                addAnnotation(createConvertersAnnotation(useCollection = useCollection))
             }
             if (hasQueryReturningEntity) {
                 addMethod(MethodSpec.methodBuilder("loadAll").apply {
@@ -253,18 +316,23 @@
                     returns(ENTITY)
                 }.build())
             }
+            val customType = if (useCollection) {
+                CUSTOM_TYPE_SET
+            } else {
+                CUSTOM_TYPE
+            }
             if (hasQueryWithCustomParam) {
                 addMethod(MethodSpec.methodBuilder("queryWithCustom").apply {
                     addAnnotation(AnnotationSpec.builder(Query::class.java).apply {
                         addMember("value", S, "SELECT COUNT(*) FROM ${ENTITY.simpleName()} where" +
-                                " id IN(:customs)")
+                                " id = :custom")
                     }.build())
                     if (hasMethodConverters) {
-                        addAnnotation(createConvertersAnnotation())
+                        addAnnotation(createConvertersAnnotation(useCollection = useCollection))
                     }
-                    addParameter(ParameterSpec.builder(CUSTOM_TYPE, "customs").apply {
+                    addParameter(ParameterSpec.builder(customType, "custom").apply {
                         if (hasParameterConverters) {
-                            addAnnotation(createConvertersAnnotation())
+                            addAnnotation(createConvertersAnnotation(useCollection = useCollection))
                         }
                     }.build())
                     addModifiers(Modifier.ABSTRACT)
@@ -274,8 +342,13 @@
         }.build()
     }
 
-    private fun createConvertersAnnotation(): AnnotationSpec {
+    private fun createConvertersAnnotation(useCollection: Boolean = false): AnnotationSpec {
+        val converter = if (useCollection) {
+            CUSTOM_TYPE_SET_CONVERTER
+        } else {
+            CUSTOM_TYPE_CONVERTER
+        }
         return AnnotationSpec.builder(TypeConverters::class.java)
-                .addMember("value", "$T.class", CUSTOM_TYPE_CONVERTER).build()
+                .addMember("value", "$T.class", converter).build()
     }
 }
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/solver/TypeAssignmentTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/solver/TypeAssignmentTest.kt
new file mode 100644
index 0000000..0945ae1
--- /dev/null
+++ b/room/compiler/src/test/kotlin/android/arch/persistence/room/solver/TypeAssignmentTest.kt
@@ -0,0 +1,113 @@
+/*
+ * 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 android.arch.persistence.room.solver
+
+import android.arch.persistence.room.ext.getAllFieldsIncludingPrivateSupers
+import android.arch.persistence.room.ext.isAssignableWithoutVariance
+import android.arch.persistence.room.testing.TestInvocation
+import com.google.testing.compile.JavaFileObjects
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Test
+import simpleRun
+import javax.annotation.processing.ProcessingEnvironment
+import javax.lang.model.element.TypeElement
+import javax.lang.model.element.VariableElement
+
+class TypeAssignmentTest {
+    companion object {
+        private val TEST_OBJECT = JavaFileObjects.forSourceString("foo.bar.MyObject",
+                """
+            package foo.bar;
+            import java.util.Set;
+            import java.util.HashSet;
+            class MyObject {
+                String mString;
+                Integer mInteger;
+                Set<MyObject> mSet;
+                Set<? extends MyObject> mVarianceSet;
+                HashSet<MyObject> mHashSet;
+            }
+            """.trimIndent())
+    }
+
+    @Test
+    fun basic() {
+        runTest {
+            val testObject = typeElement("foo.bar.MyObject")
+            val string = testObject.getField(processingEnv, "mString")
+            val integer = testObject.getField(processingEnv, "mInteger")
+            assertThat(typeUtils.isAssignableWithoutVariance(string.asType(),
+                    integer.asType()),
+                    `is`(false))
+        }
+    }
+
+    @Test
+    fun generics() {
+        runTest {
+            val testObject = typeElement("foo.bar.MyObject")
+            val set = testObject.getField(processingEnv, "mSet").asType()
+            val hashSet = testObject.getField(processingEnv, "mHashSet").asType()
+            assertThat(typeUtils.isAssignableWithoutVariance(
+                    from = set,
+                    to = hashSet
+            ), `is`(false))
+            assertThat(typeUtils.isAssignableWithoutVariance(
+                    from = hashSet,
+                    to = set
+            ), `is`(true))
+        }
+    }
+
+    @Test
+    fun variance() {
+        /**
+         *  Set<User> userSet = null;
+         *  Set<? extends User> userSet2 = null;
+         *  userSet = userSet2;  // NOT OK for java but kotlin data classes hit this so we want
+         *                       // to accept it
+         */
+        runTest {
+            val testObject = typeElement("foo.bar.MyObject")
+            val set = testObject.getField(processingEnv, "mSet").asType()
+            val varianceSet = testObject.getField(processingEnv, "mVarianceSet").asType()
+            assertThat(typeUtils.isAssignableWithoutVariance(
+                    from = set,
+                    to = varianceSet
+            ), `is`(true))
+            assertThat(typeUtils.isAssignableWithoutVariance(
+                    from = varianceSet,
+                    to = set
+            ), `is`(true))
+        }
+    }
+
+    private fun TypeElement.getField(
+            env: ProcessingEnvironment,
+            name: String): VariableElement {
+        return getAllFieldsIncludingPrivateSupers(env).first {
+            it.simpleName.toString() == name
+        }
+    }
+
+    private fun runTest(handler: TestInvocation.() -> Unit) {
+        simpleRun(TEST_OBJECT) {
+            it.apply { handler() }
+        }.compilesWithoutError()
+    }
+}
\ No newline at end of file
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/testing/TestInvocation.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/testing/TestInvocation.kt
index 86b6e01..e03f1db 100644
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/testing/TestInvocation.kt
+++ b/room/compiler/src/test/kotlin/android/arch/persistence/room/testing/TestInvocation.kt
@@ -29,4 +29,6 @@
     fun typeElement(qName: String): TypeElement {
         return processingEnv.elementUtils.getTypeElement(qName)
     }
+
+    val typeUtils by lazy { processingEnv.typeUtils }
 }
diff --git a/room/integration-tests/kotlintestapp/schemas/android.arch.persistence.room.integration.kotlintestapp.TestDatabase/1.json b/room/integration-tests/kotlintestapp/schemas/android.arch.persistence.room.integration.kotlintestapp.TestDatabase/1.json
index e6bb21c..04e7cad 100644
--- a/room/integration-tests/kotlintestapp/schemas/android.arch.persistence.room.integration.kotlintestapp.TestDatabase/1.json
+++ b/room/integration-tests/kotlintestapp/schemas/android.arch.persistence.room.integration.kotlintestapp.TestDatabase/1.json
@@ -2,11 +2,11 @@
   "formatVersion": 1,
   "database": {
     "version": 1,
-    "identityHash": "933c7e2810b0f89ab84faa68bbea5852",
+    "identityHash": "c7e8b9f03366a1b0a03ab28b7c9c5ad7",
     "entities": [
       {
         "tableName": "Book",
-        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookId` TEXT NOT NULL, `title` TEXT NOT NULL, `bookPublisherId` TEXT NOT NULL, PRIMARY KEY(`bookId`), FOREIGN KEY(`bookPublisherId`) REFERENCES `Publisher`(`publisherId`) ON UPDATE NO ACTION ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED)",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookId` TEXT NOT NULL, `title` TEXT NOT NULL, `bookPublisherId` TEXT NOT NULL, `languages` INTEGER NOT NULL, PRIMARY KEY(`bookId`), FOREIGN KEY(`bookPublisherId`) REFERENCES `Publisher`(`publisherId`) ON UPDATE NO ACTION ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED)",
         "fields": [
           {
             "fieldPath": "bookId",
@@ -25,6 +25,12 @@
             "columnName": "bookPublisherId",
             "affinity": "TEXT",
             "notNull": true
+          },
+          {
+            "fieldPath": "languages",
+            "columnName": "languages",
+            "affinity": "INTEGER",
+            "notNull": true
           }
         ],
         "primaryKey": {
@@ -191,7 +197,7 @@
     ],
     "setupQueries": [
       "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
-      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"933c7e2810b0f89ab84faa68bbea5852\")"
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"c7e8b9f03366a1b0a03ab28b7c9c5ad7\")"
     ]
   }
 }
\ No newline at end of file
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/dao/BooksDao.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/dao/BooksDao.kt
index 20e9d18..39e8cae 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/dao/BooksDao.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/dao/BooksDao.kt
@@ -20,10 +20,12 @@
 import android.arch.persistence.room.Dao
 import android.arch.persistence.room.Insert
 import android.arch.persistence.room.Query
+import android.arch.persistence.room.TypeConverters
 import android.arch.persistence.room.integration.kotlintestapp.vo.Author
 import android.arch.persistence.room.integration.kotlintestapp.vo.Book
 import android.arch.persistence.room.integration.kotlintestapp.vo.BookAuthor
 import android.arch.persistence.room.integration.kotlintestapp.vo.BookWithPublisher
+import android.arch.persistence.room.integration.kotlintestapp.vo.Lang
 import android.arch.persistence.room.integration.kotlintestapp.vo.Publisher
 import android.arch.persistence.room.integration.kotlintestapp.vo.PublisherWithBooks
 import io.reactivex.Flowable
@@ -91,4 +93,8 @@
 
     @Query("UPDATE book SET title = :title WHERE bookId = :bookId")
     fun updateBookTitle(bookId: String, title: String?)
+
+    @Query("SELECT * FROM book WHERE languages & :langs != 0 ORDER BY bookId ASC")
+    @TypeConverters(Lang::class)
+    fun findByLanguages(langs: Set<Lang>): List<Book>
 }
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/BooksDaoTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/BooksDaoTest.kt
index acc2c7d..93be6e9 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/BooksDaoTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/BooksDaoTest.kt
@@ -19,6 +19,7 @@
 import android.arch.persistence.room.integration.kotlintestapp.vo.Author
 import android.arch.persistence.room.integration.kotlintestapp.vo.Book
 import android.arch.persistence.room.integration.kotlintestapp.vo.BookWithPublisher
+import android.arch.persistence.room.integration.kotlintestapp.vo.Lang
 import android.arch.persistence.room.integration.kotlintestapp.vo.Publisher
 import android.database.sqlite.SQLiteConstraintException
 import android.support.test.filters.SmallTest
@@ -119,4 +120,25 @@
                 TestUtil.BOOK_2.bookId))
         assertThat(books, `is`(listOf(TestUtil.BOOK_2, TestUtil.BOOK_1)))
     }
+
+    @Test
+    fun findBooksByLanguage() {
+        booksDao.addPublishers(TestUtil.PUBLISHER)
+        val book1 = TestUtil.BOOK_1.copy(languages = setOf(Lang.TR))
+        val book2 = TestUtil.BOOK_2.copy(languages = setOf(Lang.ES, Lang.TR))
+        val book3 = TestUtil.BOOK_3.copy(languages = setOf(Lang.EN))
+        booksDao.addBooks(book1, book2, book3)
+
+        assertThat(booksDao.findByLanguages(setOf(Lang.EN, Lang.TR)),
+                `is`(listOf(book1, book2, book3)))
+
+        assertThat(booksDao.findByLanguages(setOf(Lang.TR)),
+                `is`(listOf(book1, book2)))
+
+        assertThat(booksDao.findByLanguages(setOf(Lang.ES)),
+                `is`(listOf(book2)))
+
+        assertThat(booksDao.findByLanguages(setOf(Lang.EN)),
+                `is`(listOf(book3)))
+    }
 }
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/TestUtil.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/TestUtil.kt
index c4d406c..57546d0 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/TestUtil.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/TestUtil.kt
@@ -19,6 +19,7 @@
 import android.arch.persistence.room.integration.kotlintestapp.vo.Author
 import android.arch.persistence.room.integration.kotlintestapp.vo.Book
 import android.arch.persistence.room.integration.kotlintestapp.vo.BookAuthor
+import android.arch.persistence.room.integration.kotlintestapp.vo.Lang
 import android.arch.persistence.room.integration.kotlintestapp.vo.Publisher
 
 class TestUtil {
@@ -31,8 +32,12 @@
         val AUTHOR_1 = Author("a1", "author 1")
         val AUTHOR_2 = Author("a2", "author 2")
 
-        val BOOK_1 = Book("b1", "book title 1", "ph1")
-        val BOOK_2 = Book("b2", "book title 2", "ph1")
+        val BOOK_1 = Book("b1", "book title 1", "ph1",
+                setOf(Lang.EN))
+        val BOOK_2 = Book("b2", "book title 2", "ph1",
+                setOf(Lang.TR))
+        val BOOK_3 = Book("b3", "book title 2", "ph1",
+                setOf(Lang.ES))
 
         val BOOK_AUTHOR_1_1 = BookAuthor(BOOK_1.bookId, AUTHOR_1.authorId)
         val BOOK_AUTHOR_1_2 = BookAuthor(BOOK_1.bookId, AUTHOR_2.authorId)
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/Book.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/Book.kt
index 794e60b..fe4a600 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/Book.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/Book.kt
@@ -19,10 +19,16 @@
 import android.arch.persistence.room.Entity
 import android.arch.persistence.room.ForeignKey
 import android.arch.persistence.room.PrimaryKey
+import android.arch.persistence.room.TypeConverters
 
 @Entity(foreignKeys = arrayOf(
         ForeignKey(entity = Publisher::class,
                 parentColumns = arrayOf("publisherId"),
                 childColumns = arrayOf("bookPublisherId"),
                 deferred = true)))
-data class Book(@PrimaryKey val bookId: String, val title: String, val bookPublisherId: String)
+data class Book(
+        @PrimaryKey val bookId: String,
+        val title: String,
+        val bookPublisherId: String,
+        @field:TypeConverters(Lang::class)
+        val languages: Set<Lang>)
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/Lang.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/Lang.kt
new file mode 100644
index 0000000..1d5fb5c
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/Lang.kt
@@ -0,0 +1,46 @@
+/*
+ * 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 android.arch.persistence.room.integration.kotlintestapp.vo
+
+import android.arch.persistence.room.TypeConverter
+
+/**
+ * An enum class which gets saved as a bit set in the database.
+ */
+enum class Lang {
+    TR,
+    EN,
+    ES;
+
+    companion object {
+        @JvmStatic
+        @TypeConverter
+        fun toInt(langs: Set<Lang>): Int {
+            return langs.fold(0) { left, lang ->
+                left.or(1 shl lang.ordinal)
+            }
+        }
+
+        @JvmStatic
+        @TypeConverter
+        fun toSet(value: Int): Set<Lang> {
+            return Lang.values().filter {
+                (1 shl it.ordinal).and(value) != 0
+            }.toSet()
+        }
+    }
+}
\ No newline at end of file
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/TestDatabase.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/TestDatabase.java
index 2fad7b1..610afb2 100644
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/TestDatabase.java
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/TestDatabase.java
@@ -32,6 +32,7 @@
 import android.arch.persistence.room.integration.testapp.dao.UserPetDao;
 import android.arch.persistence.room.integration.testapp.dao.WithClauseDao;
 import android.arch.persistence.room.integration.testapp.vo.BlobEntity;
+import android.arch.persistence.room.integration.testapp.vo.Day;
 import android.arch.persistence.room.integration.testapp.vo.FunnyNamedEntity;
 import android.arch.persistence.room.integration.testapp.vo.Pet;
 import android.arch.persistence.room.integration.testapp.vo.PetCouple;
@@ -41,6 +42,8 @@
 import android.arch.persistence.room.integration.testapp.vo.User;
 
 import java.util.Date;
+import java.util.HashSet;
+import java.util.Set;
 
 @Database(entities = {User.class, Pet.class, School.class, PetCouple.class, Toy.class,
         BlobEntity.class, Product.class, FunnyNamedEntity.class},
@@ -74,5 +77,25 @@
                 return date.getTime();
             }
         }
+
+        @TypeConverter
+        public 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 int composeDays(Set<Day> days) {
+            int result = 0;
+            for (Day day : days) {
+                result |= 1 << day.ordinal();
+            }
+            return result;
+        }
     }
 }
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/UserDao.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/UserDao.java
index 0b184a9..69463da 100644
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/UserDao.java
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/UserDao.java
@@ -29,6 +29,7 @@
 import android.arch.persistence.room.Update;
 import android.arch.persistence.room.integration.testapp.TestDatabase;
 import android.arch.persistence.room.integration.testapp.vo.AvgWeightByAge;
+import android.arch.persistence.room.integration.testapp.vo.Day;
 import android.arch.persistence.room.integration.testapp.vo.User;
 import android.database.Cursor;
 
@@ -36,6 +37,7 @@
 
 import java.util.Date;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.Callable;
 
 import io.reactivex.Flowable;
@@ -203,6 +205,9 @@
     @Query("UPDATE User set mWeight = :weight WHERE mId IN (:ids) AND mAge == :age")
     public abstract int updateByAgeAndIds(float weight, int age, List<Integer> ids);
 
+    @Query("SELECT * FROM user WHERE (mWorkDays & :days) != 0")
+    public abstract List<User> findUsersByWorkDays(Set<Day> days);
+
     // QueryLoader
 
     @Query("SELECT COUNT(*) from user")
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java
index f8049f3..2f1ca54 100644
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java
@@ -35,6 +35,7 @@
 import android.arch.persistence.room.integration.testapp.dao.UserDao;
 import android.arch.persistence.room.integration.testapp.dao.UserPetDao;
 import android.arch.persistence.room.integration.testapp.vo.BlobEntity;
+import android.arch.persistence.room.integration.testapp.vo.Day;
 import android.arch.persistence.room.integration.testapp.vo.Pet;
 import android.arch.persistence.room.integration.testapp.vo.Product;
 import android.arch.persistence.room.integration.testapp.vo.User;
@@ -57,7 +58,9 @@
 import java.util.Calendar;
 import java.util.Collections;
 import java.util.Date;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 @SuppressWarnings("ArraysAsListWithZeroOrOneArgument")
 @SmallTest
@@ -524,4 +527,35 @@
         assertTrue("SQLiteConstraintException expected", caught);
         assertThat(mUserDao.count(), is(0));
     }
+
+    @Test
+    public void enumSet_simpleLoad() {
+        User a = TestUtil.createUser(3);
+        Set<Day> expected = toSet(Day.MONDAY, Day.TUESDAY);
+        a.setWorkDays(expected);
+        mUserDao.insert(a);
+        User loaded = mUserDao.load(3);
+        assertThat(loaded.getWorkDays(), is(expected));
+    }
+
+    @Test
+    public void enumSet_query() {
+        User user1 = TestUtil.createUser(3);
+        user1.setWorkDays(toSet(Day.MONDAY, Day.FRIDAY));
+        User user2 = TestUtil.createUser(5);
+        user2.setWorkDays(toSet(Day.MONDAY, Day.THURSDAY));
+        mUserDao.insert(user1);
+        mUserDao.insert(user2);
+        List<User> empty = mUserDao.findUsersByWorkDays(toSet(Day.WEDNESDAY));
+        assertThat(empty.size(), is(0));
+        List<User> friday = mUserDao.findUsersByWorkDays(toSet(Day.FRIDAY));
+        assertThat(friday, is(Arrays.asList(user1)));
+        List<User> monday = mUserDao.findUsersByWorkDays(toSet(Day.MONDAY));
+        assertThat(monday, is(Arrays.asList(user1, user2)));
+
+    }
+
+    private Set<Day> toSet(Day... days) {
+        return new HashSet<>(Arrays.asList(days));
+    }
 }
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/Day.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/Day.java
new file mode 100644
index 0000000..e02b91c
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/Day.java
@@ -0,0 +1,27 @@
+/*
+ * 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 android.arch.persistence.room.integration.testapp.vo;
+
+public enum Day {
+    MONDAY,
+    TUESDAY,
+    WEDNESDAY,
+    THURSDAY,
+    FRIDAY,
+    SATURDAY,
+    SUNDAY
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/User.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/User.java
index a5b8839..a615819 100644
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/User.java
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/User.java
@@ -23,6 +23,8 @@
 import android.arch.persistence.room.integration.testapp.TestDatabase;
 
 import java.util.Date;
+import java.util.HashSet;
+import java.util.Set;
 
 @Entity
 @TypeConverters({TestDatabase.Converters.class})
@@ -46,6 +48,9 @@
     @ColumnInfo(name = "custommm", collate = ColumnInfo.NOCASE)
     private String mCustomField;
 
+    // bit flags
+    private Set<Day> mWorkDays = new HashSet<>();
+
     public int getId() {
         return mId;
     }
@@ -110,6 +115,15 @@
         mCustomField = customField;
     }
 
+    public Set<Day> getWorkDays() {
+        return mWorkDays;
+    }
+
+    public void setWorkDays(
+            Set<Day> workDays) {
+        mWorkDays = workDays;
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;
@@ -128,8 +142,11 @@
         if (mBirthday != null ? !mBirthday.equals(user.mBirthday) : user.mBirthday != null) {
             return false;
         }
-        return mCustomField != null ? mCustomField.equals(user.mCustomField)
-                : user.mCustomField == null;
+        if (mCustomField != null ? !mCustomField.equals(user.mCustomField)
+                : user.mCustomField != null) {
+            return false;
+        }
+        return mWorkDays != null ? mWorkDays.equals(user.mWorkDays) : user.mWorkDays == null;
     }
 
     @Override
@@ -142,6 +159,7 @@
         result = 31 * result + (mWeight != +0.0f ? Float.floatToIntBits(mWeight) : 0);
         result = 31 * result + (mBirthday != null ? mBirthday.hashCode() : 0);
         result = 31 * result + (mCustomField != null ? mCustomField.hashCode() : 0);
+        result = 31 * result + (mWorkDays != null ? mWorkDays.hashCode() : 0);
         return result;
     }
 
@@ -155,7 +173,8 @@
                 + ", mAdmin=" + mAdmin
                 + ", mWeight=" + mWeight
                 + ", mBirthday=" + mBirthday
-                + ", mCustom=" + mCustomField
+                + ", mCustomField='" + mCustomField + '\''
+                + ", mWorkDays=" + mWorkDays
                 + '}';
     }
 }