blob: 0f8ed9ba09b44946a0f388fc964d180dfeb86b34 [file] [log] [blame]
/*
* Copyright (C) 2016 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.processor
import COMMON
import androidx.room.Dao
import androidx.room.Query
import androidx.room.compiler.codegen.CodeLanguage
import androidx.room.compiler.codegen.XClassName
import androidx.room.compiler.codegen.XTypeName
import androidx.room.compiler.processing.XType
import androidx.room.compiler.processing.XTypeElement
import androidx.room.compiler.processing.util.Source
import androidx.room.compiler.processing.util.XTestInvocation
import androidx.room.compiler.processing.util.runProcessorTest
import androidx.room.ext.CommonTypeNames
import androidx.room.ext.CommonTypeNames.LIST
import androidx.room.ext.CommonTypeNames.MUTABLE_LIST
import androidx.room.ext.CommonTypeNames.STRING
import androidx.room.ext.GuavaUtilConcurrentTypeNames
import androidx.room.ext.KotlinTypeNames
import androidx.room.ext.LifecyclesTypeNames
import androidx.room.ext.PagingTypeNames
import androidx.room.ext.ReactiveStreamsTypeNames
import androidx.room.ext.RxJava2TypeNames
import androidx.room.ext.RxJava3TypeNames
import androidx.room.parser.QueryType
import androidx.room.parser.Table
import androidx.room.processor.ProcessorErrors.DO_NOT_USE_GENERIC_IMMUTABLE_MULTIMAP
import androidx.room.processor.ProcessorErrors.MAP_INFO_MUST_HAVE_AT_LEAST_ONE_COLUMN_PROVIDED
import androidx.room.processor.ProcessorErrors.cannotFindQueryResultAdapter
import androidx.room.processor.ProcessorErrors.keyMayNeedMapInfo
import androidx.room.processor.ProcessorErrors.valueMayNeedMapInfo
import androidx.room.solver.query.result.DataSourceFactoryQueryResultBinder
import androidx.room.solver.query.result.ListQueryResultAdapter
import androidx.room.solver.query.result.LiveDataQueryResultBinder
import androidx.room.solver.query.result.PojoRowAdapter
import androidx.room.solver.query.result.SingleColumnRowAdapter
import androidx.room.solver.query.result.SingleItemQueryResultAdapter
import androidx.room.testing.context
import androidx.room.vo.Field
import androidx.room.vo.QueryMethod
import androidx.room.vo.ReadQueryMethod
import androidx.room.vo.Warning
import androidx.room.vo.WriteQueryMethod
import com.google.common.truth.Truth.assertThat
import createVerifierFromEntitiesAndViews
import mockElementAndType
import org.hamcrest.CoreMatchers.hasItem
import org.hamcrest.CoreMatchers.instanceOf
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.CoreMatchers.not
import org.hamcrest.CoreMatchers.notNullValue
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Assert.assertEquals
import org.junit.AssumptionViolatedException
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import org.mockito.Mockito
@RunWith(Parameterized::class)
class QueryMethodProcessorTest(private val enableVerification: Boolean) {
companion object {
const val DAO_PREFIX = """
package foo.bar;
import androidx.annotation.NonNull;
import androidx.room.*;
import java.util.*;
import com.google.common.collect.*;
@Dao
abstract class MyClass {
"""
const val DAO_PREFIX_KT = """
package foo.bar
import androidx.room.*
import java.util.*
import io.reactivex.*
import io.reactivex.rxjava3.core.*
import androidx.lifecycle.*
import com.google.common.util.concurrent.*
import org.reactivestreams.*
import kotlinx.coroutines.flow.*
@Dao
abstract class MyClass {
"""
const val DAO_SUFFIX = "}"
val POJO = XClassName.get("foo.bar", "MyClass.Pojo")
@Parameterized.Parameters(name = "enableDbVerification={0}")
@JvmStatic
fun getParams() = arrayOf(true, false)
fun createField(name: String, columnName: String? = null): Field {
val (element, type) = mockElementAndType()
return Field(
element = element,
name = name,
type = type,
columnName = columnName ?: name,
affinity = null
)
}
}
@Test
fun testReadNoParams() {
singleQueryMethod<ReadQueryMethod>(
"""
@Query("SELECT * from User")
abstract public int[] foo();
"""
) { parsedQuery, _ ->
assertThat(parsedQuery.element.jvmName, `is`("foo"))
assertThat(parsedQuery.parameters.size, `is`(0))
assertThat(
parsedQuery.returnType.asTypeName(),
`is`(XTypeName.getArrayName(XTypeName.PRIMITIVE_INT))
)
}
}
@Test
fun testSingleParam() {
singleQueryMethod<ReadQueryMethod>(
"""
@Query("SELECT * from User where uid = :x")
abstract public long foo(int x);
"""
) { parsedQuery, invocation ->
assertThat(parsedQuery.element.jvmName, `is`("foo"))
assertThat(parsedQuery.returnType.asTypeName(), `is`(XTypeName.PRIMITIVE_LONG))
assertThat(parsedQuery.parameters.size, `is`(1))
val param = parsedQuery.parameters.first()
assertThat(param.name, `is`("x"))
assertThat(param.sqlName, `is`("x"))
assertThat(
param.type,
`is`(invocation.processingEnv.requireType(XTypeName.PRIMITIVE_INT))
)
}
}
@Test
fun testVarArgs() {
singleQueryMethod<ReadQueryMethod>(
"""
@Query("SELECT * from User where uid in (:ids)")
abstract public long foo(int... ids);
"""
) { parsedQuery, _ ->
assertThat(parsedQuery.element.jvmName, `is`("foo"))
assertThat(parsedQuery.returnType.asTypeName(), `is`(XTypeName.PRIMITIVE_LONG))
assertThat(parsedQuery.parameters.size, `is`(1))
val param = parsedQuery.parameters.first()
assertThat(param.name, `is`("ids"))
assertThat(param.sqlName, `is`("ids"))
assertThat(
param.type.asTypeName(),
`is`(XTypeName.getArrayName(XTypeName.PRIMITIVE_INT))
)
}
}
@Test
fun testParamBindingMatchingNoName() {
singleQueryMethod<ReadQueryMethod>(
"""
@Query("SELECT uid from User where uid = :id")
abstract public long getIdById(int id);
"""
) { parsedQuery, _ ->
val section = parsedQuery.query.bindSections.first()
val param = parsedQuery.parameters.firstOrNull()
assertThat(section, notNullValue())
assertThat(param, notNullValue())
assertThat(parsedQuery.sectionToParamMapping, `is`(listOf(Pair(section, param))))
}
}
@Test
fun testParamBindingMatchingSimpleBind() {
singleQueryMethod<ReadQueryMethod>(
"""
@Query("SELECT uid from User where uid = :id")
abstract public long getIdById(int id);
"""
) { parsedQuery, _ ->
val section = parsedQuery.query.bindSections.first()
val param = parsedQuery.parameters.firstOrNull()
assertThat(section, notNullValue())
assertThat(param, notNullValue())
assertThat(
parsedQuery.sectionToParamMapping,
`is`(listOf(Pair(section, param)))
)
}
}
@Test
fun testParamBindingTwoBindVarsIntoTheSameParameter() {
singleQueryMethod<ReadQueryMethod>(
"""
@Query("SELECT uid from User where uid = :id OR uid = :id")
abstract public long getIdById(int id);
"""
) { parsedQuery, _ ->
val section = parsedQuery.query.bindSections[0]
val section2 = parsedQuery.query.bindSections[1]
val param = parsedQuery.parameters.firstOrNull()
assertThat(section, notNullValue())
assertThat(section2, notNullValue())
assertThat(param, notNullValue())
assertThat(
parsedQuery.sectionToParamMapping,
`is`(listOf(Pair(section, param), Pair(section2, param)))
)
}
}
@Test
fun testMissingParameterForBinding() {
singleQueryMethod<ReadQueryMethod>(
"""
@Query("SELECT uid from User where uid = :id OR uid = :uid")
abstract public long getIdById(int id);
"""
) { parsedQuery, invocation ->
val section = parsedQuery.query.bindSections[0]
val section2 = parsedQuery.query.bindSections[1]
val param = parsedQuery.parameters.firstOrNull()
assertThat(section, notNullValue())
assertThat(section2, notNullValue())
assertThat(param, notNullValue())
assertThat(
parsedQuery.sectionToParamMapping,
`is`(listOf(Pair(section, param), Pair(section2, null)))
)
invocation.assertCompilationResult {
hasErrorContaining(
ProcessorErrors.missingParameterForBindVariable(listOf(":uid"))
)
}
}
}
@Test
fun test2MissingParameterForBinding() {
singleQueryMethod<ReadQueryMethod>(
"""
@Query("SELECT uid from User where name = :bar AND uid = :id OR uid = :uid")
abstract public long getIdById(int id);
"""
) { parsedQuery, invocation ->
val bar = parsedQuery.query.bindSections[0]
val id = parsedQuery.query.bindSections[1]
val uid = parsedQuery.query.bindSections[2]
val param = parsedQuery.parameters.firstOrNull()
assertThat(bar, notNullValue())
assertThat(id, notNullValue())
assertThat(uid, notNullValue())
assertThat(param, notNullValue())
assertThat(
parsedQuery.sectionToParamMapping,
`is`(listOf(Pair(bar, null), Pair(id, param), Pair(uid, null)))
)
invocation.assertCompilationResult {
hasErrorContaining(
ProcessorErrors.missingParameterForBindVariable(listOf(":bar", ":uid"))
)
}
}
}
@Test
fun testUnusedParameters() {
singleQueryMethod<ReadQueryMethod>(
"""
@Query("SELECT uid from User where name = :bar")
abstract public long getIdById(int bar, int whyNotUseMe);
"""
) { parsedQuery, invocation ->
val bar = parsedQuery.query.bindSections[0]
val barParam = parsedQuery.parameters.firstOrNull()
assertThat(bar, notNullValue())
assertThat(barParam, notNullValue())
assertThat(
parsedQuery.sectionToParamMapping,
`is`(listOf(Pair(bar, barParam)))
)
invocation.assertCompilationResult {
hasErrorContaining(
ProcessorErrors.unusedQueryMethodParameter(listOf("whyNotUseMe"))
)
}
}
}
@Test
fun testNameWithUnderscore() {
singleQueryMethod<ReadQueryMethod>(
"""
@Query("select * from User where uid = :_blah")
abstract public long getSth(int _blah);
"""
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorContaining(
ProcessorErrors.QUERY_PARAMETERS_CANNOT_START_WITH_UNDERSCORE
)
}
}
}
@Test
fun testGenericReturnType() {
singleQueryMethod<ReadQueryMethod>(
"""
@Query("select * from User")
abstract public <T> ${LIST.canonicalName}<T> foo(int x);
"""
) { parsedQuery, invocation ->
val expected = MUTABLE_LIST.parametrizedBy(
XClassName.get("", "T")
)
assertThat(parsedQuery.returnType.asTypeName(), `is`(expected))
invocation.assertCompilationResult {
hasErrorContaining(
ProcessorErrors.CANNOT_USE_UNBOUND_GENERICS_IN_QUERY_METHODS
)
}
}
}
@Test
fun testBadQuery() {
singleQueryMethod<ReadQueryMethod>(
"""
@Query("select * from :1 :2")
abstract public long foo(int x);
"""
) { _, invocation ->
// do nothing
invocation.assertCompilationResult {
hasErrorContaining("UNEXPECTED_CHAR=:")
}
}
}
@Test
fun testLiveDataWithWithClause() {
singleQueryMethod<ReadQueryMethod>(
"""
@Query("WITH RECURSIVE tempTable(n, fact) AS (SELECT 0, 1 UNION ALL SELECT n+1,"
+ " (n+1)*fact FROM tempTable WHERE n < 9) SELECT fact FROM tempTable, User")
abstract public ${LifecyclesTypeNames.LIVE_DATA.canonicalName}<${LIST.canonicalName}<Integer>>
getFactorialLiveData();
"""
) { parsedQuery, _ ->
assertThat(parsedQuery.query.tables, hasItem(Table("User", "User")))
assertThat(
parsedQuery.query.tables,
not(hasItem(Table("tempTable", "tempTable")))
)
assertThat(parsedQuery.query.tables.size, `is`(1))
}
}
@Test
fun testLiveDataWithNothingToObserve() {
singleQueryMethod<ReadQueryMethod>(
"""
@Query("SELECT 1")
abstract public ${LifecyclesTypeNames.LIVE_DATA.canonicalName}<Integer> getOne();
"""
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorContaining(
ProcessorErrors.OBSERVABLE_QUERY_NOTHING_TO_OBSERVE
)
}
}
}
@Test
fun testLiveDataWithWithClauseAndNothingToObserve() {
singleQueryMethod<ReadQueryMethod>(
"""
@Query("WITH RECURSIVE tempTable(n, fact) AS (SELECT 0, 1 UNION ALL SELECT n+1,"
+ " (n+1)*fact FROM tempTable WHERE n < 9) SELECT fact FROM tempTable")
abstract public ${LifecyclesTypeNames.LIVE_DATA.canonicalName}<${LIST.canonicalName}<Integer>>
getFactorialLiveData();
"""
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorContaining(
ProcessorErrors.OBSERVABLE_QUERY_NOTHING_TO_OBSERVE
)
}
}
}
@Test
fun testBoundGeneric() {
singleQueryMethod<ReadQueryMethod>(
"""
static abstract class BaseModel<T> {
@Query("select COUNT(*) from User")
abstract public T getT();
}
@Dao
static abstract class ExtendingModel extends BaseModel<Integer> {
}
"""
) { parsedQuery, _ ->
assertThat(
parsedQuery.returnType.asTypeName(),
`is`(XTypeName.BOXED_INT)
)
}
}
@Test
fun testBoundGenericParameter() {
singleQueryMethod<ReadQueryMethod>(
"""
static abstract class BaseModel<T> {
@Query("select COUNT(*) from User where :t")
abstract public int getT(T t);
}
@Dao
static abstract class ExtendingModel extends BaseModel<Integer> {
}
"""
) { parsedQuery, _ ->
assertThat(
parsedQuery.parameters.first().type.asTypeName(),
`is`(
XTypeName.BOXED_INT
)
)
}
}
@Test
fun testReadDeleteWithBadReturnType() {
singleQueryMethod<WriteQueryMethod>(
"""
@Query("DELETE from User where uid = :id")
abstract public float foo(int id);
"""
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorContaining(
ProcessorErrors
.cannotFindPreparedQueryResultAdapter("float", QueryType.DELETE)
)
}
}
}
@Test
fun testSimpleDelete() {
singleQueryMethod<WriteQueryMethod>(
"""
@Query("DELETE from User where uid = :id")
abstract public int foo(int id);
"""
) { parsedQuery, _ ->
assertThat(parsedQuery.element.jvmName, `is`("foo"))
assertThat(parsedQuery.parameters.size, `is`(1))
assertThat(parsedQuery.returnType.asTypeName(), `is`(XTypeName.PRIMITIVE_INT))
}
}
@Test
fun testVoidDeleteQuery() {
singleQueryMethod<WriteQueryMethod>(
"""
@Query("DELETE from User where uid = :id")
abstract public void foo(int id);
"""
) { parsedQuery, _ ->
assertThat(parsedQuery.element.jvmName, `is`("foo"))
assertThat(parsedQuery.parameters.size, `is`(1))
assertThat(parsedQuery.returnType.asTypeName(), `is`(XTypeName.UNIT_VOID))
}
}
@Test
fun testVoidUpdateQuery() {
singleQueryMethod<WriteQueryMethod>(
"""
@Query("update user set name = :name")
abstract public void updateAllNames(String name);
"""
) { parsedQuery, _ ->
assertThat(parsedQuery.element.jvmName, `is`("updateAllNames"))
assertThat(parsedQuery.parameters.size, `is`(1))
assertThat(parsedQuery.returnType.asTypeName(), `is`(XTypeName.UNIT_VOID))
assertThat(
parsedQuery.parameters.first().type.asTypeName(),
`is`(STRING)
)
}
}
@Test
fun testVoidInsertQuery() {
singleQueryMethod<WriteQueryMethod>(
"""
@Query("insert into user (name) values (:name)")
abstract public void insertUsername(String name);
"""
) { parsedQuery, _ ->
assertThat(parsedQuery.element.jvmName, `is`("insertUsername"))
assertThat(parsedQuery.parameters.size, `is`(1))
assertThat(parsedQuery.returnType.asTypeName(), `is`(XTypeName.UNIT_VOID))
assertThat(parsedQuery.parameters.first().type.asTypeName(), `is`(STRING))
}
}
@Test
fun testLongInsertQuery() {
singleQueryMethod<WriteQueryMethod>(
"""
@Query("insert into user (name) values (:name)")
abstract public long insertUsername(String name);
"""
) { parsedQuery, _ ->
assertThat(parsedQuery.element.jvmName, `is`("insertUsername"))
assertThat(parsedQuery.parameters.size, `is`(1))
assertThat(parsedQuery.returnType.asTypeName(), `is`(XTypeName.PRIMITIVE_LONG))
assertThat(parsedQuery.parameters.first().type.asTypeName(), `is`(STRING))
}
}
@Test
fun testInsertQueryWithBadReturnType() {
singleQueryMethod<WriteQueryMethod>(
"""
@Query("insert into user (name) values (:name)")
abstract public int insert(String name);
"""
) { parsedQuery, invocation ->
assertThat(parsedQuery.returnType.asTypeName(), `is`(XTypeName.PRIMITIVE_INT))
invocation.assertCompilationResult {
hasErrorContaining(
ProcessorErrors
.cannotFindPreparedQueryResultAdapter("int", QueryType.INSERT)
)
}
}
}
@Test
fun testLiveDataQuery() {
singleQueryMethod<ReadQueryMethod>(
"""
@Query("select name from user where uid = :id")
abstract ${LifecyclesTypeNames.LIVE_DATA.canonicalName}<String> nameLiveData(String id);
"""
) { parsedQuery, _ ->
assertThat(
parsedQuery.returnType.asTypeName(),
`is`(
LifecyclesTypeNames.LIVE_DATA.parametrizedBy(STRING)
)
)
assertThat(
parsedQuery.queryResultBinder,
instanceOf(LiveDataQueryResultBinder::class.java)
)
}
}
@Test
fun testBadReturnForDeleteQuery() {
singleQueryMethod<WriteQueryMethod>(
"""
@Query("delete from user where uid = :id")
abstract ${LifecyclesTypeNames.LIVE_DATA.canonicalName}<Integer> deleteLiveData(String id);
"""
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorContaining(
ProcessorErrors.cannotFindPreparedQueryResultAdapter(
"androidx.lifecycle.LiveData<java.lang.Integer>",
QueryType.DELETE
)
)
}
}
}
@Test
fun testBadReturnForUpdateQuery() {
singleQueryMethod<WriteQueryMethod>(
"""
@Query("update user set name = :name")
abstract ${LifecyclesTypeNames.LIVE_DATA.canonicalName}<Integer> updateNameLiveData(String name);
"""
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorContaining(
ProcessorErrors.cannotFindPreparedQueryResultAdapter(
"androidx.lifecycle.LiveData<java.lang.Integer>",
QueryType.UPDATE
)
)
}
}
}
@Test
fun testDataSourceFactoryQuery() {
singleQueryMethod<ReadQueryMethod>(
"""
@Query("select name from user")
abstract ${PagingTypeNames.DATA_SOURCE_FACTORY.canonicalName}<Integer, String>
nameDataSourceFactory();
"""
) { parsedQuery, _ ->
assertThat(
parsedQuery.returnType.asTypeName(),
`is`(
PagingTypeNames.DATA_SOURCE_FACTORY.parametrizedBy(
XTypeName.BOXED_INT,
STRING
)
)
)
assertThat(
parsedQuery.queryResultBinder,
instanceOf(DataSourceFactoryQueryResultBinder::class.java)
)
val tableNames =
(parsedQuery.queryResultBinder as DataSourceFactoryQueryResultBinder)
.positionalDataSourceQueryResultBinder.tableNames
assertEquals(setOf("user"), tableNames)
}
}
@Test
fun testMultiTableDataSourceFactoryQuery() {
singleQueryMethod<ReadQueryMethod>(
"""
@Query("select name from User u LEFT OUTER JOIN Book b ON u.uid == b.uid")
abstract ${PagingTypeNames.DATA_SOURCE_FACTORY.canonicalName}<Integer, String>
nameDataSourceFactory();
"""
) { parsedQuery, _ ->
assertThat(
parsedQuery.returnType.asTypeName(),
`is`(
PagingTypeNames.DATA_SOURCE_FACTORY.parametrizedBy(
XTypeName.BOXED_INT,
STRING
)
)
)
assertThat(
parsedQuery.queryResultBinder,
instanceOf(DataSourceFactoryQueryResultBinder::class.java)
)
val tableNames =
(parsedQuery.queryResultBinder as DataSourceFactoryQueryResultBinder)
.positionalDataSourceQueryResultBinder.tableNames
assertEquals(setOf("User", "Book"), tableNames)
}
}
@Test
fun testBadChannelReturnForQuery() {
singleQueryMethod<QueryMethod>(
"""
@Query("select * from user")
abstract ${KotlinTypeNames.CHANNEL.canonicalName}<User> getUsersChannel();
""",
additionalSources = listOf(COMMON.CHANNEL)
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorContaining(
ProcessorErrors.invalidChannelType(
KotlinTypeNames.CHANNEL.canonicalName
)
)
}
}
}
@Test
fun testBadSendChannelReturnForQuery() {
singleQueryMethod<QueryMethod>(
"""
@Query("select * from user")
abstract ${KotlinTypeNames.SEND_CHANNEL.canonicalName}<User> getUsersChannel();
""",
additionalSources = listOf(COMMON.SEND_CHANNEL)
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorContaining(
ProcessorErrors.invalidChannelType(
KotlinTypeNames.SEND_CHANNEL.canonicalName
)
)
}
}
}
@Test
fun testBadReceiveChannelReturnForQuery() {
singleQueryMethod<QueryMethod>(
"""
@Query("select * from user")
abstract ${KotlinTypeNames.RECEIVE_CHANNEL.canonicalName}<User> getUsersChannel();
""",
additionalSources = listOf(COMMON.RECEIVE_CHANNEL)
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorContaining(
ProcessorErrors.invalidChannelType(
KotlinTypeNames.RECEIVE_CHANNEL.toString(CodeLanguage.JAVA)
)
)
}
}
}
@Test
fun query_detectTransaction_select() {
singleQueryMethod<ReadQueryMethod>(
"""
@Query("select * from user")
abstract int loadUsers();
"""
) { method, _ ->
assertThat(method.inTransaction, `is`(false))
}
}
@Test
fun query_detectTransaction_selectInTransaction() {
singleQueryMethod<ReadQueryMethod>(
"""
@Transaction
@Query("select * from user")
abstract int loadUsers();
"""
) { method, _ ->
assertThat(method.inTransaction, `is`(true))
}
}
@Test
fun skipVerification() {
singleQueryMethod<ReadQueryMethod>(
"""
@SkipQueryVerification
@Query("SELECT foo from User")
abstract public int[] foo();
"""
) { parsedQuery, _ ->
assertThat(parsedQuery.element.jvmName, `is`("foo"))
assertThat(parsedQuery.parameters.size, `is`(0))
assertThat(
parsedQuery.returnType.asTypeName(),
`is`(XTypeName.getArrayName(XTypeName.PRIMITIVE_INT))
)
}
}
@Test
fun skipVerificationPojo() {
singleQueryMethod<ReadQueryMethod>(
"""
@SkipQueryVerification
@Query("SELECT bookId, uid FROM User")
abstract NotAnEntity getPojo();
"""
) { parsedQuery, _ ->
assertThat(parsedQuery.element.jvmName, `is`("getPojo"))
assertThat(parsedQuery.parameters.size, `is`(0))
assertThat(
parsedQuery.returnType.asTypeName(),
`is`(COMMON.NOT_AN_ENTITY_TYPE_NAME)
)
val adapter = parsedQuery.queryResultBinder.adapter
checkNotNull(adapter)
assertThat(adapter::class, `is`(SingleItemQueryResultAdapter::class))
val rowAdapter = adapter.rowAdapters.single()
assertThat(rowAdapter::class, `is`(PojoRowAdapter::class))
}
}
@Test
fun suppressWarnings() {
singleQueryMethod<ReadQueryMethod>(
"""
@SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
@Query("SELECT uid from User")
abstract public int[] foo();
"""
) { method, invocation ->
assertThat(
QueryMethodProcessor(
baseContext = invocation.context,
containing = Mockito.mock(XType::class.java),
executableElement = method.element,
dbVerifier = null
).context.logger.suppressedWarnings,
`is`(setOf(Warning.CURSOR_MISMATCH))
)
}
}
@Test
fun relationWithExtendsBounds() {
if (!enableVerification) {
return
}
singleQueryMethod<ReadQueryMethod>(
"""
static class Merged extends User {
@Relation(parentColumn = "name", entityColumn = "lastName",
entity = User.class)
java.util.List<? extends User> users;
}
@Transaction
@Query("select * from user")
abstract java.util.List<Merged> loadUsers();
"""
) { method, invocation ->
assertThat(
method.queryResultBinder.adapter,
instanceOf(ListQueryResultAdapter::class.java)
)
val listAdapter = method.queryResultBinder.adapter as ListQueryResultAdapter
assertThat(listAdapter.rowAdapters.single(), instanceOf(PojoRowAdapter::class.java))
val pojoRowAdapter = listAdapter.rowAdapters.single() as PojoRowAdapter
assertThat(pojoRowAdapter.relationCollectors.size, `is`(1))
assertThat(
pojoRowAdapter.relationCollectors[0].relationTypeName,
`is`(
CommonTypeNames.ARRAY_LIST.parametrizedBy(COMMON.USER_TYPE_NAME)
)
)
invocation.assertCompilationResult {
hasNoWarnings()
}
}
}
@Test
fun pojo_renamedColumn() {
pojoTest(
"""
String name;
String lName;
""",
listOf("name", "lastName as lName")
) { adapter, _, invocation ->
assertThat(adapter?.mapping?.unusedColumns, `is`(emptyList()))
assertThat(adapter?.mapping?.unusedFields, `is`(emptyList()))
invocation.assertCompilationResult {
hasNoWarnings()
}
}
}
@Test
fun pojo_exactMatch() {
pojoTest(
"""
String name;
String lastName;
""",
listOf("name", "lastName")
) { adapter, _, invocation ->
assertThat(adapter?.mapping?.unusedColumns, `is`(emptyList()))
assertThat(adapter?.mapping?.unusedFields, `is`(emptyList()))
invocation.assertCompilationResult {
hasNoWarnings()
}
}
}
@Test
fun pojo_exactMatchWithStar() {
pojoTest(
"""
String name;
String lastName;
int uid;
@ColumnInfo(name = "ageColumn")
int age;
""",
listOf("*")
) { adapter, _, invocation ->
assertThat(adapter?.mapping?.unusedColumns, `is`(emptyList()))
assertThat(adapter?.mapping?.unusedFields, `is`(emptyList()))
invocation.assertCompilationResult {
hasNoWarnings()
}
}
}
@Test
fun primitive_removeUnusedColumns() {
if (!enableVerification) {
throw AssumptionViolatedException("nothing to test w/o db verification")
}
singleQueryMethod<ReadQueryMethod>(
"""
@RewriteQueriesToDropUnusedColumns
@Query("select 1 from user")
abstract int getOne();
"""
) { method, invocation ->
val adapter = method.queryResultBinder.adapter?.rowAdapters?.single()
check(adapter is SingleColumnRowAdapter)
assertThat(method.query.original)
.isEqualTo("select 1 from user")
invocation.assertCompilationResult {
hasNoWarnings()
}
}
}
@Test
fun pojo_removeUnusedColumns() {
if (!enableVerification) {
throw AssumptionViolatedException("nothing to test w/o db verification")
}
singleQueryMethod<ReadQueryMethod>(
"""
public static class Pojo {
public String name;
public String lastName;
}
@RewriteQueriesToDropUnusedColumns
@Query("select * from user LIMIT 1")
abstract Pojo loadUsers();
"""
) { method, invocation ->
val adapter = method.queryResultBinder.adapter?.rowAdapters?.single()
check(adapter is PojoRowAdapter)
assertThat(method.query.original)
.isEqualTo("SELECT `name`, `lastName` FROM (select * from user LIMIT 1)")
invocation.assertCompilationResult {
hasNoWarnings()
}
}
}
@Test
fun pojo_multimapQuery_removeUnusedColumns() {
if (!enableVerification) {
throw AssumptionViolatedException("nothing to test w/o db verification")
}
val relatingEntity = Source.java(
"foo.bar.Relation",
"""
package foo.bar;
import androidx.room.*;
@Entity
public class Relation {
@PrimaryKey
long relationId;
long userId;
}
""".trimIndent()
)
singleQueryMethod<ReadQueryMethod>(
"""
public static class Username {
public String name;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Username username = (Username) o;
if (name != username.name) return false;
return true;
}
@Override
public int hashCode() {
return name.hashCode();
}
}
@RewriteQueriesToDropUnusedColumns
@Query("SELECT * FROM User JOIN Relation ON (User.uid = Relation.userId)")
abstract Map<Username, List<Relation>> loadUserRelations();
""",
additionalSources = listOf(relatingEntity)
) { method, invocation ->
assertThat(method.query.original)
.isEqualTo(
"SELECT `name`, `relationId`, `userId` FROM " +
"(SELECT * FROM User JOIN Relation ON (User.uid = Relation.userId))"
)
invocation.assertCompilationResult {
hasNoWarnings()
}
}
}
@Test
fun pojo_dontRemoveUnusedColumnsWhenColumnNamesConflict() {
if (!enableVerification) {
throw AssumptionViolatedException("nothing to test w/o db verification")
}
singleQueryMethod<ReadQueryMethod>(
"""
public static class Pojo {
public String name;
public String lastName;
}
@RewriteQueriesToDropUnusedColumns
@Query("select * from user u, user u2 LIMIT 1")
abstract Pojo loadUsers();
"""
) { method, invocation ->
val adapter = method.queryResultBinder.adapter?.rowAdapters?.single()
check(adapter is PojoRowAdapter)
assertThat(method.query.original).isEqualTo("select * from user u, user u2 LIMIT 1")
invocation.assertCompilationResult {
hasWarningContaining("The query returns some columns [uid")
}
}
}
@Test
fun pojo_nonJavaName() {
pojoTest(
"""
@ColumnInfo(name = "MAX(ageColumn)")
int maxAge;
String name;
""",
listOf("MAX(ageColumn)", "name")
) { adapter, _, invocation ->
assertThat(adapter?.mapping?.unusedColumns, `is`(emptyList()))
assertThat(adapter?.mapping?.unusedFields, `is`(emptyList()))
invocation.assertCompilationResult {
hasNoWarnings()
}
}
}
@Test
fun pojo_noMatchingFields() {
pojoTest(
"""
String nameX;
String lastNameX;
""",
listOf("name", "lastName")
) { adapter, _, invocation ->
assertThat(adapter?.mapping?.unusedColumns, `is`(listOf("name", "lastName")))
assertThat(adapter?.mapping?.unusedFields, `is`(adapter?.pojo?.fields as List<Field>))
invocation.assertCompilationResult {
hasErrorContaining(
cannotFindQueryResultAdapter(
XClassName.get("foo.bar", "MyClass", "Pojo").canonicalName
)
)
hasWarningContaining(
ProcessorErrors.cursorPojoMismatch(
pojoTypeNames = listOf(POJO.canonicalName),
unusedColumns = listOf("name", "lastName"),
pojoUnusedFields = mapOf(
POJO.canonicalName to listOf(
createField("nameX"),
createField("lastNameX")
)
),
allColumns = listOf("name", "lastName"),
)
)
}
}
}
@Test
fun pojo_badQuery() {
// do not report mismatch if query is broken
pojoTest(
"""
@ColumnInfo(name = "MAX(ageColumn)")
int maxAge;
String name;
""",
listOf("MAX(age)", "name")
) { _, _, invocation ->
invocation.assertCompilationResult {
hasErrorContaining("no such column: age")
hasErrorContaining(
cannotFindQueryResultAdapter(
XClassName.get("foo.bar", "MyClass", "Pojo").canonicalName
)
)
hasErrorCount(2)
hasNoWarnings()
}
}
}
@Test
fun pojo_tooManyColumns() {
pojoTest(
"""
String name;
String lastName;
""",
listOf("uid", "name", "lastName")
) { adapter, _, invocation ->
assertThat(adapter?.mapping?.unusedColumns, `is`(listOf("uid")))
assertThat(adapter?.mapping?.unusedFields, `is`(emptyList()))
invocation.assertCompilationResult {
hasWarningContaining(
ProcessorErrors.cursorPojoMismatch(
pojoTypeNames = listOf(POJO.canonicalName),
unusedColumns = listOf("uid"),
pojoUnusedFields = emptyMap(),
allColumns = listOf("uid", "name", "lastName"),
)
)
}
}
}
@Test
fun pojo_tooManyFields() {
pojoTest(
"""
String name;
String lastName;
""",
listOf("lastName")
) { adapter, _, invocation ->
assertThat(adapter?.mapping?.unusedColumns, `is`(emptyList()))
assertThat(
adapter?.mapping?.unusedFields,
`is`(
adapter?.pojo?.fields?.filter { it.name == "name" }
)
)
invocation.assertCompilationResult {
hasWarningContaining(
ProcessorErrors.cursorPojoMismatch(
pojoTypeNames = listOf(POJO.canonicalName),
unusedColumns = emptyList(),
allColumns = listOf("lastName"),
pojoUnusedFields = mapOf(POJO.canonicalName to listOf(createField("name"))),
)
)
}
}
}
@Test
fun pojo_missingNonNull() {
pojoTest(
"""
@NonNull
String name;
String lastName;
""",
listOf("lastName")
) { adapter, _, invocation ->
assertThat(adapter?.mapping?.unusedColumns, `is`(emptyList()))
assertThat(
adapter?.mapping?.unusedFields,
`is`(
adapter?.pojo?.fields?.filter { it.name == "name" }
)
)
invocation.assertCompilationResult {
hasWarningContaining(
ProcessorErrors.cursorPojoMismatch(
pojoTypeNames = listOf(POJO.canonicalName),
unusedColumns = emptyList(),
pojoUnusedFields = mapOf(POJO.canonicalName to listOf(createField("name"))),
allColumns = listOf("lastName"),
)
)
hasErrorContaining(
ProcessorErrors.pojoMissingNonNull(
pojoTypeName = POJO.canonicalName,
missingPojoFields = listOf("name"),
allQueryColumns = listOf("lastName")
)
)
}
}
}
@Test
fun pojo_tooManyFieldsAndColumns() {
pojoTest(
"""
String name;
String lastName;
""",
listOf("uid", "name")
) { adapter, _, invocation ->
assertThat(adapter?.mapping?.unusedColumns, `is`(listOf("uid")))
assertThat(
adapter?.mapping?.unusedFields,
`is`(
adapter?.pojo?.fields?.filter { it.name == "lastName" }
)
)
invocation.assertCompilationResult {
hasWarningContaining(
ProcessorErrors.cursorPojoMismatch(
pojoTypeNames = listOf(POJO.canonicalName),
unusedColumns = listOf("uid"),
allColumns = listOf("uid", "name"),
pojoUnusedFields = mapOf(
POJO.canonicalName to listOf(createField("lastName"))
)
)
)
}
}
}
@Test
fun pojo_expandProjection() {
if (!enableVerification) return
pojoTest(
"""
String uid;
String name;
""",
listOf("*"),
options = mapOf("room.expandProjection" to "true")
) { adapter, _, invocation ->
adapter!!
assertThat(adapter.mapping.unusedColumns).isEmpty()
assertThat(adapter.mapping.unusedFields).isEmpty()
invocation.assertCompilationResult {
hasNoWarnings()
}
}
}
private fun pojoTest(
pojoFields: String,
queryColumns: List<String>,
options: Map<String, String> = emptyMap(),
handler: (PojoRowAdapter?, QueryMethod, XTestInvocation) -> Unit
) {
singleQueryMethod<ReadQueryMethod>(
"""
static class Pojo {
$pojoFields
}
@Query("SELECT ${queryColumns.joinToString(", ")} from User LIMIT 1")
abstract MyClass.Pojo getNameAndLastNames();
""",
options = options
) { parsedQuery, invocation ->
val adapter = parsedQuery.queryResultBinder.adapter
if (enableVerification) {
if (adapter is SingleItemQueryResultAdapter) {
handler(
adapter.rowAdapters.single() as? PojoRowAdapter,
parsedQuery,
invocation
)
} else {
handler(null, parsedQuery, invocation)
}
} else {
assertThat(adapter, notNullValue())
}
}
}
private fun <T : QueryMethod> singleQueryMethod(
vararg input: String,
additionalSources: Iterable<Source> = emptyList(),
options: Map<String, String> = emptyMap(),
handler: (T, XTestInvocation) -> Unit
) {
val inputSource = Source.java(
"foo.bar.MyClass",
DAO_PREFIX + input.joinToString("\n") + DAO_SUFFIX
)
val commonSources = listOf(
COMMON.LIVE_DATA, COMMON.COMPUTABLE_LIVE_DATA, COMMON.USER, COMMON.BOOK,
COMMON.NOT_AN_ENTITY, COMMON.ARTIST, COMMON.SONG, COMMON.IMAGE, COMMON.IMAGE_FORMAT,
COMMON.CONVERTER
)
runProcessorTest(
sources = additionalSources + commonSources + inputSource,
options = options
) { invocation ->
val (owner, methods) = invocation.roundEnv
.getElementsAnnotatedWith(Dao::class.qualifiedName!!)
.filterIsInstance<XTypeElement>()
.map { typeElement ->
Pair(
typeElement,
typeElement.getAllMethods().filter { method ->
method.hasAnnotation(Query::class)
}.toList()
)
}.first { it.second.isNotEmpty() }
val verifier = if (enableVerification) {
createVerifierFromEntitiesAndViews(invocation).also(
invocation.context::attachDatabaseVerifier
)
} else {
null
}
val parser = QueryMethodProcessor(
baseContext = invocation.context,
containing = owner.type,
executableElement = methods.first(),
dbVerifier = verifier
)
val parsedQuery = parser.process()
@Suppress("UNCHECKED_CAST")
handler(parsedQuery as T, invocation)
}
}
private fun <T : QueryMethod> singleQueryMethodKotlin(
vararg input: String,
additionalSources: Iterable<Source> = emptyList(),
options: Map<String, String> = emptyMap(),
handler: (T, XTestInvocation) -> Unit
) {
val inputSource = Source.kotlin(
"MyClass.kt",
DAO_PREFIX_KT + input.joinToString("\n") + DAO_SUFFIX
)
val commonSources = listOf(
COMMON.USER, COMMON.BOOK, COMMON.NOT_AN_ENTITY, COMMON.RX2_COMPLETABLE,
COMMON.RX2_MAYBE, COMMON.RX2_SINGLE, COMMON.RX2_FLOWABLE, COMMON.RX2_OBSERVABLE,
COMMON.RX3_COMPLETABLE, COMMON.RX3_MAYBE, COMMON.RX3_SINGLE, COMMON.RX3_FLOWABLE,
COMMON.RX3_OBSERVABLE, COMMON.LISTENABLE_FUTURE, COMMON.LIVE_DATA,
COMMON.COMPUTABLE_LIVE_DATA, COMMON.PUBLISHER, COMMON.FLOW, COMMON.GUAVA_ROOM,
COMMON.RX2_ROOM, COMMON.RX2_EMPTY_RESULT_SET_EXCEPTION
)
runProcessorTest(
sources = additionalSources + commonSources + inputSource,
options = options
) { invocation ->
val (owner, methods) = invocation.roundEnv
.getElementsAnnotatedWith(Dao::class.qualifiedName!!)
.filterIsInstance<XTypeElement>()
.map { typeElement ->
Pair(
typeElement,
typeElement.getAllMethods().filter { method ->
method.hasAnnotation(Query::class)
}.toList()
)
}.first { it.second.isNotEmpty() }
val verifier = if (enableVerification) {
createVerifierFromEntitiesAndViews(invocation).also(
invocation.context::attachDatabaseVerifier
)
} else {
null
}
val parser = QueryMethodProcessor(
baseContext = invocation.context,
containing = owner.type,
executableElement = methods.first(),
dbVerifier = verifier
)
val parsedQuery = parser.process()
@Suppress("UNCHECKED_CAST")
handler(parsedQuery as T, invocation)
}
}
@Test
fun testInvalidLinkedListCollectionInMultimapJoin() {
singleQueryMethod<ReadQueryMethod>(
"""
@Query("select * from User u JOIN Book b ON u.uid == b.uid")
abstract Map<User, LinkedList<Book>> getInvalidCollectionMultimap();
"""
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorCount(2)
hasErrorContaining("Multimap 'value' collection type must be a List, Set or Map.")
hasErrorContaining("Not sure how to convert a Cursor to this method's return type")
}
}
}
@Test
fun testInvalidGenericMultimapJoin() {
singleQueryMethod<ReadQueryMethod>(
"""
@Query("select * from User u JOIN Book b ON u.uid == b.uid")
abstract com.google.common.collect.ImmutableMultimap<User, Book>
getInvalidCollectionMultimap();
"""
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorCount(2)
hasErrorContaining(DO_NOT_USE_GENERIC_IMMUTABLE_MULTIMAP)
hasErrorContaining("Not sure how to convert a Cursor to this method's return type")
}
}
}
@Test
fun testUseMapInfoWithBothEmptyColumnsProvided() {
if (!enableVerification) {
return
}
singleQueryMethod<ReadQueryMethod>(
"""
@MapInfo
@Query("select * from User u JOIN Book b ON u.uid == b.uid")
abstract Map<User, Book> getMultimap();
"""
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorCount(1)
hasErrorContaining(MAP_INFO_MUST_HAVE_AT_LEAST_ONE_COLUMN_PROVIDED)
}
}
}
@Test
fun testUseMapInfoWithTableAndColumnName() {
if (!enableVerification) {
return
}
singleQueryMethod<ReadQueryMethod>(
"""
@SuppressWarnings(
{RoomWarnings.CURSOR_MISMATCH, RoomWarnings.AMBIGUOUS_COLUMN_IN_RESULT}
)
@MapInfo(keyColumn = "uid", keyTable = "u")
@Query("SELECT * FROM User u JOIN Book b ON u.uid == b.uid")
abstract Map<Integer, Book> getMultimap();
"""
) { _, invocation ->
invocation.assertCompilationResult {
hasNoWarnings()
}
}
}
@Test
fun testUseMapInfoWithOriginalTableAndColumnName() {
if (!enableVerification) {
return
}
singleQueryMethod<ReadQueryMethod>(
"""
@SuppressWarnings(
{RoomWarnings.CURSOR_MISMATCH, RoomWarnings.AMBIGUOUS_COLUMN_IN_RESULT}
)
@MapInfo(keyColumn = "uid", keyTable = "User")
@Query("SELECT * FROM User u JOIN Book b ON u.uid == b.uid")
abstract Map<Integer, Book> getMultimap();
"""
) { _, invocation ->
invocation.assertCompilationResult {
hasNoWarnings()
}
}
}
@Test
fun testUseMapInfoWithColumnAlias() {
if (!enableVerification) {
return
}
singleQueryMethod<ReadQueryMethod>(
"""
@SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
@MapInfo(keyColumn = "name", valueColumn = "bookCount")
@Query("SELECT name, (SELECT count(*) FROM User u JOIN Book b ON u.uid == b.uid) "
+ "AS bookCount FROM User")
abstract Map<String, Integer> getMultimap();
"""
) { _, invocation ->
invocation.assertCompilationResult {
hasNoWarnings()
}
}
}
@Test
fun testDoesNotImplementEqualsAndHashcodeQuery() {
singleQueryMethod<ReadQueryMethod>(
"""
@Query("select * from User u JOIN Book b ON u.uid == b.uid")
abstract Map<User, Book> getMultimap();
"""
) { _, invocation ->
invocation.assertCompilationResult {
hasWarningCount(1)
hasWarningContaining(
ProcessorErrors.classMustImplementEqualsAndHashCode(
"foo.bar.User"
)
)
}
}
}
@Test
fun testMissingMapInfoOneToOneString() {
singleQueryMethod<ReadQueryMethod>(
"""
@Query("select * from Artist JOIN Song ON Artist.mArtistName == Song.mArtist")
abstract Map<Artist, String> getAllArtistsWithAlbumCoverYear();
"""
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorContaining(
valueMayNeedMapInfo(CommonTypeNames.STRING.canonicalName)
)
}
}
}
@Test
fun testOneToOneStringMapInfoForKeyInsteadOfColumn() {
singleQueryMethod<ReadQueryMethod>(
"""
@MapInfo(keyColumn = "mArtistName")
@Query("select * from Artist JOIN Song ON Artist.mArtistName == Song.mArtist")
abstract Map<Artist, String> getAllArtistsWithAlbumCoverYear();
"""
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorContaining(
valueMayNeedMapInfo(CommonTypeNames.STRING.canonicalName)
)
}
}
}
@Test
fun testMissingMapInfoOneToManyString() {
singleQueryMethod<ReadQueryMethod>(
"""
@Query("select * from Artist JOIN Song ON Artist.mArtistName == Song.mArtist")
abstract Map<Artist, List<String>> getAllArtistsWithAlbumCoverYear();
"""
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorContaining(
valueMayNeedMapInfo(CommonTypeNames.STRING.canonicalName)
)
}
}
}
@Test
fun testMissingMapInfoImmutableListMultimapOneToOneString() {
singleQueryMethod<ReadQueryMethod>(
"""
@Query("select * from Artist JOIN Song ON Artist.mArtistName == Song.mArtist")
abstract ImmutableListMultimap<Artist, String> getAllArtistsWithAlbumCoverYear();
"""
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorContaining(
valueMayNeedMapInfo(CommonTypeNames.STRING.canonicalName)
)
}
}
}
@Test
fun testMissingMapInfoOneToOneLong() {
singleQueryMethod<ReadQueryMethod>(
"""
@Query("SELECT * FROM Artist JOIN Image ON Artist.mArtistName = Image.mArtistInImage")
Map<Artist, Long> getAllArtistsWithAlbumCoverYear();
"""
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorContaining(
valueMayNeedMapInfo(XTypeName.BOXED_LONG.canonicalName)
)
}
}
}
@Test
fun testMissingMapInfoOneToManyLong() {
singleQueryMethod<ReadQueryMethod>(
"""
@Query("SELECT * FROM Artist JOIN Image ON Artist.mArtistName = Image.mArtistInImage")
Map<Artist, Set<Long>> getAllArtistsWithAlbumCoverYear();
"""
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorContaining(
valueMayNeedMapInfo(XTypeName.BOXED_LONG.canonicalName)
)
}
}
}
@Test
fun testMissingMapInfoImmutableListMultimapOneToOneLong() {
singleQueryMethod<ReadQueryMethod>(
"""
@Query("SELECT * FROM Artist JOIN Image ON Artist.mArtistName = Image.mArtistInImage")
ImmutableListMultimap<Artist, Long> getAllArtistsWithAlbumCoverYear();
"""
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorContaining(
valueMayNeedMapInfo(XTypeName.BOXED_LONG.canonicalName)
)
}
}
}
@Test
fun testMissingMapInfoImmutableListMultimapOneToOneTypeConverterKey() {
singleQueryMethod<ReadQueryMethod>(
"""
@TypeConverters(DateConverter.class)
@Query("SELECT * FROM Image JOIN Artist ON Artist.mArtistName = Image.mArtistInImage")
ImmutableMap<java.util.Date, Artist> getAlbumDateWithBandActivity();
"""
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorContaining(
keyMayNeedMapInfo("java.util.Date")
)
}
}
}
@Test
fun testMissingMapInfoImmutableListMultimapOneToOneTypeConverterValue() {
singleQueryMethod<ReadQueryMethod>(
"""
@TypeConverters(DateConverter.class)
@Query("SELECT * FROM Artist JOIN Image ON Artist.mArtistName = Image.mArtistInImage")
ImmutableMap<Artist, java.util.Date> getAlbumDateWithBandActivity();
"""
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorContaining(
valueMayNeedMapInfo("java.util.Date")
)
}
}
}
@Test
fun testUseMapInfoWithColumnsNotInQuery() {
if (!enableVerification) {
return
}
singleQueryMethod<ReadQueryMethod>(
"""
@MapInfo(keyColumn="cat", valueColumn="dog")
@Query("select * from User u JOIN Book b ON u.uid == b.uid")
abstract Map<User, Book> getMultimap();
"""
) { _, invocation ->
invocation.assertCompilationResult {
hasWarningCount(1)
hasWarningContaining(
ProcessorErrors.classMustImplementEqualsAndHashCode(
"foo.bar.User"
)
)
hasErrorCount(2)
hasErrorContaining(
"Column specified in the provided @MapInfo annotation must " +
"be present in the query. Provided: cat."
)
hasErrorContaining(
"Column specified in the provided @MapInfo annotation must " +
"be present in the query. Provided: dog."
)
}
}
}
@Test
fun testAmbiguousColumnInMapInfo() {
if (!enableVerification) {
// No warning without verification, avoiding false positives
return
}
singleQueryMethod<ReadQueryMethod>(
"""
@SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
@MapInfo(keyColumn = "uid")
@Query("SELECT * FROM User u JOIN Book b ON u.uid == b.uid")
abstract Map<Integer, Book> getMultimap();
"""
) { _, invocation ->
invocation.assertCompilationResult {
hasWarning(
ProcessorErrors.ambiguousColumn(
"uid",
ProcessorErrors.AmbiguousColumnLocation.MAP_INFO,
null
)
)
}
}
}
@Test
fun testAmbiguousColumnInMapPojo() {
if (!enableVerification) {
// No warning without verification, avoiding false positives
return
}
val extraPojo = Source.java(
"foo.bar.Id",
"""
package foo.bar;
public class Id {
public int uid;
@Override
public boolean equals(Object o) {
return true;
}
@Override
public int hashCode() {
return 0;
}
}
""".trimIndent()
)
singleQueryMethod<ReadQueryMethod>(
"""
@SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
@Query("SELECT * FROM User u JOIN Book b ON u.uid == b.uid")
abstract Map<Id, Book> getMultimap();
""",
additionalSources = listOf(extraPojo)
) { _, invocation ->
invocation.assertCompilationResult {
hasWarning(
ProcessorErrors.ambiguousColumn(
"uid",
ProcessorErrors.AmbiguousColumnLocation.POJO,
"foo.bar.Id"
)
)
}
}
}
@Test
fun suspendReturnsDeferredType() {
listOf(
"${RxJava2TypeNames.FLOWABLE.canonicalName}<Int>",
"${RxJava2TypeNames.OBSERVABLE.canonicalName}<Int>",
"${RxJava2TypeNames.MAYBE.canonicalName}<Int>",
"${RxJava2TypeNames.SINGLE.canonicalName}<Int>",
"${RxJava2TypeNames.COMPLETABLE.canonicalName}",
"${RxJava3TypeNames.FLOWABLE.canonicalName}<Int>",
"${RxJava3TypeNames.OBSERVABLE.canonicalName}<Int>",
"${RxJava3TypeNames.MAYBE.canonicalName}<Int>",
"${RxJava3TypeNames.SINGLE.canonicalName}<Int>",
"${RxJava3TypeNames.COMPLETABLE.canonicalName}",
"${LifecyclesTypeNames.LIVE_DATA.canonicalName}<Int>",
"${LifecyclesTypeNames.COMPUTABLE_LIVE_DATA.canonicalName}<Int>",
"${GuavaUtilConcurrentTypeNames.LISTENABLE_FUTURE.canonicalName}<Int>",
"${ReactiveStreamsTypeNames.PUBLISHER.canonicalName}<Int>",
"${KotlinTypeNames.FLOW.canonicalName}<Int>"
).forEach { type ->
singleQueryMethodKotlin<WriteQueryMethod>(
"""
@Query("DELETE from User where uid = :id")
abstract suspend fun foo(id: Int): $type
"""
) { _, invocation ->
invocation.assertCompilationResult {
val rawTypeName = type.substringBefore("<")
hasErrorContaining(ProcessorErrors.suspendReturnsDeferredType(rawTypeName))
}
}
}
}
@Test
fun nonNullVoidGuava() {
singleQueryMethodKotlin<WriteQueryMethod>(
"""
@Query("DELETE from User where uid = :id")
abstract fun foo(id: Int): ListenableFuture<Void>
"""
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorContaining(ProcessorErrors.NONNULL_VOID)
}
}
}
@Test
fun maybe() {
singleQueryMethodKotlin<ReadQueryMethod>(
"""
@Query("SELECT * FROM book WHERE bookId = :bookId")
abstract fun getBookMaybe(bookId: String): io.reactivex.Maybe<Book>
"""
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorCount(0)
}
}
}
@Test
fun single() {
singleQueryMethodKotlin<ReadQueryMethod>(
"""
@Query("SELECT * FROM book WHERE bookId = :bookId")
abstract fun getBookSingle(bookId: String): io.reactivex.Single<Book>
"""
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorCount(0)
}
}
}
}