Prioritize query binding param adapter selection based on param usage.

This changes makes it so that when selecting an adapter for a query
parameter used in a multi-value expression, priority is given to
adapters and type converters of the collection's type argument such
that Room is able to wrap it in the built-in collection adapter which
will correctly bind n parameters. Where as if the binding parameter is
simple, then priority is given to direct adapters / type converters
that can convert the collection to a single binding param.

For example given two type converters, one of `Date -> String` and
another of `List<Date> -> String` then:

Room will choose Date -> String and bind n params,
'date IN (?, ?, ?, ...)' for
```
@Query("SELECT * FROM Foo WHERE date IN (:param)")
fun getFoo(param: List<Date>)
```

Room will choose List<Date> -> String and bind a single param,
'date = ?' for
```
@Query("SELECT * FROM Foo WHERE date = :param")
fun getFoo(param: List<Date>)
```

Room will choose Date -> String and bind a single param,
'date IN (?)' for
```
@Query("SELECT * FROM Foo WHERE date IN (:param)")
fun getFoo(param: Date)
```

Room will choose Date -> String and bind a single param,
'date = ?' for
```
@Query("SELECT * FROM Foo WHERE date = :param")
fun getFoo(param: Date)
```

When the binding param is within the context of a function
expression, the function name is considered whether to
expand the params or not.

Bug: 173130710
Bug: 173647684
Test: ./gradlew :room:integration-tests:room-testapp:cC :room:integration-tests:room-testapp:cC
Change-Id: I474f21fe31e0a2510b1a07826fb96ec8eb391acd
diff --git a/room/compiler/SQLite.g4 b/room/compiler/SQLite.g4
index ab60202..83af9e3 100644
--- a/room/compiler/SQLite.g4
+++ b/room/compiler/SQLite.g4
@@ -171,7 +171,7 @@
                 | K_INSERT K_OR K_FAIL
                 | K_INSERT K_OR K_IGNORE ) K_INTO
    ( schema_name '.' )? table_name ( K_AS table_alias )? ( '(' column_name ( ',' column_name )* ')' )?
