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