blob: 95e4a8fabd66258f77be866a0f24c839ccdac1db [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
import androidx.room.compiler.processing.XNullability
import androidx.room.compiler.processing.XNullability.NONNULL
import androidx.room.compiler.processing.XNullability.NULLABLE
import androidx.room.compiler.processing.XNullability.UNKNOWN
import androidx.room.compiler.processing.XProcessingEnv.Backend
import androidx.room.compiler.processing.XType
import androidx.room.processor.Context
import androidx.room.solver.types.CompositeTypeConverter
import androidx.room.solver.types.NoOpConverter
import androidx.room.solver.types.NullSafeTypeConverter
import androidx.room.solver.types.RequireNotNullTypeConverter
import androidx.room.solver.types.TypeConverter
import androidx.room.solver.types.UpCastTypeConverter
import java.util.PriorityQueue
/**
* A [TypeConverterStore] implementation that generates better code when we have the nullability
* information in types. It is enabled by default only in KSP backend but it can also be turned
* on via the [Context.BooleanProcessorOptions.USE_NULL_AWARE_CONVERTER] flag.
*
* This [TypeConverterStore] tries to maintain the nullability of the input/output type
* when writing into/reading from database. Even though nullability preservation is preferred, it is
* not strictly required such that it will fall back to the mismatched nullability.
*/
class NullAwareTypeConverterStore(
context: Context,
/**
* Available TypeConverters. Note that we might synthesize new type converters based on this
* list.
*/
typeConverters: List<TypeConverter>,
/**
* List of types that can be saved into db/read from without a converter.
*/
private val knownColumnTypes: List<XType>
) : TypeConverterStore {
override val typeConverters = if (context.processingEnv.backend == Backend.KSP) {
val processedConverters = typeConverters.toMutableList()
// create copies for converters that receive non-null values
typeConverters.forEach { converter ->
if (converter.from.nullability == NONNULL) {
val candidate = NullSafeTypeConverter(delegate = converter)
// before we add this null safe converter, make sure there is no other converter
// that would already handle the same arguments.
val match = processedConverters.any { other ->
other.from.isAssignableFrom(candidate.from) &&
candidate.to.isAssignableFrom(other.to)
}
if (!match) {
processedConverters.add(candidate)
}
}
}
processedConverters
} else {
typeConverters
}
// cache for type converter lookups to avoid traversing all of the list every time we need to
// find possible converters for a type. Unlike JAVAC, KSP supports equality in its objects so
// this tends to work rather well.
private val typeConvertersByFromCache = mutableMapOf<XType, List<TypeConverter>>()
private val typeConvertersByToCache = mutableMapOf<XType, List<TypeConverter>>()
/**
* Known column types that are nullable.
* Used in [getColumnTypesInPreferenceBuckets] to avoid re-partitioning known type lists.
*/
private val knownNullableColumnTypes by lazy {
knownColumnTypes.filter { it.nullability == NULLABLE }
}
/**
* Known column types that are non-null or have unknown nullability.
* Used in [getColumnTypesInPreferenceBuckets] to avoid re-partitioning known type lists.
*/
private val knownNonNullableColumnTypes by lazy {
knownColumnTypes.filter { it.nullability != NULLABLE }
}
/**
* Returns a list of lists for the given type, ordered by preference buckets for
* the given nullability.
*/
private fun getColumnTypesInPreferenceBuckets(
nullability: XNullability,
explicitColumnTypes: List<XType>?
): List<List<XType>> {
return if (explicitColumnTypes == null) {
when (nullability) {
NULLABLE -> {
// prioritize nulls
listOf(
knownNullableColumnTypes,
knownNonNullableColumnTypes
)
}
NONNULL -> {
// prioritize non-null
listOf(
knownNonNullableColumnTypes,
knownNullableColumnTypes
)
}
else -> {
// we don't know, YOLO
listOf(knownColumnTypes)
}
}
} else {
when (nullability) {
UNKNOWN -> listOf(explicitColumnTypes)
else -> listOf(
explicitColumnTypes.filter { it.nullability == nullability },
explicitColumnTypes.filter { it.nullability != nullability }
)
}
}
}
override fun findConverterIntoStatement(
input: XType,
columnTypes: List<XType>?
): TypeConverter? {
getColumnTypesInPreferenceBuckets(
nullability = input.nullability,
explicitColumnTypes = columnTypes
).forEach { types ->
findConverterIntoStatementInternal(
input = input,
columnTypes = types
)?.getOrCreateConverter()?.let {
return it
}
}
return null
}
private fun findConverterIntoStatementInternal(
input: XType,
columnTypes: List<XType>
): TypeConverterEntry? {
if (columnTypes.isEmpty()) return null
val queue = TypeConverterQueue(
sourceType = input,
// each converter is keyed on which type they will take us to
keyType = TypeConverter::to
)
while (true) {
val current = queue.next() ?: break
val match = columnTypes.any { columnType ->
columnType.isSameType(current.type)
}
if (match) {
return current
}
// check for assignable matches but only enqueue them as there might be another shorter
// path
columnTypes.forEach { columnType ->
if (columnType.isAssignableFrom(current.type)) {
queue.maybeEnqueue(
current.appendConverter(
UpCastTypeConverter(
upCastFrom = current.type,
upCastTo = columnType
)
)
)
}
}
getAllTypeConvertersFrom(current.type).forEach {
queue.maybeEnqueue(current.appendConverter(it))
}
}
return null
}
override fun findConverterFromCursor(
columnTypes: List<XType>?,
output: XType
): TypeConverter? {
@Suppress("NAME_SHADOWING") // intentional
val columnTypes = columnTypes ?: knownColumnTypes
// prefer nullable when reading from database, regardless of the output type
getColumnTypesInPreferenceBuckets(
nullability = NULLABLE,
explicitColumnTypes = columnTypes
).forEach { types ->
findConverterFromCursorInternal(
columnTypes = types,
output = output
)?.let {
return it.getOrCreateConverter()
}
}
// if type is non-null, try to find nullable and add null check
return if (output.nullability == NONNULL) {
findConverterFromCursorInternal(
columnTypes = columnTypes,
output = output.makeNullable()
)?.appendConverter(
RequireNotNullTypeConverter(
from = output.makeNullable()
)
)
} else {
null
}
}
private fun findConverterFromCursorInternal(
columnTypes: List<XType>,
output: XType
): TypeConverterEntry? {
if (columnTypes.isEmpty()) return null
val queue = TypeConverterQueue(
sourceType = output,
// each converter is keyed on which type they receive as we are doing pathfinding
// reverse here
keyType = TypeConverter::from
)
while (true) {
val current = queue.next() ?: break
val match = columnTypes.any { columnType ->
columnType.isSameType(current.type)
}
if (match) {
return current
}
// check for assignable matches but only enqueue them as there might be another shorter
// path
columnTypes.forEach { columnType ->
if (current.type.isAssignableFrom(columnType)) {
queue.maybeEnqueue(
current.prependConverter(
UpCastTypeConverter(
upCastFrom = columnType,
upCastTo = current.type
)
)
)
}
}
getAllTypeConvertersTo(current.type).forEach {
queue.maybeEnqueue(current.prependConverter(it))
}
}
return null
}
override fun findTypeConverter(input: XType, output: XType): TypeConverter? {
return findConverterIntoStatementInternal(
input = input,
columnTypes = listOf(output)
)?.getOrCreateConverter()
}
/**
* Returns all type converters that can receive input type and return into another type.
*/
private fun getAllTypeConvertersFrom(
input: XType
): List<TypeConverter> {
// for input, check assignability because it defines whether we can use the method or not.
return typeConvertersByFromCache.getOrPut(input) {
// this cache avoids us many assignability checks.
typeConverters.mapNotNull { converter ->
when {
converter.from.isSameType(input) -> converter
converter.from.isAssignableFrom(input) -> CompositeTypeConverter(
conv1 = UpCastTypeConverter(
upCastFrom = input,
upCastTo = converter.from
),
conv2 = converter
)
else -> null
}
}
}
}
/**
* Returns all type converters that can return the output type.
*/
private fun getAllTypeConvertersTo(
output: XType
): List<TypeConverter> {
return typeConvertersByToCache.getOrPut(output) {
// this cache avoids us many assignability checks.
typeConverters.mapNotNull { converter ->
when {
converter.to.isSameType(output) -> converter
output.isAssignableFrom(converter.to) -> CompositeTypeConverter(
conv1 = converter,
conv2 = UpCastTypeConverter(
upCastFrom = converter.to,
upCastTo = output
)
)
else -> null
}
}
}
}
/**
* Priority queue for the type converter search.
*/
private class TypeConverterQueue(
sourceType: XType,
val keyType: TypeConverter.() -> XType
) {
// using insertion order as the tie breaker for reproducible builds.
private var insertionOrder = 0
// map of XType to the converter that includes the path from the source type to the XType.
private val cheapestEntry = mutableMapOf<XType, TypeConverterEntry>()
private val queue = PriorityQueue<TypeConverterEntry>()
init {
val typeConverterEntry = TypeConverterEntry(
tieBreakerPriority = insertionOrder++,
type = sourceType,
converter = null
)
cheapestEntry[sourceType] = typeConverterEntry
queue.add(typeConverterEntry)
}
fun next(): TypeConverterEntry? {
while (queue.isNotEmpty()) {
val entry = queue.remove()
// check if we processed this type as there is no reason to process it again
if (cheapestEntry[entry.type] !== entry) {
continue
}
return entry
}
return null
}
/**
* Enqueues the given [converter] if its target type (defined by [keyType]) is not visited
* or visited with a more expensive converter.
*/
fun maybeEnqueue(
converter: TypeConverter
): Boolean {
val keyType = converter.keyType()
val existing = cheapestEntry[keyType]
if (existing == null ||
(existing.converter != null && existing.converter.cost > converter.cost)
) {
val entry = TypeConverterEntry(insertionOrder++, keyType, converter)
cheapestEntry[keyType] = entry
queue.add(entry)
return true
}
return false
}
}
private data class TypeConverterEntry(
// when costs are equal, tieBreakerPriority is used
val tieBreakerPriority: Int,
val type: XType,
val converter: TypeConverter?
) : Comparable<TypeConverterEntry> {
override fun compareTo(other: TypeConverterEntry): Int {
if (converter == null) {
if (other.converter != null) {
return -1
}
} else if (other.converter == null) {
return 1
} else {
val costCmp = converter.cost.compareTo(other.converter.cost)
if (costCmp != 0) {
return costCmp
}
}
return tieBreakerPriority.compareTo(other.tieBreakerPriority)
}
fun getOrCreateConverter() = converter ?: NoOpConverter(type)
fun appendConverter(nextConverter: TypeConverter): TypeConverter {
if (converter == null) {
return nextConverter
}
return CompositeTypeConverter(
conv1 = converter,
conv2 = nextConverter
)
}
fun prependConverter(previous: TypeConverter): TypeConverter {
if (converter == null) {
return previous
}
return CompositeTypeConverter(
conv1 = previous,
conv2 = converter
)
}
}
}