-   ( K_VALUES '(' expr ( ',' expr )* ')' ( ',' '(' expr ( ',' expr )* ')' )*
+   ( K_VALUES '(' comma_separated_expr ')' ( ',' '(' comma_separated_expr ')' )*
    | select_stmt
    | K_DEFAULT K_VALUES
    )
@@ -220,8 +220,8 @@
  : K_SELECT ( K_DISTINCT | K_ALL )? result_column ( ',' result_column )*
    ( K_FROM ( table_or_subquery ( ',' table_or_subquery )* | join_clause ) )?
    ( K_WHERE expr )?
-   ( K_GROUP K_BY expr ( ',' expr )* ( K_HAVING expr )? )?
- | K_VALUES '(' expr ( ',' expr )* ')' ( ',' '(' expr ( ',' expr )* ')' )*
+   ( K_GROUP K_BY comma_separated_expr ( K_HAVING expr )? )?
+ | K_VALUES '(' comma_separated_expr ')' ( ',' '(' comma_separated_expr ')' )*
  ;
 
 update_stmt
@@ -286,22 +286,26 @@
  | ( ( schema_name '.' )? table_name '.' )? column_name
  | unary_operator expr
  | expr binary_operator expr
- | function_name '(' ( K_DISTINCT? expr ( ',' expr )* | '*' )? ')'
- | '(' expr ( ',' expr )* ')'
+ | function_name '(' ( K_DISTINCT? comma_separated_expr | '*' )? ')'
+ | '(' comma_separated_expr ')'
  | K_CAST '(' expr K_AS type_name ')'
  | expr K_COLLATE collation_name
  | expr K_NOT? ( K_LIKE | K_GLOB | K_REGEXP | K_MATCH ) expr ( K_ESCAPE expr )?
  | expr ( K_ISNULL | K_NOTNULL | K_NOT K_NULL )
  | expr K_IS K_NOT? expr
  | expr K_NOT? K_BETWEEN expr K_AND expr
- | expr K_NOT? K_IN ( '(' ( select_stmt | expr ( ',' expr )* )? ')'
+ | expr K_NOT? K_IN ( '(' ( select_stmt | comma_separated_expr )? ')'
                     | ( schema_name '.' )? table_name
-                    | ( schema_name '.' )? table_function '(' ( expr ( ',' expr )* )? ')' )
+                    | ( schema_name '.' )? table_function '(' ( comma_separated_expr )? ')' )
  | ( ( K_NOT )? K_EXISTS )? '(' select_stmt ')'
  | K_CASE expr? ( K_WHEN expr K_THEN expr )+ ( K_ELSE expr )? K_END
  | raise_function
  ;
 
+comma_separated_expr
+ : expr ( ',' expr )*
+ ;
+
 foreign_key_clause
  : K_REFERENCES foreign_table ( '(' column_name ( ',' column_name )* ')' )?
    ( ( K_ON ( K_DELETE | K_UPDATE ) ( K_SET K_NULL
diff --git a/room/compiler/src/main/kotlin/androidx/room/parser/ParsedQuery.kt b/room/compiler/src/main/kotlin/androidx/room/parser/ParsedQuery.kt
index 1b402da..87eb8dd 100644
--- a/room/compiler/src/main/kotlin/androidx/room/parser/ParsedQuery.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/parser/ParsedQuery.kt
@@ -16,23 +16,36 @@
 
 package androidx.room.parser
 
-import androidx.room.parser.SectionType.BIND_VAR
-import androidx.room.parser.SectionType.NEWLINE
-import androidx.room.parser.SectionType.TEXT
 import androidx.room.verifier.QueryResultInfo
-import org.antlr.v4.runtime.tree.TerminalNode
 
-enum class SectionType {
-    BIND_VAR,
-    TEXT,
-    NEWLINE
-}
+sealed class Section {
 
-data class Section(val text: String, val type: SectionType) {
+    abstract val text: String
+
+    data class Text(override val text: String) : Section()
+
+    object NewLine : Section() {
+        override val text: String
+            get() = ""
+    }
+
+    data class BindVar(
+        override val text: String,
+        val isMultiple: Boolean
+    ) : Section() {
+        val varName by lazy {
+            if (text.startsWith(":")) {
+                text.substring(1)
+            } else {
+                null
+            }
+        }
+    }
+
     companion object {
-        fun text(text: String) = Section(text, TEXT)
-        fun newline() = Section("", NEWLINE)
-        fun bindVar(text: String) = Section(text, BIND_VAR)
+        fun text(text: String) = Text(text)
+        fun newline() = NewLine
+        fun bindVar(text: String, isMultiple: Boolean) = BindVar(text, isMultiple)
     }
 }
 
@@ -40,7 +53,7 @@
 data class ParsedQuery(
     val original: String,
     val type: QueryType,
-    val inputs: List<TerminalNode>,
+    val inputs: List<BindParameterNode>,
     val tables: Set<Table>, // pairs of table name and alias
     val syntaxErrors: List<String>,
     val runtimeQueryPlaceholder: Boolean
@@ -80,7 +93,12 @@
                         )
                     )
                 }
-                sections.add(Section.bindVar(bindVar.text))
+                sections.add(
+                    Section.bindVar(
+                        bindVar.text,
+                        bindVar.isMultiple
+                    )
+                )
                 charInLine = bindVar.symbol.charPositionInLine + bindVar.symbol.text.length
             }
             if (charInLine < line.length) {
@@ -92,7 +110,7 @@
         }
         sections
     }
-    val bindSections by lazy { sections.filter { it.type == BIND_VAR } }
+    val bindSections by lazy { sections.filterIsInstance<Section.BindVar>() }
     private fun unnamedVariableErrors(): List<String> {
         val anonymousBindError = if (inputs.any { it.text == "?" }) {
             arrayListOf(ParserErrors.ANONYMOUS_BIND_ARGUMENT)
@@ -124,10 +142,10 @@
     }
     val queryWithReplacedBindParams by lazy {
         sections.joinToString("") {
-            when (it.type) {
-                TEXT -> it.text
-                BIND_VAR -> "?"
-                NEWLINE -> "\n"
+            when (it) {
+                is Section.Text -> it.text
+                is Section.BindVar -> "?"
+                is Section.NewLine -> "\n"
             }
         }
     }
diff --git a/room/compiler/src/main/kotlin/androidx/room/parser/SqlParser.kt b/room/compiler/src/main/kotlin/androidx/room/parser/SqlParser.kt
index 2eb1e3c..2dee64a 100644
--- a/room/compiler/src/main/kotlin/androidx/room/parser/SqlParser.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/parser/SqlParser.kt
@@ -22,6 +22,7 @@
 import com.squareup.javapoet.TypeName
 import org.antlr.v4.runtime.tree.ParseTree
 import org.antlr.v4.runtime.tree.TerminalNode
+import java.util.Locale
 
 @Suppress("FunctionName")
 class QueryVisitor(
@@ -30,7 +31,7 @@
     statement: ParseTree,
     private val forRuntimeQuery: Boolean
 ) : SQLiteBaseVisitor<Void?>() {
-    private val bindingExpressions = arrayListOf<TerminalNode>()
+    private val bindingExpressions = arrayListOf<BindParameterNode>()
     // table name alias mappings
     private val tableNames = mutableSetOf<Table>()
     private val withClauseNames = mutableSetOf<String>()
@@ -66,11 +67,38 @@
     override fun visitExpr(ctx: SQLiteParser.ExprContext): Void? {
         val bindParameter = ctx.BIND_PARAMETER()
         if (bindParameter != null) {
-            bindingExpressions.add(bindParameter)
+            val parentContext = ctx.parent
+            val isMultiple = parentContext is SQLiteParser.Comma_separated_exprContext &&
+                !isFixedParamFunctionExpr(parentContext)
+            bindingExpressions.add(
+                BindParameterNode(
+                    node = bindParameter,
+                    isMultiple = isMultiple
+                )
+            )
         }
         return super.visitExpr(ctx)
     }
 
+    /**
+     * Check if a comma separated expression (where multiple binding parameters are accepted) is
+     * part of a function expression that receives a fixed number of parameters. This is
+     * important for determining the priority of type converters used when binding a collection
+     * into a binding parameters and specifically if the function takes a fixed number of
+     * parameter, the collection should not be expanded.
+     */
+    private fun isFixedParamFunctionExpr(
+        ctx: SQLiteParser.Comma_separated_exprContext
+    ): Boolean {
+        if (ctx.parent is SQLiteParser.ExprContext) {
+            val parentExpr = ctx.parent as SQLiteParser.ExprContext
+            val functionName = parentExpr.function_name() ?: return false
+            return fixedParamFunctions.contains(functionName.text.toLowerCase(Locale.US))
+        } else {
+            return false
+        }
+    }
+
     fun createParsedQuery(): ParsedQuery {
         return ParsedQuery(
             original = original,
@@ -120,6 +148,41 @@
 
     companion object {
         private val ESCAPE_LITERALS = listOf("\"", "'", "`")
+
+        // List of built-in SQLite functions that take a fixed non-zero number of parameters
+        // See: https://sqlite.org/lang_corefunc.html
+        val fixedParamFunctions = setOf(
+            "abs",
+            "glob",
+            "hex",
+            "ifnull",
+            "iif",
+            "instr",
+            "length",
+            "like",
+            "likelihood",
+            "likely",
+            "load_extension",
+            "lower",
+            "ltrim",
+            "nullif",
+            "quote",
+            "randomblob",
+            "replace",
+            "round",
+            "rtrim",
+            "soundex",
+            "sqlite_compileoption_get",
+            "sqlite_compileoption_used",
+            "sqlite_offset",
+            "substr",
+            "trim",
+            "typeof",
+            "unicode",
+            "unlikely",
+            "upper",
+            "zeroblob"
+        )
     }
 }
 
@@ -168,6 +231,11 @@
     }
 }
 
+data class BindParameterNode(
+    private val node: TerminalNode,
+    val isMultiple: Boolean // true if this is a multi-param node
+) : TerminalNode by node
+
 enum class QueryType {
     UNKNOWN,
     SELECT,
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt b/room/compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt
index e702802..74f01c6 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt
@@ -16,12 +16,6 @@
 
 package androidx.room.processor
 
-import androidx.room.ext.KotlinTypeNames
-import androidx.room.ext.L
-import androidx.room.ext.N
-import androidx.room.ext.RoomCoroutinesTypeNames
-import androidx.room.ext.T
-import androidx.room.parser.ParsedQuery
 import androidx.room.compiler.processing.XDeclaredType
 import androidx.room.compiler.processing.XMethodElement
 import androidx.room.compiler.processing.XMethodType
@@ -29,6 +23,12 @@
 import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.XVariableElement
 import androidx.room.compiler.processing.isSuspendFunction
+import androidx.room.ext.KotlinTypeNames
+import androidx.room.ext.L
+import androidx.room.ext.N
+import androidx.room.ext.RoomCoroutinesTypeNames
+import androidx.room.ext.T
+import androidx.room.parser.ParsedQuery
 import androidx.room.solver.prepared.binder.CallablePreparedQueryResultBinder.Companion.createPreparedBinder
 import androidx.room.solver.prepared.binder.PreparedQueryResultBinder
 import androidx.room.solver.query.result.CoroutineResultBinder
@@ -58,13 +58,16 @@
 
     abstract fun extractParams(): List<XVariableElement>
 
-    fun extractQueryParams(): List<QueryParameter> {
+    fun extractQueryParams(query: ParsedQuery): List<QueryParameter> {
         return extractParams().map { variableElement ->
             QueryParameterProcessor(
                 baseContext = context,
                 containing = containing,
                 element = variableElement,
-                sqlName = variableElement.name
+                sqlName = variableElement.name,
+                bindVarSection = query.bindSections.firstOrNull {
+                    it.varName == variableElement.name
+                }
             ).process()
         }
     }
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/QueryMethodProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/QueryMethodProcessor.kt
index e4ea3b1..3fcdf59 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/QueryMethodProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/QueryMethodProcessor.kt
@@ -186,7 +186,7 @@
             ProcessorErrors.cannotFindPreparedQueryResultAdapter(returnType.toString(), query.type)
         )
 
-        val parameters = delegate.extractQueryParams()
+        val parameters = delegate.extractQueryParams(query)
         return WriteQueryMethod(
             element = executableElement,
             query = query,
@@ -221,7 +221,7 @@
             }
         }
 
-        val parameters = delegate.extractQueryParams()
+        val parameters = delegate.extractQueryParams(query)
 
         return ReadQueryMethod(
             element = executableElement,
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/QueryParameterProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/QueryParameterProcessor.kt
index 29fd451..4cd8fa7 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/QueryParameterProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/QueryParameterProcessor.kt
@@ -18,18 +18,23 @@
 
 import androidx.room.compiler.processing.XDeclaredType
 import androidx.room.compiler.processing.XVariableElement
+import androidx.room.parser.Section
 import androidx.room.vo.QueryParameter
 
 class QueryParameterProcessor(
     baseContext: Context,
     val containing: XDeclaredType,
     val element: XVariableElement,
-    private val sqlName: String? = null
+    private val sqlName: String,
+    private val bindVarSection: Section.BindVar?
 ) {
     val context = baseContext.fork(element)
     fun process(): QueryParameter {
         val asMember = element.asMemberOf(containing)
-        val parameterAdapter = context.typeAdapterStore.findQueryParameterAdapter(asMember)
+        val parameterAdapter = context.typeAdapterStore.findQueryParameterAdapter(
+            typeMirror = asMember,
+            isMultipleParameter = bindVarSection?.isMultiple ?: false
+        )
         context.checker.check(
             parameterAdapter != null, element,
             ProcessorErrors.CANNOT_BIND_QUERY_PARAMETER_INTO_STMT
@@ -42,7 +47,7 @@
         )
         return QueryParameter(
             name = name,
-            sqlName = sqlName ?: name,
+            sqlName = sqlName,
             type = asMember,
             queryParamAdapter = parameterAdapter
         )
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 f4cf3a5..f04871d 100644
--- a/room/compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
@@ -557,21 +557,32 @@
         }
     }
 
-    fun findQueryParameterAdapter(typeMirror: XType): QueryParameterAdapter? {
+    fun findQueryParameterAdapter(
+        typeMirror: XType,
+        isMultipleParameter: Boolean
+    ): QueryParameterAdapter? {
         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
-            ) ?: return null
-            return CollectionQueryParameterAdapter(binder)
+            val typeArg = declared.typeArguments.first().extendsBoundOrSelf()
+            // An adapter for the collection type arg wrapped in the built-in collection adapter.
+            val wrappedCollectionAdapter = findStatementValueBinder(typeArg, null)?.let {
+                CollectionQueryParameterAdapter(it)
+            }
+            // An adapter for the collection itself, likely a user provided type converter for the
+            // collection.
+            val directCollectionAdapter = findStatementValueBinder(typeMirror, null)?.let {
+                BasicQueryParameterAdapter(it)
+            }
+            // Prioritize built-in collection adapters when finding an adapter for a multi-value
+            // binding param since it is likely wrong to use a collection to single value converter
+            // for an expression that takes in multiple values.
+            return if (isMultipleParameter) {
+                wrappedCollectionAdapter ?: directCollectionAdapter
+            } else {
+                directCollectionAdapter ?: wrappedCollectionAdapter
+            }
         } 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/vo/RelationCollector.kt b/room/compiler/src/main/kotlin/androidx/room/vo/RelationCollector.kt
index dc3171e..5aaf495 100644
--- a/room/compiler/src/main/kotlin/androidx/room/vo/RelationCollector.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/RelationCollector.kt
@@ -284,7 +284,8 @@
                         sqlName = RelationCollectorMethodWriter.KEY_SET_VARIABLE,
                         type = keySet,
                         queryParamAdapter = context.typeAdapterStore.findQueryParameterAdapter(
-                            keySet
+                            typeMirror = keySet,
+                            isMultipleParameter = true
                         )
                     )
                 }
diff --git a/room/compiler/src/main/kotlin/androidx/room/writer/QueryWriter.kt b/room/compiler/src/main/kotlin/androidx/room/writer/QueryWriter.kt
index 8662e74..ae38d5f 100644
--- a/room/compiler/src/main/kotlin/androidx/room/writer/QueryWriter.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/writer/QueryWriter.kt
@@ -24,7 +24,6 @@
 import androidx.room.ext.typeName
 import androidx.room.parser.ParsedQuery
 import androidx.room.parser.Section
-import androidx.room.parser.SectionType
 import androidx.room.solver.CodeGenScope
 import androidx.room.vo.QueryMethod
 import androidx.room.vo.QueryParameter
@@ -81,14 +80,11 @@
                     ClassName.get(StringBuilder::class.java), stringBuilderVar, STRING_UTIL
                 )
                 query.sections.forEach {
-                    when (it.type) {
-                        SectionType.TEXT -> addStatement(
-                            "$L.append($S)", stringBuilderVar,
-                            it
-                                .text
-                        )
-                        SectionType.NEWLINE -> addStatement("$L.append($S)", stringBuilderVar, "\n")
-                        SectionType.BIND_VAR -> {
+                    @Suppress("UNUSED_VARIABLE")
+                    val exhaustive = when (it) {
+                        is Section.Text -> addStatement("$L.append($S)", stringBuilderVar, it.text)
+                        is Section.NewLine -> addStatement("$L.append($S)", stringBuilderVar, "\n")
+                        is Section.BindVar -> {
                             // If it is null, will be reported as error before. We just try out
                             // best to generate as much code as possible.
                             sectionToParamMapping.firstOrNull { mapping ->
diff --git a/room/compiler/src/test/kotlin/androidx/room/parser/SqlParserTest.kt b/room/compiler/src/test/kotlin/androidx/room/parser/SqlParserTest.kt
index 6b2e31d..43835ef8 100644
--- a/room/compiler/src/test/kotlin/androidx/room/parser/SqlParserTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/parser/SqlParserTest.kt
@@ -228,46 +228,104 @@
     }
 
     @Test
+    fun selectMultipleBindingParameter() {
+        SqlParser.parse("SELECT * FROM Foo WHERE id IN (:param1) AND bar = :param2").let {
+            assertThat(it.inputs.size, `is`(2))
+            assertThat(it.inputs[0].isMultiple, `is`(true))
+            assertThat(it.inputs[1].isMultiple, `is`(false))
+        }
+        SqlParser.parse("SELECT * FROM Foo WHERE id NOT IN (:param1)").let {
+            assertThat(it.inputs.size, `is`(1))
+            assertThat(it.inputs[0].isMultiple, `is`(true))
+        }
+        SqlParser.parse("SELECT * FROM Foo WHERE id IN ('a', :param1, 'c')").let {
+            assertThat(it.inputs.size, `is`(1))
+            assertThat(it.inputs[0].isMultiple, `is`(true))
+        }
+        SqlParser.parse("SELECT (:param1), ifnull(:param2, data) FROM Foo").let {
+            assertThat(it.inputs.size, `is`(2))
+            assertThat(it.inputs[0].isMultiple, `is`(true))
+            assertThat(it.inputs[1].isMultiple, `is`(false))
+        }
+        SqlParser.parse("SELECT max(:param1) FROM Foo").let {
+            assertThat(it.inputs.size, `is`(1))
+            assertThat(it.inputs[0].isMultiple, `is`(true))
+        }
+        SqlParser.parse("SELECT * FROM Foo JOIN Bar ON (Foo.id = Bar.id) GROUP BY (:param1)").let {
+            assertThat(it.inputs.size, `is`(1))
+            assertThat(it.inputs[0].isMultiple, `is`(true))
+        }
+        SqlParser.parse("SELECT MAX(:param1) AS num FROM Foo WHERE num > ABS(:param2)").let {
+            assertThat(it.inputs.size, `is`(2))
+            assertThat(it.inputs[0].isMultiple, `is`(true))
+            assertThat(it.inputs[1].isMultiple, `is`(false))
+        }
+        SqlParser.parse("SELECT * FROM Foo WHERE num > customFun(:param)").let {
+            assertThat(it.inputs.size, `is`(1))
+            assertThat(it.inputs[0].isMultiple, `is`(true))
+        }
+    }
+
+    @Test
+    fun insertMultipleBindingParameter() {
+        val query = SqlParser.parse("INSERT INTO Foo VALUES (:param)")
+        assertThat(query.inputs.size, `is`(1))
+        assertThat(query.inputs.first().isMultiple, `is`(true))
+    }
+
+    @Test
     fun foo() {
         assertSections(
             "select * from users where name like ?",
             Section.text("select * from users where name like "),
-            Section.bindVar("?")
+            Section.bindVar("?", false)
         )
         assertSections(
             "select * from users where name like :name AND last_name like :lastName",
             Section.text("select * from users where name like "),
-            Section.bindVar(":name"),
+            Section.bindVar(":name", false),
             Section.text(" AND last_name like "),
-            Section.bindVar(":lastName")
+            Section.bindVar(":lastName", false)
         )
         assertSections(
             "select * from users where name \nlike :name AND last_name like :lastName",
             Section.text("select * from users where name "),
             Section.newline(),
             Section.text("like "),
-            Section.bindVar(":name"),
+            Section.bindVar(":name", false),
             Section.text(" AND last_name like "),
-            Section.bindVar(":lastName")
+            Section.bindVar(":lastName", false)
         )
         assertSections(
             "select * from users where name like :name \nAND last_name like :lastName",
             Section.text("select * from users where name like "),
-            Section.bindVar(":name"),
+            Section.bindVar(":name", false),
             Section.text(" "),
             Section.newline(),
             Section.text("AND last_name like "),
-            Section.bindVar(":lastName")
+            Section.bindVar(":lastName", false)
         )
         assertSections(
             "select * from users where name like :name \nAND last_name like \n:lastName",
             Section.text("select * from users where name like "),
-            Section.bindVar(":name"),
+            Section.bindVar(":name", false),
             Section.text(" "),
             Section.newline(),
             Section.text("AND last_name like "),
             Section.newline(),
-            Section.bindVar(":lastName")
+            Section.bindVar(":lastName", false)
+        )
+        assertSections(
+            "select * from users where name in (?)",
+            Section.text("select * from users where name in ("),
+            Section.bindVar("?", true),
+            Section.text(")")
+        )
+        assertSections(
+            "select * from users where name in (:names)",
+            Section.text("select * from users where name in ("),
+            Section.bindVar(":names", true),
+            Section.text(")")
         )
     }
 
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/TypeConverterPriorityTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/TypeConverterPriorityTest.java
new file mode 100644
index 0000000..562eb2d
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/TypeConverterPriorityTest.java
@@ -0,0 +1,122 @@
+/*
+ * 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 com.google.common.truth.Truth.assertThat;
+
+import androidx.annotation.NonNull;
+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.TypeConverter;
+import androidx.room.TypeConverters;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+@SdkSuppress(minSdkVersion = 24)
+public class TypeConverterPriorityTest {
+
+    private TestDatabase mDB;
+
+    @Before
+    public void setup() {
+        mDB = Room.inMemoryDatabaseBuilder(
+                InstrumentationRegistry.getInstrumentation().getTargetContext(),
+                TestDatabase.class
+        ).build();
+    }
+
+    @Test
+    public void testConverterMultiParam() {
+        mDB.getDao().insert(new TestEntity("1", List.of("a", "b", "c")));
+        mDB.getDao().insert(new TestEntity("2", List.of("d", "e", "f")));
+        mDB.getDao().insert(new TestEntity("3", List.of("g", "h", "i")));
+        mDB.getDao().delete(List.of("2", "3"));
+        assertThat(mDB.getDao().getAll().size()).isEqualTo(1);
+    }
+
+    @Test
+    public void testConverterSingleParam() {
+        mDB.getDao().insert(new TestEntity("1", List.of("a", "b", "c")));
+        mDB.getDao().update("1", List.of("d", "e", "f"));
+        assertThat(mDB.getDao().getAll().size()).isEqualTo(1);
+        assertThat(mDB.getDao().getAll().get(0).data).isEqualTo(List.of("d", "e", "f"));
+    }
+
+    @Database(entities = {TestEntity.class}, version = 1, exportSchema = false)
+    @TypeConverters(Converters.class)
+    abstract static class TestDatabase extends RoomDatabase {
+        abstract TestDao getDao();
+    }
+
+    @Dao
+    interface TestDao {
+        @Insert
+        void insert(TestEntity entity);
+
+        @Query("SELECT * FROM TestEntity")
+        List<TestEntity> getAll();
+
+        @Query("DELETE FROM TestEntity WHERE id IN (:ids)")
+        void delete(List<String> ids);
+
+        @Query("UPDATE TestEntity SET data = :csv WHERE id = :id")
+        void update(String id, List<String> csv);
+    }
+
+    @Entity
+    static final class TestEntity {
+        @NonNull
+        @PrimaryKey
+        public String id;
+        public List<String> data;
+
+        TestEntity(@NonNull String id, List<String> data) {
+            this.id = id;
+            this.data = data;
+        }
+    }
+
+    static final class Converters {
+        @TypeConverter
+        public static String fromData(List<String> list) {
+            return String.join(",", list);
+        }
+
+        @TypeConverter
+        public static List<String> toData(String string) {
+            return Stream.of(string.split(",")).collect(Collectors.toList());
+        }
+    }
+}