POJO Support
This CL adds ability to return arbitrary POJOs from query results
as long as we match the columns of the result into the pojo.
If the pojo has unused fields or if it ignores some columns from
the result, we show a warning. This warning is listed in
RoomWarnings class and can be disabled via SuppressWarnings.
If SQLVerification is disabled, POJO support does not work.
If the return type is an Entity, we still prefer the entity
converter. This may change in the future.
When converting cursor into POJO, the code is inlined. This
is simpler and avoids the cursor field lookup (because we know
indices ahead of time). This may get expensive if same pojo
is used in multiple places. In the future, we may prefer
creating converters for commonly used pojos.
Bug: 33463891
Test: QueryMethodProcessorTest.kt, PojoTest.java, LiveDataQueryTest.java
Change-Id: Ib82f35d26f839c568e7137c39d4843a9854b20b1
diff --git a/room/common/src/main/java/com/android/support/room/RoomWarnings.java b/room/common/src/main/java/com/android/support/room/RoomWarnings.java
new file mode 100644
index 0000000..7a25556
--- /dev/null
+++ b/room/common/src/main/java/com/android/support/room/RoomWarnings.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.room;
+
+/**
+ * The list of warnings that are produced by Room that you can disable via {@code SurpressWarnings}.
+ */
+public class RoomWarnings {
+ /**
+ * The warning dispatched by Room when the return value of a @Query method does not exactly
+ * match the fields in the query result.
+ * <p>
+ * You can use this value inside a {@link SuppressWarnings} annotation to disable such warnings
+ * for a method.
+ */
+ // if you change this, don't forget to change SurpressWarningsVisitor
+ public static final String CURSOR_MISMATCH = "ROOM_CURSOR_MISMATCH";
+}
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/processor/DaoProcessor.kt b/room/compiler/src/main/kotlin/com/android/support/room/processor/DaoProcessor.kt
index bbadc2e..678f10d 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/processor/DaoProcessor.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/processor/DaoProcessor.kt
@@ -75,8 +75,11 @@
} else {
dbVerifier
}
+
+ val suppressedWarnings = SuppressWarningProcessor.getSuppressedWarnings(element)
+
val queryMethods = methods[Query::class]?.map {
- queryProcessor.parse(declaredType, it)
+ queryProcessor.parse(declaredType, it, suppressedWarnings)
} ?: emptyList()
val insertionMethods = methods[Insert::class]?.map {
@@ -93,10 +96,12 @@
val type = TypeName.get(declaredType)
context.checker.notUnbound(type, element,
ProcessorErrors.CANNOT_USE_UNBOUND_GENERICS_IN_DAO_CLASSES)
+
return Dao(element = element,
type = declaredType,
queryMethods = queryMethods,
insertionMethods = insertionMethods,
- deletionMethods = deletionMethods)
+ deletionMethods = deletionMethods,
+ suppressedWarnings = suppressedWarnings)
}
}
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/processor/DatabaseProcessor.kt b/room/compiler/src/main/kotlin/com/android/support/room/processor/DatabaseProcessor.kt
index 9a5f31e..d6f1210 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/processor/DatabaseProcessor.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/processor/DatabaseProcessor.kt
@@ -81,7 +81,8 @@
val database = Database(element = element,
type = MoreElements.asType(element).asType(),
entities = entities,
- daoMethods = daoMethods)
+ daoMethods = daoMethods,
+ suppressedWarnings = SuppressWarningProcessor.getSuppressedWarnings(element))
return database
}
@@ -139,9 +140,9 @@
private val TO_LIST_OF_TYPES = object
: SimpleAnnotationValueVisitor6<List<TypeMirror>, String>() {
- override fun visitArray(vals: List<AnnotationValue>, elementName: String)
+ override fun visitArray(values: List<AnnotationValue>, elementName: String)
: List<TypeMirror> {
- return vals.map {
+ return values.map {
val tmp = TO_TYPE.visit(it)
tmp
}
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/processor/EntityProcessor.kt b/room/compiler/src/main/kotlin/com/android/support/room/processor/EntityProcessor.kt
index 9c45e6d..8f3d08e 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/processor/EntityProcessor.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/processor/EntityProcessor.kt
@@ -16,60 +16,18 @@
package com.android.support.room.processor
-import com.android.support.room.Ignore
-import com.android.support.room.ext.hasAnnotation
-import com.android.support.room.ext.hasAnyOf
-import com.android.support.room.vo.CallType
import com.android.support.room.vo.Entity
-import com.android.support.room.vo.Field
-import com.android.support.room.vo.FieldGetter
-import com.android.support.room.vo.FieldSetter
import com.google.auto.common.AnnotationMirrors
import com.google.auto.common.MoreElements
-import com.google.auto.common.MoreTypes
-import javax.lang.model.element.ElementKind
-import javax.lang.model.element.ExecutableElement
-import javax.lang.model.element.Modifier
-import javax.lang.model.element.Modifier.ABSTRACT
-import javax.lang.model.element.Modifier.PRIVATE
-import javax.lang.model.element.Modifier.STATIC
import javax.lang.model.element.TypeElement
-import javax.lang.model.type.TypeKind
class EntityProcessor(val context: Context) {
- val fieldParser = FieldProcessor(context)
+ val pojoProcessor = PojoProcessor(context)
fun parse(element: TypeElement): Entity {
context.checker.hasAnnotation(element, com.android.support.room.Entity::class,
ProcessorErrors.ENTITY_MUST_BE_ANNOTATED_WITH_ENTITY)
- val declaredType = MoreTypes.asDeclared(element.asType())
- val allMembers = context.processingEnv.elementUtils.getAllMembers(element)
- val fields = allMembers
- .filter {
- it.kind == ElementKind.FIELD
- && !it.hasAnnotation(Ignore::class)
- && !it.hasAnyOf(Modifier.STATIC)
- }
- .map { fieldParser.parse(declaredType, it) }
-
- val methods = allMembers
- .filter {
- it.kind == ElementKind.METHOD
- && !it.hasAnyOf(PRIVATE, ABSTRACT, STATIC)
- && !it.hasAnnotation(Ignore::class)
- }
- .map { MoreElements.asExecutable(it) }
-
- val getterCandidates = methods.filter {
- it.parameters.size == 0 && it.returnType.kind != TypeKind.VOID
- }
-
- val setterCandidates = methods.filter {
- it.parameters.size == 1 && it.returnType.kind == TypeKind.VOID
- }
-
- assignGetters(fields, getterCandidates)
- assignSetters(fields, setterCandidates)
+ val pojo = pojoProcessor.parse(element)
val annotation = MoreElements.getAnnotationMirror(element,
com.android.support.room.Entity::class.java).orNull()
val tableName : String
@@ -86,98 +44,9 @@
}
context.checker.notBlank(tableName, element,
ProcessorErrors.ENTITY_TABLE_NAME_CANNOT_BE_EMPTY)
- val entity = Entity(element, tableName, declaredType, fields)
+ val entity = Entity(element, tableName, pojo.type, pojo.fields)
context.checker.check(entity.primaryKeys.isNotEmpty(), element,
ProcessorErrors.MISSING_PRIMARY_KEY)
return entity
}
-
- private fun assignGetters(fields: List<Field>, getterCandidates: List<ExecutableElement>) {
- val types = context.processingEnv.typeUtils
-
- fields.forEach { field ->
- if (!field.element.hasAnyOf(PRIVATE)) {
- field.getter = FieldGetter(
- name = field.name,
- type = field.type,
- callType = CallType.FIELD,
- columnAdapter = context.typeAdapterStore.findColumnTypeAdapter(field.type))
- } else {
- val matching = getterCandidates
- .filter {
- types.isSameType(field.element.asType(), it.returnType)
- && field.nameWithVariations.contains(it.simpleName.toString())
- || field.getterNameWithVariations
- .contains(it.simpleName.toString())
- }
- context.checker.check(matching.isNotEmpty(), field.element,
- ProcessorErrors.CANNOT_FIND_GETTER_FOR_FIELD)
- context.checker.check(matching.size < 2, field.element,
- ProcessorErrors.tooManyMatchingGetters(field,
- matching.map { it.simpleName.toString() }))
- val match = matching.firstOrNull()
- if (match == null) {
- // just assume we can set it. the error will block javac anyways.
- field.getter = FieldGetter(
- name = field.name,
- type = field.type,
- callType = CallType.FIELD,
- columnAdapter = context.typeAdapterStore
- .findColumnTypeAdapter(field.type))
- } else {
- field.getter = FieldGetter(
- name = match.simpleName.toString(),
- type = match.returnType,
- callType = CallType.METHOD,
- columnAdapter = context.typeAdapterStore
- .findColumnTypeAdapter(match.returnType))
- }
- }
- }
- }
-
- private fun assignSetters(fields: List<Field>, setterCandidates: List<ExecutableElement>) {
- val types = context.processingEnv.typeUtils
-
- fields.forEach { field ->
- if (!field.element.hasAnyOf(PRIVATE)) {
- field.setter = FieldSetter(
- name = field.name,
- type = field.type,
- callType = CallType.FIELD,
- columnAdapter = context.typeAdapterStore.findColumnTypeAdapter(field.type))
- } else {
- val matching = setterCandidates
- .filter {
- types.isSameType(field.element.asType(), it.parameters.first().asType())
- && field.nameWithVariations.contains(it.simpleName.toString())
- || field.setterNameWithVariations
- .contains(it.simpleName.toString())
- }
- context.checker.check(matching.isNotEmpty(), field.element,
- ProcessorErrors.CANNOT_FIND_SETTER_FOR_FIELD)
- context.checker.check(matching.size < 2, field.element,
- ProcessorErrors.tooManyMatchingSetter(field,
- matching.map { it.simpleName.toString() }))
- val match = matching.firstOrNull()
- if (match == null) {
- // default to field setter
- field.setter = FieldSetter(
- name = field.name,
- type = field.type,
- callType = CallType.FIELD,
- columnAdapter = context.typeAdapterStore
- .findColumnTypeAdapter(field.type))
- } else {
- val paramType = match.parameters.first().asType()
- field.setter = FieldSetter(
- name = match.simpleName.toString(),
- type = paramType,
- callType = CallType.METHOD,
- columnAdapter = context.typeAdapterStore
- .findColumnTypeAdapter(paramType))
- }
- }
- }
- }
}
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/processor/PojoProcessor.kt b/room/compiler/src/main/kotlin/com/android/support/room/processor/PojoProcessor.kt
new file mode 100644
index 0000000..e23c3d0
--- /dev/null
+++ b/room/compiler/src/main/kotlin/com/android/support/room/processor/PojoProcessor.kt
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.room.processor
+
+import com.android.support.room.Ignore
+import com.android.support.room.ext.hasAnnotation
+import com.android.support.room.ext.hasAnyOf
+import com.android.support.room.vo.CallType
+import com.android.support.room.vo.Field
+import com.android.support.room.vo.FieldGetter
+import com.android.support.room.vo.FieldSetter
+import com.android.support.room.vo.Pojo
+import com.google.auto.common.MoreElements
+import com.google.auto.common.MoreTypes
+import javax.lang.model.element.ElementKind
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.element.Modifier
+import javax.lang.model.element.Modifier.ABSTRACT
+import javax.lang.model.element.Modifier.PRIVATE
+import javax.lang.model.element.Modifier.STATIC
+import javax.lang.model.element.TypeElement
+import javax.lang.model.type.TypeKind
+
+/**
+ * Processes any class as if it is a Pojo.
+ */
+class PojoProcessor(val context: Context) {
+ val fieldParser = FieldProcessor(context)
+
+ fun parse(element: TypeElement): Pojo {
+ val declaredType = MoreTypes.asDeclared(element.asType())
+ val allMembers = context.processingEnv.elementUtils.getAllMembers(element)
+ val fields = allMembers
+ .filter {
+ it.kind == ElementKind.FIELD
+ && !it.hasAnnotation(Ignore::class)
+ && !it.hasAnyOf(Modifier.STATIC)
+ }
+ .map { fieldParser.parse(declaredType, it) }
+
+ val methods = allMembers
+ .filter {
+ it.kind == ElementKind.METHOD
+ && !it.hasAnyOf(PRIVATE, ABSTRACT, STATIC)
+ && !it.hasAnnotation(Ignore::class)
+ }
+ .map { MoreElements.asExecutable(it) }
+
+ val getterCandidates = methods.filter {
+ it.parameters.size == 0 && it.returnType.kind != TypeKind.VOID
+ }
+
+ val setterCandidates = methods.filter {
+ it.parameters.size == 1 && it.returnType.kind == TypeKind.VOID
+ }
+
+ assignGetters(fields, getterCandidates)
+ assignSetters(fields, setterCandidates)
+
+ val pojo = Pojo(element, declaredType, fields)
+ return pojo
+ }
+
+ private fun assignGetters(fields: List<Field>, getterCandidates: List<ExecutableElement>) {
+ val types = context.processingEnv.typeUtils
+
+ fields.forEach { field ->
+ if (!field.element.hasAnyOf(PRIVATE)) {
+ field.getter = FieldGetter(
+ name = field.name,
+ type = field.type,
+ callType = CallType.FIELD,
+ columnAdapter = context.typeAdapterStore.findColumnTypeAdapter(field.type))
+ } else {
+ val matching = getterCandidates
+ .filter {
+ types.isSameType(field.element.asType(), it.returnType)
+ && field.nameWithVariations.contains(it.simpleName.toString())
+ || field.getterNameWithVariations
+ .contains(it.simpleName.toString())
+ }
+ context.checker.check(matching.isNotEmpty(), field.element,
+ ProcessorErrors.CANNOT_FIND_GETTER_FOR_FIELD)
+ context.checker.check(matching.size < 2, field.element,
+ ProcessorErrors.tooManyMatchingGetters(field,
+ matching.map { it.simpleName.toString() }))
+ val match = matching.firstOrNull()
+ if (match == null) {
+ // just assume we can set it. the error will block javac anyways.
+ field.getter = FieldGetter(
+ name = field.name,
+ type = field.type,
+ callType = CallType.FIELD,
+ columnAdapter = context.typeAdapterStore
+ .findColumnTypeAdapter(field.type))
+ } else {
+ field.getter = FieldGetter(
+ name = match.simpleName.toString(),
+ type = match.returnType,
+ callType = CallType.METHOD,
+ columnAdapter = context.typeAdapterStore
+ .findColumnTypeAdapter(match.returnType))
+ }
+ }
+ }
+ }
+
+ private fun assignSetters(fields: List<Field>, setterCandidates: List<ExecutableElement>) {
+ val types = context.processingEnv.typeUtils
+
+ fields.forEach { field ->
+ if (!field.element.hasAnyOf(PRIVATE)) {
+ field.setter = FieldSetter(
+ name = field.name,
+ type = field.type,
+ callType = CallType.FIELD,
+ columnAdapter = context.typeAdapterStore.findColumnTypeAdapter(field.type))
+ } else {
+ val matching = setterCandidates
+ .filter {
+ types.isSameType(field.element.asType(), it.parameters.first().asType())
+ && field.nameWithVariations.contains(it.simpleName.toString())
+ || field.setterNameWithVariations
+ .contains(it.simpleName.toString())
+ }
+ context.checker.check(matching.isNotEmpty(), field.element,
+ ProcessorErrors.CANNOT_FIND_SETTER_FOR_FIELD)
+ context.checker.check(matching.size < 2, field.element,
+ ProcessorErrors.tooManyMatchingSetter(field,
+ matching.map { it.simpleName.toString() }))
+ val match = matching.firstOrNull()
+ if (match == null) {
+ // default to field setter
+ field.setter = FieldSetter(
+ name = field.name,
+ type = field.type,
+ callType = CallType.FIELD,
+ columnAdapter = context.typeAdapterStore
+ .findColumnTypeAdapter(field.type))
+ } else {
+ val paramType = match.parameters.first().asType()
+ field.setter = FieldSetter(
+ name = match.simpleName.toString(),
+ type = paramType,
+ callType = CallType.METHOD,
+ columnAdapter = context.typeAdapterStore
+ .findColumnTypeAdapter(paramType))
+ }
+ }
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/processor/ProcessorErrors.kt b/room/compiler/src/main/kotlin/com/android/support/room/processor/ProcessorErrors.kt
index 417da86..3118b58 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/processor/ProcessorErrors.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/processor/ProcessorErrors.kt
@@ -21,7 +21,9 @@
import com.android.support.room.Query
import com.android.support.room.ext.RoomTypeNames
import com.android.support.room.vo.Field
+import com.android.support.room.vo.Pojo
import com.squareup.javapoet.TypeName
+import javax.lang.model.type.TypeMirror
object ProcessorErrors {
val MISSING_QUERY_ANNOTATION = "Query methods must be annotated with ${Query::class.java}"
@@ -122,7 +124,7 @@
}
private val DUPLICATE_TABLES = "Table name \"%s\" is used by multiple entities: %s"
- fun duplicateTableNames(tableName: String, entityNames: List<String>): String {
+ fun duplicateTableNames(tableName: String, entityNames: List<String>): String {
return DUPLICATE_TABLES.format(tableName, entityNames.joinToString(", "))
}
@@ -131,13 +133,45 @@
val DAO_METHOD_CONFLICTS_WITH_OTHERS = "Dao method has conflicts."
- fun duplicateDao(dao : TypeName, methodNames : List<String>) : String {
+ fun duplicateDao(dao: TypeName, methodNames: List<String>): String {
return """
- All of these functions (${methodNames.joinToString(", ")}) return the same DAO class ($dao).
- A database can use a DAO only once so you should remove ${methodNames.size - 1} of these
- conflicting DAO methods. If you are implementing any of these to fulfill an interface, don't
- make it abstract, instead, implement the code that calls the other one.
- """.trimIndent().replace(System.lineSeparator(), " ")
+ All of these functions (${methodNames.joinToString(", ")}) return the same DAO
+ class ($dao).
+ A database can use a DAO only once so you should remove ${methodNames.size - 1} of
+ these conflicting DAO methods. If you are implementing any of these to fulfill an
+ interface, don't make it abstract, instead, implement the code that calls the
+ other one.
+ """.trimIndent().replace(System.lineSeparator(), " ")
+ }
+
+ fun cursorPojoMismatch(pojoTypeName: TypeName,
+ unusedColumns: List<String>, allColumns: List<String>,
+ unusedFields: List<Field>, allFields: List<Field>): String {
+ val unusedColumnsWarning = if (unusedColumns.isNotEmpty()) { """
+ The query returns some columns (${unusedColumns.joinToString(", ")}) which are not
+ use by $pojoTypeName. You can use @ColumnName annotation on the fields to specify
+ the mapping.
+ """.trimIndent().replace(System.lineSeparator(), " ")
+ } else {
+ ""
+ }
+ val unusedFieldsWarning = if (unusedFields.isNotEmpty()) { """
+ $pojoTypeName has some fields
+ (${unusedFields.joinToString(", ") { it.columnName }}) which are not returned by the
+ query. If they are not supposed to be read from the result, you can mark them with
+ @Ignore annotation.
+ """.trimIndent().replace(System.lineSeparator(), " ")
+ } else {
+ ""
+ }
+ return """
+ $unusedColumnsWarning
+ $unusedFieldsWarning
+ You can suppress this warning by annotating the method with
+ @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH).
+ Columns returned by the query: ${allColumns.joinToString(", ")}.
+ Fields in $pojoTypeName: ${allFields.joinToString(", ") { it.columnName }}.
+ """.trimIndent().replace(System.lineSeparator(), " ")
}
}
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/processor/QueryMethodProcessor.kt b/room/compiler/src/main/kotlin/com/android/support/room/processor/QueryMethodProcessor.kt
index 6d3b838..a536486 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/processor/QueryMethodProcessor.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/processor/QueryMethodProcessor.kt
@@ -26,6 +26,7 @@
import com.android.support.room.verifier.DatabaseVerificaitonErrors
import com.android.support.room.verifier.DatabaseVerifier
import com.android.support.room.vo.QueryMethod
+import com.android.support.room.vo.QueryParameter
import com.google.auto.common.AnnotationMirrors
import com.google.auto.common.MoreElements
import com.google.auto.common.MoreTypes
@@ -37,9 +38,10 @@
class QueryMethodProcessor(val context: Context) {
val parameterParser = QueryParameterProcessor(context)
// not enforced
- var dbVerifier : DatabaseVerifier? = null
+ var dbVerifier: DatabaseVerifier? = null
- fun parse(containing: DeclaredType, executableElement: ExecutableElement): QueryMethod {
+ fun parse(containing: DeclaredType, executableElement: ExecutableElement,
+ inheritedSuppressedWarnings: Set<String>): QueryMethod {
val asMember = context.processingEnv.typeUtils.asMemberOf(containing, executableElement)
val executableType = MoreTypes.asExecutable(asMember)
@@ -80,15 +82,20 @@
ProcessorErrors.DELETION_METHODS_MUST_RETURN_VOID_OR_INT
)
}
-
+ val suppressedWarnings = inheritedSuppressedWarnings +
+ SuppressWarningProcessor.getSuppressedWarnings(executableElement)
val resultBinder = context.typeAdapterStore
- .findQueryResultBinder(executableType.returnType, query.tables)
+ .findQueryResultBinder(executableType.returnType, query)
context.checker.check(resultBinder.adapter != null || query.type != QueryType.SELECT,
executableElement, ProcessorErrors.CANNOT_FIND_QUERY_RESULT_ADAPTER)
if (resultBinder is LiveDataQueryResultBinder) {
context.checker.check(query.type == QueryType.SELECT, executableElement,
ProcessorErrors.LIVE_DATA_QUERY_WITHOUT_SELECT)
}
+ // report adapter errors only if query makes sense or verification is off.
+ if (query.resultInfo?.error == null) {
+ resultBinder.reportErrors(context, executableElement, suppressedWarnings)
+ }
val queryMethod = QueryMethod(
element = executableElement,
@@ -97,7 +104,8 @@
returnType = executableType.returnType,
parameters = executableElement.parameters
.map { parameterParser.parse(containing, it) },
- queryResultBinder = resultBinder)
+ queryResultBinder = resultBinder,
+ suppressedWarnings = suppressedWarnings)
val missing = queryMethod.sectionToParamMapping
.filter { it.second == null }
@@ -109,7 +117,8 @@
val unused = queryMethod.parameters.filterNot { param ->
queryMethod.sectionToParamMapping.any { it.second == param }
- }.map { it.name }
+ }.map(QueryParameter::name)
+
if (unused.isNotEmpty()) {
context.logger.w(executableElement,
ProcessorErrors.unusedQueryMethodParameter(unused))
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/processor/SuppressWarningProcessor.kt b/room/compiler/src/main/kotlin/com/android/support/room/processor/SuppressWarningProcessor.kt
new file mode 100644
index 0000000..0b4ed1f
--- /dev/null
+++ b/room/compiler/src/main/kotlin/com/android/support/room/processor/SuppressWarningProcessor.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.room.processor
+
+import com.android.support.room.RoomWarnings
+import com.google.auto.common.AnnotationMirrors
+import com.google.auto.common.MoreElements
+import javax.lang.model.element.AnnotationValue
+import javax.lang.model.element.Element
+import javax.lang.model.util.SimpleAnnotationValueVisitor6
+
+/**
+ * A visitor that reads SuppressWarnings annotations and keeps the ones we know about.
+ */
+object SuppressWarningProcessor {
+ private const val ALL = "ALL"
+ private val KNOWN_WARNINGS = setOf(ALL, RoomWarnings.CURSOR_MISMATCH)
+ fun getSuppressedWarnings(element: Element): Set<String> {
+ val annotation = MoreElements.getAnnotationMirror(element,
+ SuppressWarnings::class.java).orNull()
+ return if (annotation == null) {
+ emptySet<String>()
+ } else {
+ val value = AnnotationMirrors.getAnnotationValue(annotation, "value")
+ if (value == null) {
+ emptySet<String>()
+ } else {
+ VISITOR.visit(value)
+ }
+ }
+ }
+
+ fun isSuppressed(warning: String, suppressedWarnings: Set<String>): Boolean {
+ return suppressedWarnings.contains(ALL) || suppressedWarnings.contains(warning)
+ }
+
+ private object VISITOR : SimpleAnnotationValueVisitor6<Set<String>, String>() {
+ override fun visitArray(values: List<AnnotationValue>?, elementName: String?)
+ : Set<String> {
+ return values?.map {
+ it.value.toString()
+ }?.filter {
+ KNOWN_WARNINGS.contains(it)
+ }?.toSet() ?: emptySet()
+ }
+
+ override fun defaultAction(o: Any?, p: String?): Set<String> {
+ return emptySet()
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/solver/TypeAdapterStore.kt b/room/compiler/src/main/kotlin/com/android/support/room/solver/TypeAdapterStore.kt
index 8369fa8..54f1ef2 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/solver/TypeAdapterStore.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/solver/TypeAdapterStore.kt
@@ -20,8 +20,10 @@
import com.android.support.room.ext.LifecyclesTypeNames
import com.android.support.room.ext.RoomTypeNames
import com.android.support.room.ext.hasAnnotation
+import com.android.support.room.parser.ParsedQuery
import com.android.support.room.parser.Table
import com.android.support.room.processor.Context
+import com.android.support.room.processor.PojoProcessor
import com.android.support.room.solver.query.parameter.ArrayQueryParameterAdapter
import com.android.support.room.solver.query.parameter.BasicQueryParameterAdapter
import com.android.support.room.solver.query.parameter.CollectionQueryParameterAdapter
@@ -31,6 +33,7 @@
import com.android.support.room.solver.query.result.InstantQueryResultBinder
import com.android.support.room.solver.query.result.ListQueryResultAdapter
import com.android.support.room.solver.query.result.LiveDataQueryResultBinder
+import com.android.support.room.solver.query.result.PojoRowAdapter
import com.android.support.room.solver.query.result.QueryResultAdapter
import com.android.support.room.solver.query.result.QueryResultBinder
import com.android.support.room.solver.query.result.RowAdapter
@@ -50,6 +53,8 @@
import com.android.support.room.solver.types.ReverseTypeConverter
import com.android.support.room.solver.types.StringColumnTypeAdapter
import com.android.support.room.solver.types.TypeConverter
+import com.android.support.room.verifier.QueryResultInfo
+import com.android.support.room.vo.Pojo
import com.google.auto.common.MoreElements
import com.google.auto.common.MoreTypes
import com.google.common.annotations.VisibleForTesting
@@ -129,7 +134,7 @@
return findTypeConverter(input, listOf(output))
}
- private fun isLiveData(declared : DeclaredType) : Boolean {
+ private fun isLiveData(declared: DeclaredType): Boolean {
val typeElement = MoreElements.asType(declared.asElement())
val qName = typeElement.qualifiedName.toString()
// even though computable live data is internal, we still check for it as we may inherit
@@ -138,50 +143,56 @@
qName == LifecyclesTypeNames.LIVE_DATA.toString()
}
- fun findQueryResultBinder(typeMirror: TypeMirror, tables: Set<Table>): QueryResultBinder {
+ fun findQueryResultBinder(typeMirror: TypeMirror, query: ParsedQuery): QueryResultBinder {
return if (typeMirror.kind == TypeKind.DECLARED) {
val declared = MoreTypes.asDeclared(typeMirror)
if (declared.typeArguments.isEmpty()) {
- InstantQueryResultBinder(findQueryResultAdapter(typeMirror))
+ InstantQueryResultBinder(findQueryResultAdapter(typeMirror, query))
} else {
if (isLiveData(declared)) {
val liveDataTypeArg = declared.typeArguments.first()
- LiveDataQueryResultBinder(liveDataTypeArg, tables,
- findQueryResultAdapter(liveDataTypeArg))
+ LiveDataQueryResultBinder(liveDataTypeArg, query.tables,
+ findQueryResultAdapter(liveDataTypeArg, query))
} else {
- InstantQueryResultBinder(findQueryResultAdapter(typeMirror))
+ InstantQueryResultBinder(findQueryResultAdapter(typeMirror, query))
}
}
} else {
- InstantQueryResultBinder(findQueryResultAdapter(typeMirror))
+ InstantQueryResultBinder(findQueryResultAdapter(typeMirror, query))
}
}
- private fun findQueryResultAdapter(typeMirror: TypeMirror): QueryResultAdapter? {
+ private fun findQueryResultAdapter(typeMirror: TypeMirror, query: ParsedQuery)
+ : QueryResultAdapter? {
if (typeMirror.kind == TypeKind.DECLARED) {
val declared = MoreTypes.asDeclared(typeMirror)
if (declared.typeArguments.isEmpty()) {
- val rowAdapter = findRowAdapter(typeMirror) ?: return null
+ val rowAdapter = findRowAdapter(typeMirror, query) ?: return null
return SingleEntityQueryResultAdapter(rowAdapter)
}
- // TODO make this flexible so that things like LiveData, Rx can work
if (MoreTypes.isTypeOf(java.util.List::class.java, typeMirror)) {
val typeArg = declared.typeArguments.first()
- val rowAdapter = findRowAdapter(typeArg) ?: return null
+ val rowAdapter = findRowAdapter(typeArg, query) ?: return null
return ListQueryResultAdapter(rowAdapter)
}
return null
} else if (typeMirror.kind == TypeKind.ARRAY) {
val array = MoreTypes.asArray(typeMirror)
- val rowAdapter = findRowAdapter(array.componentType) ?: return null
+ val rowAdapter =
+ findRowAdapter(array.componentType, query) ?: return null
return ArrayQueryResultAdapter(rowAdapter)
} else {
- val rowAdapter = findRowAdapter(typeMirror) ?: return null
+ val rowAdapter = findRowAdapter(typeMirror, query) ?: return null
return SingleEntityQueryResultAdapter(rowAdapter)
}
}
- private fun findRowAdapter(typeMirror: TypeMirror): RowAdapter? {
+ /**
+ * Find a converter from cursor to the given type mirror.
+ * If there is information about the query result, we try to use it to accept *any* POJO.
+ */
+ @VisibleForTesting
+ fun findRowAdapter(typeMirror: TypeMirror, query: ParsedQuery): RowAdapter? {
if (typeMirror.kind == TypeKind.DECLARED) {
val declared = MoreTypes.asDeclared(typeMirror)
if (declared.typeArguments.isNotEmpty()) {
@@ -192,13 +203,21 @@
if (asElement.hasAnnotation(Entity::class)) {
return EntityRowAdapter(typeMirror)
}
- val singleColumn = findColumnTypeAdapter(typeMirror)
-
- if (singleColumn != null) {
- return SingleColumnRowAdapter(singleColumn)
+ // if result is unknown, we are fine w/ single column result
+ val resultInfo = query.resultInfo
+ if ((resultInfo?.columns?.size ?: 1) == 1) {
+ val singleColumn = findColumnTypeAdapter(typeMirror)
+ if (singleColumn != null) {
+ return SingleColumnRowAdapter(singleColumn)
+ }
}
- // TODO we can allow any class actually but need a proper API for that to avoid false
- // positives
+ if (resultInfo != null) {
+ val pojo = PojoProcessor(context).parse(MoreTypes.asTypeElement(typeMirror))
+ return PojoRowAdapter(
+ info = resultInfo,
+ pojo = pojo,
+ out = typeMirror)
+ }
return null
} else {
val singleColumn = findColumnTypeAdapter(typeMirror) ?: return null
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/ArrayQueryResultAdapter.kt b/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/ArrayQueryResultAdapter.kt
index 89ce6fd..2624603 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/ArrayQueryResultAdapter.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/ArrayQueryResultAdapter.kt
@@ -19,11 +19,17 @@
import com.android.support.room.ext.L
import com.android.support.room.ext.T
import com.android.support.room.ext.typeName
+import com.android.support.room.processor.Context
import com.android.support.room.solver.CodeGenScope
import com.squareup.javapoet.ArrayTypeName
import com.squareup.javapoet.TypeName
+import javax.lang.model.element.Element
class ArrayQueryResultAdapter(val rowAdapter: RowAdapter) : QueryResultAdapter() {
+ override fun reportErrors(context: Context, element: Element, suppressedWarnings: Set<String>) {
+ rowAdapter.reportErrors(context, element, suppressedWarnings)
+ }
+
val type = rowAdapter.out
override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
scope.builder().apply {
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/EntityRowAdapter.kt b/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/EntityRowAdapter.kt
index a561144..21c2de6 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/EntityRowAdapter.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/EntityRowAdapter.kt
@@ -20,11 +20,17 @@
import com.android.support.room.ext.RoomTypeNames
import com.android.support.room.ext.T
import com.android.support.room.ext.typeName
+import com.android.support.room.processor.Context
import com.android.support.room.solver.CodeGenScope
import com.squareup.javapoet.ParameterizedTypeName
+import javax.lang.model.element.Element
import javax.lang.model.type.TypeMirror
class EntityRowAdapter(type : TypeMirror) : RowAdapter(type) {
+ override fun reportErrors(context: Context, element: Element, suppressedWarnings: Set<String>) {
+ // we don't know much so no errors to report
+ }
+
override fun init(cursorVarName: String, scope : CodeGenScope) : RowConverter {
val converterVar = scope.getTmpVar("_converter")
scope.builder()
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/ListQueryResultAdapter.kt b/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/ListQueryResultAdapter.kt
index ece879d..d7b4c66 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/ListQueryResultAdapter.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/ListQueryResultAdapter.kt
@@ -19,12 +19,18 @@
import com.android.support.room.ext.L
import com.android.support.room.ext.T
import com.android.support.room.ext.typeName
+import com.android.support.room.processor.Context
import com.android.support.room.solver.CodeGenScope
import com.squareup.javapoet.ClassName
import com.squareup.javapoet.ParameterizedTypeName
import java.util.ArrayList
+import javax.lang.model.element.Element
class ListQueryResultAdapter(val rowAdapter: RowAdapter) : QueryResultAdapter() {
+ override fun reportErrors(context: Context, element: Element, suppressedWarnings: Set<String>) {
+ rowAdapter.reportErrors(context, element, suppressedWarnings)
+ }
+
val type = rowAdapter.out
override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
scope.builder().apply {
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/PojoRowAdapter.kt b/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/PojoRowAdapter.kt
new file mode 100644
index 0000000..a6994f3
--- /dev/null
+++ b/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/PojoRowAdapter.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.room.solver.query.result
+
+import com.android.support.room.RoomWarnings
+import com.android.support.room.ext.L
+import com.android.support.room.ext.T
+import com.android.support.room.ext.typeName
+import com.android.support.room.processor.Context
+import com.android.support.room.processor.ProcessorErrors
+import com.android.support.room.processor.SuppressWarningProcessor
+import com.android.support.room.solver.CodeGenScope
+import com.android.support.room.verifier.QueryResultInfo
+import com.android.support.room.vo.CallType
+import com.android.support.room.vo.Field
+import com.android.support.room.vo.Pojo
+import javax.lang.model.element.Element
+import javax.lang.model.type.TypeMirror
+
+/**
+ * Creates the entity from the given info.
+ * <p>
+ * The info comes from the query processor so we know about the order of columns in the result etc.
+ */
+class PojoRowAdapter(val info: QueryResultInfo, val pojo: Pojo, out: TypeMirror) : RowAdapter(out) {
+ val mapping: Mapping
+
+ init {
+ // toMutableList documentation is not clear if it copies so lets be safe.
+ val remainingFields = pojo.fields.mapTo(mutableListOf<Field>(), { it })
+ val unusedColumns = arrayListOf<String>()
+ val associations = info.columns.mapIndexed { index, column ->
+ // first check remaining, otherwise check any. maybe developer wants to map the same
+ // column into 2 fields. (if they want to post process etc)
+ val field = remainingFields.firstOrNull { it.columnName == column.name } ?:
+ pojo.fields.firstOrNull { it.columnName == column.name }
+ if (field == null) {
+ unusedColumns.add(column.name)
+ null
+ } else {
+ remainingFields.remove(field)
+ Association(index, field)
+ }
+ }.filterNotNull()
+ mapping = Mapping(
+ associations = associations,
+ unusedColumns = unusedColumns,
+ unusedFields = remainingFields
+ )
+ }
+
+ override fun reportErrors(context: Context, element: Element, suppressedWarnings: Set<String>) {
+ if (!SuppressWarningProcessor.isSuppressed(RoomWarnings.CURSOR_MISMATCH, suppressedWarnings)
+ && (mapping.unusedColumns.isNotEmpty() || mapping.unusedFields.isNotEmpty())) {
+ context.logger.w(element, ProcessorErrors.cursorPojoMismatch(
+ pojoTypeName = pojo.typeName,
+ unusedColumns = mapping.unusedColumns,
+ allColumns = info.columns.map { it.name },
+ unusedFields = mapping.unusedFields,
+ allFields = pojo.fields
+ ))
+ }
+ if (mapping.associations.isEmpty()) {
+ // Nothing matched. Clearly an error.
+ context.logger.e(element, ProcessorErrors.CANNOT_FIND_QUERY_RESULT_ADAPTER)
+ }
+ }
+
+ override fun init(cursorVarName: String, scope: CodeGenScope): RowConverter {
+ return object : RowConverter {
+ override fun convert(outVarName: String, cursorVarName: String) {
+ scope.builder().apply {
+ addStatement("$L = new $T()", outVarName, out.typeName())
+ mapping.associations.forEach { mapping ->
+ val field = mapping.field
+ val index = mapping.index
+ val columnAdapter = field.getter.columnAdapter
+ when (field.setter.callType) {
+ CallType.FIELD -> {
+ columnAdapter?.readFromCursor("$outVarName.${field.getter.name}",
+ cursorVarName, index.toString(), scope)
+ }
+ CallType.METHOD -> {
+ val tmpField = scope.getTmpVar("_tmp${field.name.capitalize()}")
+ addStatement("final $T $L", field.getter.type.typeName(), tmpField)
+ columnAdapter?.readFromCursor(tmpField, cursorVarName,
+ index.toString(), scope)
+ addStatement("$L.$L($L)", outVarName, field.setter.name, tmpField)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ data class Association(val index: Int, val field: Field)
+
+ data class Mapping(val associations: List<Association>,
+ val unusedColumns: List<String>,
+ val unusedFields: List<Field>)
+}
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/QueryResultAdapter.kt b/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/QueryResultAdapter.kt
index 3f924b8..f5ead53 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/QueryResultAdapter.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/QueryResultAdapter.kt
@@ -16,11 +16,14 @@
package com.android.support.room.solver.query.result
+import com.android.support.room.processor.Context
import com.android.support.room.solver.CodeGenScope
+import javax.lang.model.element.Element
/**
* Gets a Cursor and converts it into the return type of a method annotated with @Query.
*/
abstract class QueryResultAdapter {
- abstract fun convert(outVarName : String, cursorVarName : String, scope : CodeGenScope)
+ abstract fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope)
+ abstract fun reportErrors(context: Context, element: Element, suppressedWarnings: Set<String>)
}
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/QueryResultBinder.kt b/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/QueryResultBinder.kt
index 77c8d10..c0cefa6 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/QueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/QueryResultBinder.kt
@@ -16,8 +16,11 @@
package com.android.support.room.solver.query.result
+import android.support.annotation.CallSuper
+import com.android.support.room.processor.Context
import com.android.support.room.solver.CodeGenScope
import com.squareup.javapoet.FieldSpec
+import javax.lang.model.element.Element
/**
* Connects the query, db and the ResultAdapter.
@@ -31,6 +34,11 @@
* receives the sql, bind args and adapter and generates the code that runs the query
* and returns the result.
*/
- abstract fun convertAndReturn(roomSQLiteQueryVar : String,
- dbField : FieldSpec, scope : CodeGenScope)
+ abstract fun convertAndReturn(roomSQLiteQueryVar: String,
+ dbField: FieldSpec, scope: CodeGenScope)
+
+ @CallSuper
+ open fun reportErrors(context: Context, element: Element, suppressedWarnings: Set<String>) {
+ adapter?.reportErrors(context, element, suppressedWarnings)
+ }
}
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/RowAdapter.kt b/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/RowAdapter.kt
index 08c1ed7..1faac61 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/RowAdapter.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/RowAdapter.kt
@@ -16,7 +16,9 @@
package com.android.support.room.solver.query.result
+import com.android.support.room.processor.Context
import com.android.support.room.solver.CodeGenScope
+import javax.lang.model.element.Element
import javax.lang.model.type.TypeMirror
/**
@@ -31,6 +33,8 @@
*/
abstract fun init(cursorVarName: String, scope : CodeGenScope) : RowConverter
+ abstract fun reportErrors(context: Context, element: Element, suppressedWarnings: Set<String>)
+
interface RowConverter {
fun convert(outVarName : String, cursorVarName : String)
}
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/SingleColumnRowAdapter.kt b/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/SingleColumnRowAdapter.kt
index ab3ae4d..c79a7a5 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/SingleColumnRowAdapter.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/SingleColumnRowAdapter.kt
@@ -16,13 +16,19 @@
package com.android.support.room.solver.query.result
+import com.android.support.room.processor.Context
import com.android.support.room.solver.CodeGenScope
import com.android.support.room.solver.types.ColumnTypeAdapter
+import javax.lang.model.element.Element
/**
* Wraps a row adapter when there is only 1 item with 1 column in the response.
*/
class SingleColumnRowAdapter(val adapter : ColumnTypeAdapter) : RowAdapter(adapter.out) {
+ override fun reportErrors(context: Context, element: Element, suppressedWarnings: Set<String>) {
+ // we just assume it matches so no errors to report
+ }
+
override fun init(cursorVarName: String, scope: CodeGenScope) : RowConverter {
return object : RowConverter {
override fun convert(outVarName: String, cursorVarName: String) {
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/SingleEntityQueryResultAdapter.kt b/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/SingleEntityQueryResultAdapter.kt
index 599b565..f3da7fb 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/SingleEntityQueryResultAdapter.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/SingleEntityQueryResultAdapter.kt
@@ -19,13 +19,19 @@
import com.android.support.room.ext.L
import com.android.support.room.ext.T
import com.android.support.room.ext.typeName
+import com.android.support.room.processor.Context
import com.android.support.room.solver.CodeGenScope
import defaultValue
+import javax.lang.model.element.Element
/**
* Wraps a row adapter when there is only 1 item in the result
*/
class SingleEntityQueryResultAdapter(val rowAdapter: RowAdapter) : QueryResultAdapter() {
+ override fun reportErrors(context: Context, element: Element, suppressedWarnings: Set<String>) {
+ rowAdapter.reportErrors(context, element, suppressedWarnings)
+ }
+
val type = rowAdapter.out
override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
scope.builder().apply {
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/vo/Dao.kt b/room/compiler/src/main/kotlin/com/android/support/room/vo/Dao.kt
index 9f1862b..7e2ae33 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/vo/Dao.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/vo/Dao.kt
@@ -23,7 +23,8 @@
data class Dao(val element : Element, val type : DeclaredType,
val queryMethods: List<QueryMethod>,
val insertionMethods : List<InsertionMethod>,
- val deletionMethods : List<DeletionMethod>) {
+ val deletionMethods : List<DeletionMethod>,
+ val suppressedWarnings : Set<String> = emptySet()) {
// parsed dao might have a suffix if it is used in multiple databases.
private var suffix : String? = null
fun setSuffix(newSuffix : String) {
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/vo/Database.kt b/room/compiler/src/main/kotlin/com/android/support/room/vo/Database.kt
index e1ee2f8..574e0a8 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/vo/Database.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/vo/Database.kt
@@ -26,7 +26,8 @@
data class Database(val element : Element,
val type : TypeMirror,
val entities : List<Entity>,
- val daoMethods : List<DaoMethod>) {
+ val daoMethods : List<DaoMethod>,
+ val suppressedWarnings : Set<String> = emptySet()) {
val typeName by lazy { ClassName.get(type) as ClassName }
val implClassName by lazy {
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/vo/Pojo.kt b/room/compiler/src/main/kotlin/com/android/support/room/vo/Pojo.kt
new file mode 100644
index 0000000..3a41dad
--- /dev/null
+++ b/room/compiler/src/main/kotlin/com/android/support/room/vo/Pojo.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.room.vo
+
+import com.android.support.room.ext.typeName
+import com.squareup.javapoet.TypeName
+import javax.lang.model.element.TypeElement
+import javax.lang.model.type.DeclaredType
+
+/**
+ * A class is turned into a Pojo if it is used in a query response.
+ */
+data class Pojo(val element : TypeElement, val type: DeclaredType, val fields : List<Field>) {
+ val typeName: TypeName by lazy { type.typeName() }
+}
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/vo/QueryMethod.kt b/room/compiler/src/main/kotlin/com/android/support/room/vo/QueryMethod.kt
index eef5e9c..b7040e5 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/vo/QueryMethod.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/vo/QueryMethod.kt
@@ -30,7 +30,8 @@
*/
data class QueryMethod(val element: ExecutableElement, val query: ParsedQuery, val name: String,
val returnType: TypeMirror, val parameters: List<QueryParameter>,
- val queryResultBinder : QueryResultBinder) {
+ val queryResultBinder : QueryResultBinder,
+ val suppressedWarnings : Set<String>) {
val sectionToParamMapping by lazy {
query.bindSections.map {
if (it.text.trim() == "?") {
diff --git a/room/compiler/src/test/data/daoWriter/input/ComplexDao.java b/room/compiler/src/test/data/daoWriter/input/ComplexDao.java
index ba282eb..6272a4f 100644
--- a/room/compiler/src/test/data/daoWriter/input/ComplexDao.java
+++ b/room/compiler/src/test/data/daoWriter/input/ComplexDao.java
@@ -21,6 +21,13 @@
@Dao
abstract class ComplexDao {
+ static class FullName {
+ public int id;
+ public String fullName;
+ }
+ @Query("SELECT name || lastName as fullName, uid as id FROM user where uid = :id")
+ abstract public List<FullName> fullNames(int id);
+
@Query("SELECT * FROM user where uid = :id")
abstract public User getById(int id);
diff --git a/room/compiler/src/test/data/daoWriter/output/ComplexDao.java b/room/compiler/src/test/data/daoWriter/output/ComplexDao.java
index ffcd320..3fac730 100644
--- a/room/compiler/src/test/data/daoWriter/output/ComplexDao.java
+++ b/room/compiler/src/test/data/daoWriter/output/ComplexDao.java
@@ -24,6 +24,29 @@
}
@Override
+ public List<ComplexDao.FullName> fullNames(int id) {
+ final String _sql = "SELECT name || lastName as fullName, uid as id FROM user where uid = ?";
+ final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 1);
+ int _argIndex = 1;
+ _statement.bindLong(_argIndex, id);
+ final Cursor _cursor = __db.query(_statement);
+ try {
+ final List<ComplexDao.FullName> _result = new ArrayList<ComplexDao.FullName>(_cursor.getCount());
+ while(_cursor.moveToNext()) {
+ final ComplexDao.FullName _item;
+ _item = new ComplexDao.FullName();
+ _item.fullName = _cursor.getString(0);
+ _item.id = _cursor.getInt(1);
+ _result.add(_item);
+ }
+ return _result;
+ } finally {
+ _cursor.close();
+ _statement.release();
+ }
+ }
+
+ @Override
public User getById(int id) {
final String _sql = "SELECT * FROM user where uid = ?";
final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 1);
diff --git a/room/compiler/src/test/kotlin/com/android/support/room/processor/DaoProcessorTest.kt b/room/compiler/src/test/kotlin/com/android/support/room/processor/DaoProcessorTest.kt
index bd0c1e1..d6d1abb 100644
--- a/room/compiler/src/test/kotlin/com/android/support/room/processor/DaoProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/com/android/support/room/processor/DaoProcessorTest.kt
@@ -17,6 +17,8 @@
package com.android.support.room.processor
import COMMON
+import com.android.support.room.Room
+import com.android.support.room.RoomWarnings
import com.android.support.room.testing.TestInvocation
import com.android.support.room.testing.TestProcessor
import com.android.support.room.vo.Dao
@@ -136,6 +138,39 @@
}.compilesWithoutError()
}
+ @Test
+ fun suppressedWarnings() {
+ singleDao("""
+ @SuppressWarnings({"ALL", RoomWarnings.CURSOR_MISMATCH})
+ @Dao interface MyDao {
+ @Query("SELECT * from user")
+ abstract User users();
+ }
+ """) { dao, invocation ->
+ assertThat(dao.suppressedWarnings, `is`(setOf("ALL", RoomWarnings.CURSOR_MISMATCH)))
+ dao.queryMethods.forEach {
+ assertThat(it.suppressedWarnings, `is`(setOf("ALL", RoomWarnings.CURSOR_MISMATCH)))
+ }
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun suppressedWarningsInheritance() {
+ singleDao("""
+ @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
+ @Dao interface MyDao {
+ @SuppressWarnings("ALL")
+ @Query("SELECT * from user")
+ abstract User users();
+ }
+ """) { dao, invocation ->
+ assertThat(dao.suppressedWarnings, `is`(setOf(RoomWarnings.CURSOR_MISMATCH)))
+ dao.queryMethods.forEach {
+ assertThat(it.suppressedWarnings, `is`(setOf("ALL", RoomWarnings.CURSOR_MISMATCH)))
+ }
+ }.compilesWithoutError()
+ }
+
fun singleDao(vararg inputs: String, handler: (Dao, TestInvocation) -> Unit):
CompileTester {
return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
diff --git a/room/compiler/src/test/kotlin/com/android/support/room/processor/DatabaseProcessorTest.kt b/room/compiler/src/test/kotlin/com/android/support/room/processor/DatabaseProcessorTest.kt
index 9078341..b3e3002 100644
--- a/room/compiler/src/test/kotlin/com/android/support/room/processor/DatabaseProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/com/android/support/room/processor/DatabaseProcessorTest.kt
@@ -17,6 +17,7 @@
package com.android.support.room.processor
import com.android.support.room.RoomProcessor
+import com.android.support.room.RoomWarnings
import com.android.support.room.testing.TestInvocation
import com.android.support.room.testing.TestProcessor
import com.android.support.room.vo.Database
@@ -258,6 +259,20 @@
))
}
+ @Test
+ fun suppressedWarnings() {
+ singleDb(
+ """
+ @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
+ @Database(entities = {User.class})
+ public abstract class MyDb extends RoomDatabase {
+ abstract UserDao userDao();
+ }
+ """, USER, USER_DAO) {db, invocation ->
+ assertThat(db.suppressedWarnings, `is`(setOf(RoomWarnings.CURSOR_MISMATCH)))
+ }.compilesWithoutError()
+ }
+
fun singleDb(input: String, vararg otherFiles: JavaFileObject,
handler: (Database, TestInvocation) -> Unit): CompileTester {
return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
diff --git a/room/compiler/src/test/kotlin/com/android/support/room/processor/QueryMethodProcessorTest.kt b/room/compiler/src/test/kotlin/com/android/support/room/processor/QueryMethodProcessorTest.kt
index 1099157..db8020d 100644
--- a/room/compiler/src/test/kotlin/com/android/support/room/processor/QueryMethodProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/com/android/support/room/processor/QueryMethodProcessorTest.kt
@@ -17,14 +17,24 @@
package com.android.support.room.processor
import COMMON
+import com.android.support.room.ColumnName
import com.android.support.room.Dao
+import com.android.support.room.Entity
+import com.android.support.room.PrimaryKey
import com.android.support.room.Query
+import com.android.support.room.RoomWarnings
import com.android.support.room.ext.LifecyclesTypeNames
import com.android.support.room.ext.hasAnnotation
import com.android.support.room.ext.typeName
+import com.android.support.room.processor.ProcessorErrors.CANNOT_FIND_QUERY_RESULT_ADAPTER
+import com.android.support.room.solver.query.result.ListQueryResultAdapter
import com.android.support.room.solver.query.result.LiveDataQueryResultBinder
+import com.android.support.room.solver.query.result.PojoRowAdapter
+import com.android.support.room.solver.query.result.RowAdapter
+import com.android.support.room.solver.query.result.SingleEntityQueryResultAdapter
import com.android.support.room.testing.TestInvocation
import com.android.support.room.testing.TestProcessor
+import com.android.support.room.vo.Field
import com.android.support.room.vo.QueryMethod
import com.google.auto.common.MoreElements
import com.google.auto.common.MoreTypes
@@ -41,17 +51,19 @@
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.CoreMatchers.instanceOf
import org.hamcrest.CoreMatchers.notNullValue
+import org.hamcrest.CoreMatchers.nullValue
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Test
import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
import org.junit.runners.Parameterized
+import org.mockito.Mockito
+import javax.lang.model.element.Element
import javax.lang.model.type.TypeKind.INT
import javax.lang.model.type.TypeMirror
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
@RunWith(Parameterized::class)
-class QueryMethodProcessorTest(val enableVerification : Boolean) {
+class QueryMethodProcessorTest(val enableVerification: Boolean) {
companion object {
const val DAO_PREFIX = """
package foo.bar;
@@ -60,9 +72,20 @@
abstract class MyClass {
"""
const val DAO_SUFFIX = "}"
+ val POJO = ClassName.get("foo.bar", "MyClass.Pojo")
@Parameterized.Parameters(name = "enableDbVerification={0}")
@JvmStatic
fun getParams() = arrayOf(true, false)
+
+ fun createField(name: String, columnName: String? = null): Field {
+ return Field(
+ element = Mockito.mock(Element::class.java),
+ name = name,
+ type = Mockito.mock(TypeMirror::class.java),
+ primaryKey = false,
+ columnName = columnName ?: name
+ )
+ }
}
@Test
@@ -393,6 +416,191 @@
}.compilesWithoutError()
}
+ @Test
+ fun suppressWarnings() {
+ singleQueryMethod("""
+ @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
+ @Query("SELECT uid from User")
+ abstract public int[] foo();
+ """) { method, invocation ->
+ assertThat(method.suppressedWarnings, `is`(setOf(RoomWarnings.CURSOR_MISMATCH)))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun pojo_renamedColumn() {
+ pojoTest("""
+ String name;
+ String lName;
+ """, listOf("name", "lastName as lName")) { adapter, queryMethod, invocation ->
+ assertThat(adapter?.mapping?.unusedColumns, `is`(emptyList()))
+ assertThat(adapter?.mapping?.unusedFields, `is`(emptyList()))
+ }?.compilesWithoutError()?.withWarningCount(0)
+ }
+
+ @Test
+ fun pojo_exactMatch() {
+ pojoTest("""
+ String name;
+ String lastName;
+ """, listOf("name", "lastName")) { adapter, queryMethod, invocation ->
+ assertThat(adapter?.mapping?.unusedColumns, `is`(emptyList()))
+ assertThat(adapter?.mapping?.unusedFields, `is`(emptyList()))
+ }?.compilesWithoutError()?.withWarningCount(0)
+ }
+
+ @Test
+ fun pojo_exactMatchWithStar() {
+ pojoTest("""
+ String name;
+ String lastName;
+ int uid;
+ @ColumnName("ageColumn")
+ int age;
+ """, listOf("*")) { adapter, queryMethod, invocation ->
+ assertThat(adapter?.mapping?.unusedColumns, `is`(emptyList()))
+ assertThat(adapter?.mapping?.unusedFields, `is`(emptyList()))
+ }?.compilesWithoutError()?.withWarningCount(0)
+ }
+
+ @Test
+ fun pojo_nonJavaName() {
+ pojoTest("""
+ @ColumnName("MAX(ageColumn)")
+ int maxAge;
+ String name;
+ """, listOf("MAX(ageColumn)", "name")) { adapter, queryMethod, invocation ->
+ assertThat(adapter?.mapping?.unusedColumns, `is`(emptyList()))
+ assertThat(adapter?.mapping?.unusedFields, `is`(emptyList()))
+ }?.compilesWithoutError()?.withWarningCount(0)
+ }
+
+ @Test
+ fun pojo_noMatchingFields() {
+ pojoTest("""
+ String nameX;
+ String lastNameX;
+ """, listOf("name", "lastName")) { adapter, queryMethod, invocation ->
+ assertThat(adapter?.mapping?.unusedColumns, `is`(listOf("name", "lastName")))
+ assertThat(adapter?.mapping?.unusedFields, `is`(adapter?.pojo?.fields))
+ }?.failsToCompile()
+ ?.withErrorContaining(CANNOT_FIND_QUERY_RESULT_ADAPTER)
+ ?.and()
+ ?.withWarningContaining(
+ ProcessorErrors.cursorPojoMismatch(
+ pojoTypeName = POJO,
+ unusedColumns = listOf("name", "lastName"),
+ unusedFields = listOf(createField("nameX"),
+ createField("lastNameX")),
+ allColumns = listOf("name", "lastName"),
+ allFields = listOf(createField("nameX"), createField("lastNameX"))
+ )
+ )
+ }
+
+ @Test
+ fun pojo_badQuery() {
+ // do not report mismatch if query is broken
+ pojoTest("""
+ @ColumnName("MAX(ageColumn)")
+ int maxAge;
+ String name;
+ """, listOf("MAX(age)", "name")) { adapter, queryMethod, invocation ->
+ }?.failsToCompile()
+ ?.withErrorContaining("no such column: age")
+ ?.and()
+ ?.withErrorCount(1)
+ ?.withWarningCount(0)
+ }
+
+ @Test
+ fun pojo_tooManyColumns() {
+ pojoTest("""
+ String name;
+ String lastName;
+ """, listOf("uid", "name", "lastName")) { adapter, queryMethod, invocation ->
+ assertThat(adapter?.mapping?.unusedColumns, `is`(listOf("uid")))
+ assertThat(adapter?.mapping?.unusedFields, `is`(emptyList()))
+ }?.compilesWithoutError()?.withWarningContaining(
+ ProcessorErrors.cursorPojoMismatch(
+ pojoTypeName = POJO,
+ unusedColumns = listOf("uid"),
+ unusedFields = emptyList(),
+ allColumns = listOf("uid", "name", "lastName"),
+ allFields = listOf(createField("name"), createField("lastName"))
+ ))
+ }
+
+ @Test
+ fun pojo_tooManyFields() {
+ pojoTest("""
+ String name;
+ String lastName;
+ """, listOf("lastName")) { adapter, queryMethod, invocation ->
+ assertThat(adapter?.mapping?.unusedColumns, `is`(emptyList()))
+ assertThat(adapter?.mapping?.unusedFields, `is`(
+ adapter?.pojo?.fields?.filter { it.name == "name" }
+ ))
+ }?.compilesWithoutError()?.withWarningContaining(
+ ProcessorErrors.cursorPojoMismatch(
+ pojoTypeName = POJO,
+ unusedColumns = emptyList(),
+ unusedFields = listOf(createField("name")),
+ allColumns = listOf("lastName"),
+ allFields = listOf(createField("name"), createField("lastName"))
+ ))
+ }
+
+ @Test
+ fun pojo_tooManyFieldsAndColumns() {
+ pojoTest("""
+ String name;
+ String lastName;
+ """, listOf("uid", "name")) { adapter, queryMethod, invocation ->
+ assertThat(adapter?.mapping?.unusedColumns, `is`(listOf("uid")))
+ assertThat(adapter?.mapping?.unusedFields, `is`(
+ adapter?.pojo?.fields?.filter { it.name == "lastName" }
+ ))
+ }?.compilesWithoutError()?.withWarningContaining(
+ ProcessorErrors.cursorPojoMismatch(
+ pojoTypeName = POJO,
+ unusedColumns = listOf("uid"),
+ unusedFields = listOf(createField("lastName")),
+ allColumns = listOf("uid", "name"),
+ allFields = listOf(createField("name"), createField("lastName"))
+ ))
+ }
+
+ fun pojoTest(pojoFields: String, queryColumns: List<String>,
+ handler: (PojoRowAdapter?, QueryMethod, TestInvocation) -> Unit): CompileTester? {
+ val assertion = singleQueryMethod(
+ """
+ static class Pojo {
+ $pojoFields
+ }
+ @Query("SELECT ${queryColumns.joinToString(", ")} from User LIMIT 1")
+ abstract MyClass.Pojo getNameAndLastNames();
+ """
+ ) { parsedQuery, invocation ->
+ val adapter = parsedQuery.queryResultBinder.adapter
+ if (enableVerification) {
+ if (adapter is SingleEntityQueryResultAdapter) {
+ handler(adapter.rowAdapter as? PojoRowAdapter, parsedQuery, invocation)
+ } else {
+ handler(null, parsedQuery, invocation)
+ }
+ } else {
+ assertThat(adapter, nullValue())
+ }
+ }
+ if (enableVerification) {
+ return assertion
+ } else {
+ assertion.failsToCompile().withErrorContaining(CANNOT_FIND_QUERY_RESULT_ADAPTER)
+ return null
+ }
+ }
+
fun singleQueryMethod(vararg input: String,
handler: (QueryMethod, TestInvocation) -> Unit):
CompileTester {
@@ -401,7 +609,8 @@
DAO_PREFIX + input.joinToString("\n") + DAO_SUFFIX
), COMMON.LIVE_DATA, COMMON.COMPUTABLE_LIVE_DATA, COMMON.USER))
.processedWith(TestProcessor.builder()
- .forAnnotations(Query::class, Dao::class)
+ .forAnnotations(Query::class, Dao::class, ColumnName::class,
+ Entity::class, PrimaryKey::class)
.nextRunHandler { invocation ->
val (owner, methods) = invocation.roundEnv
.getElementsAnnotatedWith(Dao::class.java)
@@ -422,7 +631,7 @@
val parser = QueryMethodProcessor(invocation.context)
parser.dbVerifier = verifier
val parsedQuery = parser.parse(MoreTypes.asDeclared(owner.asType()),
- MoreElements.asExecutable(methods.first()))
+ MoreElements.asExecutable(methods.first()), emptySet())
handler(parsedQuery, invocation)
true
}
diff --git a/room/compiler/src/test/kotlin/com/android/support/room/solver/query/QueryWriterTest.kt b/room/compiler/src/test/kotlin/com/android/support/room/solver/query/QueryWriterTest.kt
index 0c4a37f..8876071 100644
--- a/room/compiler/src/test/kotlin/com/android/support/room/solver/query/QueryWriterTest.kt
+++ b/room/compiler/src/test/kotlin/com/android/support/room/solver/query/QueryWriterTest.kt
@@ -300,7 +300,7 @@
}.filter { it.second.isNotEmpty() }.first()
val parser = QueryMethodProcessor(invocation.context)
val parsedQuery = parser.parse(MoreTypes.asDeclared(owner.asType()),
- MoreElements.asExecutable(methods.first()))
+ MoreElements.asExecutable(methods.first()), emptySet())
handler(QueryWriter(parsedQuery))
true
}
diff --git a/room/compiler/src/test/kotlin/com/android/support/room/testing/TestProcessor.kt b/room/compiler/src/test/kotlin/com/android/support/room/testing/TestProcessor.kt
index 907f2b6..0bf42c6 100644
--- a/room/compiler/src/test/kotlin/com/android/support/room/testing/TestProcessor.kt
+++ b/room/compiler/src/test/kotlin/com/android/support/room/testing/TestProcessor.kt
@@ -23,7 +23,7 @@
import javax.lang.model.element.TypeElement
import kotlin.reflect.KClass
-@SupportedSourceVersion(SourceVersion.RELEASE_7)
+@SupportedSourceVersion(SourceVersion.RELEASE_8)// test are compiled w/ J_8
class TestProcessor(val handlers: List<(TestInvocation) -> Boolean>,
val annotations: MutableSet<String>) : AbstractProcessor() {
var count = 0
diff --git a/room/compiler/src/test/kotlin/com/android/support/room/writer/DaoWriterTest.kt b/room/compiler/src/test/kotlin/com/android/support/room/writer/DaoWriterTest.kt
index 9821688..88997a6 100644
--- a/room/compiler/src/test/kotlin/com/android/support/room/writer/DaoWriterTest.kt
+++ b/room/compiler/src/test/kotlin/com/android/support/room/writer/DaoWriterTest.kt
@@ -22,6 +22,7 @@
import com.google.common.truth.Truth
import com.google.testing.compile.CompileTester
import com.google.testing.compile.JavaSourcesSubjectFactory
+import createVerifierFromEntities
import loadJavaCode
import org.junit.Test
import org.junit.runner.RunWith
@@ -69,6 +70,7 @@
com.android.support.room.Dao::class.java)
.first()
val parser = DaoProcessor(invocation.context)
+ parser.dbVerifier = createVerifierFromEntities(invocation)
val parsedDao = parser.parse(MoreElements.asType(dao))
DaoWriter(parsedDao).write(invocation.processingEnv)
true
diff --git a/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/dao/UserDao.java b/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/dao/UserDao.java
index e760b83..c892086 100644
--- a/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/dao/UserDao.java
+++ b/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/dao/UserDao.java
@@ -21,10 +21,12 @@
import com.android.support.room.Delete;
import com.android.support.room.Insert;
import com.android.support.room.Query;
+import com.android.support.room.integration.testapp.vo.AvgWeightByAge;
import com.android.support.room.integration.testapp.vo.User;
import java.util.List;
+@SuppressWarnings("SameParameterValue")
@Dao
public interface UserDao {
@Query("select * from user where mName like :name")
@@ -83,4 +85,10 @@
@Query("select * from user where mAge = ?")
List<User> findByAge(int age);
+
+ @Query("select mAge, AVG(mWeight) from user GROUP BY mAge ORDER BY 2 DESC")
+ List<AvgWeightByAge> weightByAge();
+
+ @Query("select mAge, AVG(mWeight) from user GROUP BY mAge ORDER BY 2 DESC LIMIT 1")
+ LiveData<AvgWeightByAge> maxWeightByAgeGroup();
}
diff --git a/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/test/LiveDataQueryTest.java b/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/test/LiveDataQueryTest.java
index 144e02e..c96ac70 100644
--- a/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/test/LiveDataQueryTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/test/LiveDataQueryTest.java
@@ -36,6 +36,7 @@
import com.android.support.room.Room;
import com.android.support.room.integration.testapp.TestDatabase;
import com.android.support.room.integration.testapp.dao.UserDao;
+import com.android.support.room.integration.testapp.vo.AvgWeightByAge;
import com.android.support.room.integration.testapp.vo.User;
import org.junit.Before;
@@ -150,6 +151,42 @@
assertThat(observer.get(), is(Arrays.asList(user4, user3)));
}
+ @Test
+ public void liveDataWithPojo() throws ExecutionException, InterruptedException {
+ User[] users = TestUtil.createUsersArray(3, 5, 7, 9);
+ users[0].setAge(10);
+ users[0].setWeight(15);
+
+ users[1].setAge(20);
+ users[1].setWeight(25);
+
+ users[2].setAge(20);
+ users[2].setWeight(26);
+
+ users[3].setAge(10);
+ users[3].setWeight(21);
+
+ final TestLifecycleProvider lifecycleProvider = new TestLifecycleProvider();
+ lifecycleProvider.handleEvent(Lifecycle.ON_START);
+
+ final LatchObserver<AvgWeightByAge> observer = new LatchObserver<>();
+ LiveData<AvgWeightByAge> liveData = mUserDao.maxWeightByAgeGroup();
+
+ observe(liveData, lifecycleProvider, observer);
+ assertThat(observer.get(), is(nullValue()));
+
+ observer.reset();
+ mUserDao.insertAll(users);
+ assertThat(observer.get(), is(new AvgWeightByAge(20, 25.5f)));
+
+ observer.reset();
+ User user3 = mUserDao.load(3);
+ user3.setWeight(79);
+ mUserDao.insertOrReplace(user3);
+
+ assertThat(observer.get(), is(new AvgWeightByAge(10, 50)));
+ }
+
private void observe(final LiveData liveData, final LifecycleProvider provider,
final Observer observer) throws ExecutionException, InterruptedException {
FutureTask<Void> futureTask = new FutureTask<>(new Callable<Void>() {
diff --git a/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/test/PojoTest.java b/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/test/PojoTest.java
new file mode 100644
index 0000000..821606c
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/test/PojoTest.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.room.integration.testapp.test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.support.room.Room;
+import com.android.support.room.integration.testapp.TestDatabase;
+import com.android.support.room.integration.testapp.dao.UserDao;
+import com.android.support.room.integration.testapp.vo.AvgWeightByAge;
+import com.android.support.room.integration.testapp.vo.User;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+public class PojoTest {
+ private UserDao mUserDao;
+
+ @Before
+ public void createDb() {
+ Context context = InstrumentationRegistry.getTargetContext();
+ TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build();
+ mUserDao = db.getUserDao();
+ }
+
+ @Test
+ public void weightsByAge() {
+ User[] users = TestUtil.createUsersArray(3, 5, 7, 10);
+ users[0].setAge(10);
+ users[0].setWeight(20);
+
+ users[1].setAge(10);
+ users[1].setWeight(30);
+
+ users[2].setAge(15);
+ users[2].setWeight(12);
+
+ users[3].setAge(35);
+ users[3].setWeight(55);
+
+ mUserDao.insertAll(users);
+ assertThat(mUserDao.weightByAge(), is(
+ Arrays.asList(
+ new AvgWeightByAge(35, 55),
+ new AvgWeightByAge(10, 25),
+ new AvgWeightByAge(15, 12)
+ )
+ ));
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/vo/AvgWeightByAge.java b/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/vo/AvgWeightByAge.java
new file mode 100644
index 0000000..7bf1715
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/vo/AvgWeightByAge.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.room.integration.testapp.vo;
+
+import com.android.support.room.ColumnName;
+
+@SuppressWarnings("unused")
+public class AvgWeightByAge {
+
+ private int mAge;
+
+ @ColumnName("AVG(mWeight)")
+ private float mWeight;
+
+ public AvgWeightByAge() {
+ }
+
+ public AvgWeightByAge(int age, float weight) {
+ mAge = age;
+ mWeight = weight;
+ }
+
+ public int getAge() {
+ return mAge;
+ }
+
+ public void setAge(int age) {
+ mAge = age;
+ }
+
+ public float getWeight() {
+ return mWeight;
+ }
+
+ public void setWeight(float weight) {
+ mWeight = weight;
+ }
+
+ @Override
+ public String toString() {
+ return "AvgWeightByAge{"
+ + "mAge=" + mAge
+ + ", mWeight=" + mWeight
+ + '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ AvgWeightByAge that = (AvgWeightByAge) o;
+
+ //noinspection SimplifiableIfStatement
+ if (mAge != that.mAge) {
+ return false;
+ }
+ return Float.compare(that.mWeight, mWeight) == 0;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mAge;
+ result = 31 * result + (mWeight != +0.0f ? Float.floatToIntBits(mWeight) : 0);
+ return result;
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/vo/User.java b/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/vo/User.java
index aa75486..da0ab17 100644
--- a/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/vo/User.java
+++ b/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/vo/User.java
@@ -21,13 +21,20 @@
@Entity
public class User {
+
@PrimaryKey
private int mId;
+
private String mName;
+
private String mLastName;
+
private int mAge;
+
private boolean mAdmin;
+ private float mWeight;
+
public int getId() {
return mId;
}
@@ -68,17 +75,41 @@
mAdmin = admin;
}
+ public float getWeight() {
+ return mWeight;
+ }
+
+ public void setWeight(float weight) {
+ mWeight = weight;
+ }
+
@Override
public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
User user = (User) o;
- if (mId != user.mId) return false;
- if (mAge != user.mAge) return false;
- if (mAdmin != user.mAdmin) return false;
- if (mName != null ? !mName.equals(user.mName) : user.mName != null) return false;
+ if (mId != user.mId) {
+ return false;
+ }
+ if (mAge != user.mAge) {
+ return false;
+ }
+ if (mAdmin != user.mAdmin) {
+ return false;
+ }
+ if (Float.compare(user.mWeight, mWeight) != 0) {
+ return false;
+ }
+ //noinspection SimplifiableIfStatement
+ if (mName != null ? !mName.equals(user.mName) : user.mName != null) {
+ return false;
+ }
return mLastName != null ? mLastName.equals(user.mLastName) : user.mLastName == null;
}
@@ -89,6 +120,19 @@
result = 31 * result + (mLastName != null ? mLastName.hashCode() : 0);
result = 31 * result + mAge;
result = 31 * result + (mAdmin ? 1 : 0);
+ result = 31 * result + (mWeight != +0.0f ? Float.floatToIntBits(mWeight) : 0);
return result;
}
+
+ @Override
+ public String toString() {
+ return "User{"
+ + "mId=" + mId
+ + ", mName='" + mName + '\''
+ + ", mLastName='" + mLastName + '\''
+ + ", mAge=" + mAge
+ + ", mAdmin=" + mAdmin
+ + ", mWeight=" + mWeight
+ + '}';
+ }
}