blob: 810202eaf81125a1f0816ad0a08e18b334f9fa10 [file] [log] [blame]
/*
* Copyright 2021 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.solver.query.result
import androidx.room.compiler.codegen.CodeLanguage
import androidx.room.compiler.codegen.XClassName
import androidx.room.compiler.codegen.XCodeBlock
import androidx.room.compiler.processing.XType
import androidx.room.ext.CollectionTypeNames
import androidx.room.ext.CommonTypeNames
import androidx.room.ext.implementsEqualsAndHashcode
import androidx.room.parser.ParsedQuery
import androidx.room.processor.Context
import androidx.room.processor.ProcessorErrors
import androidx.room.processor.ProcessorErrors.AmbiguousColumnLocation.ENTITY
import androidx.room.processor.ProcessorErrors.AmbiguousColumnLocation.MAP_INFO
import androidx.room.processor.ProcessorErrors.AmbiguousColumnLocation.POJO
import androidx.room.solver.types.CursorValueReader
import androidx.room.vo.ColumnIndexVar
import androidx.room.vo.MapInfo
import androidx.room.vo.Warning
/**
* Abstract class for Map and Multimap result adapters.
*/
abstract class MultimapQueryResultAdapter(
context: Context,
parsedQuery: ParsedQuery,
rowAdapters: List<RowAdapter>,
) : QueryResultAdapter(rowAdapters) {
abstract val keyTypeArg: XType
abstract val valueTypeArg: XType
// List of duplicate columns in the query result. Note that if the query result info is not
// available then we use the adapter mappings to determine if there are duplicate columns.
// The latter approach might yield false positive (i.e. two POJOs that want the same column)
// but the resolver will still produce correct results based on the result columns at runtime.
val duplicateColumns: Set<String>
init {
val resultColumns =
parsedQuery.resultInfo?.columns?.map { it.name } ?: mappings.flatMap { it.usedColumns }
duplicateColumns = buildSet {
val visitedColumns = mutableSetOf<String>()
resultColumns.forEach {
// When Set.add() returns false the column is already visited and therefore a dupe.
if (!visitedColumns.add(it)) {
add(it)
}
}
}
if (parsedQuery.resultInfo != null && duplicateColumns.isNotEmpty()) {
// If there are duplicate columns and one of the result object is for a single column
// then we should warn the user to disambiguate in the query projections since the
// current AmbiguousColumnResolver will choose the first matching column. Only show
// this warning if the query has been analyzed or else we risk false positives.
mappings.filter {
it.usedColumns.size == 1 && duplicateColumns.contains(it.usedColumns.first())
}.forEach {
val ambiguousColumnName = it.usedColumns.first()
val (location, objectTypeName) = when (it) {
is SingleNamedColumnRowAdapter.SingleNamedColumnRowMapping ->
MAP_INFO to null
is PojoRowAdapter.PojoMapping ->
POJO to it.pojo.typeName
is EntityRowAdapter.EntityMapping ->
ENTITY to it.entity.typeName
else -> error("Unknown mapping type: $it")
}
context.logger.w(
Warning.AMBIGUOUS_COLUMN_IN_RESULT,
ProcessorErrors.ambiguousColumn(
columnName = ambiguousColumnName,
location = location,
typeName = objectTypeName?.toString(context.codeLanguage)
)
)
}
}
}
enum class MapType(val className: XClassName) {
DEFAULT(CommonTypeNames.MUTABLE_MAP),
ARRAY_MAP(CollectionTypeNames.ARRAY_MAP),
LONG_SPARSE(CollectionTypeNames.LONG_SPARSE_ARRAY),
INT_SPARSE(CollectionTypeNames.INT_SPARSE_ARRAY);
companion object {
fun MapType.isSparseArray() = this == LONG_SPARSE || this == INT_SPARSE
}
}
enum class CollectionValueType(val className: XClassName) {
LIST(CommonTypeNames.MUTABLE_LIST),
SET(CommonTypeNames.MUTABLE_SET)
}
companion object {
/**
* Checks if the @MapInfo annotation is needed for clarification regarding the return type
* of a Dao method.
*/
fun validateMapTypeArgs(
context: Context,
keyTypeArg: XType,
valueTypeArg: XType,
keyReader: CursorValueReader?,
valueReader: CursorValueReader?,
mapInfo: MapInfo?,
) {
if (!keyTypeArg.implementsEqualsAndHashcode()) {
context.logger.w(
Warning.DOES_NOT_IMPLEMENT_EQUALS_HASHCODE,
ProcessorErrors.classMustImplementEqualsAndHashCode(
keyTypeArg.asTypeName().toString(context.codeLanguage)
)
)
}
val hasKeyColumnName = mapInfo?.keyColumnName?.isNotEmpty() ?: false
if (!hasKeyColumnName && keyReader != null) {
context.logger.e(
ProcessorErrors.keyMayNeedMapInfo(
keyTypeArg.asTypeName().toString(context.codeLanguage)
)
)
}
val hasValueColumnName = mapInfo?.valueColumnName?.isNotEmpty() ?: false
if (!hasValueColumnName && valueReader != null) {
context.logger.e(
ProcessorErrors.valueMayNeedMapInfo(
valueTypeArg.asTypeName().toString(context.codeLanguage)
)
)
}
}
}
/**
* Generates a code expression that verifies if all matched fields are null.
*/
fun getColumnNullCheckCode(
language: CodeLanguage,
cursorVarName: String,
indexVars: List<ColumnIndexVar>
) = XCodeBlock.builder(language).apply {
val space = when (language) {
CodeLanguage.JAVA -> "%W"
CodeLanguage.KOTLIN -> " "
}
val conditions = indexVars.map {
XCodeBlock.of(
language,
"%L.isNull(%L)",
cursorVarName,
it.indexVar
)
}
val placeholders = conditions.joinToString(separator = "$space&&$space") { "%L" }
add(placeholders, *conditions.toTypedArray())
}.build()
}