Add table names of key and value columns in @MapInfo
This allow users with ambiguous columns and tables alias to make it clear which table.column should be used as key or value via @MapInfo.
Note that this CL specifically does not use the table information to resolve the column index on a duplicate column query, but might do so with a latter change.
Relnote: "Added APIs for providing key and value tables names for disambiguation in @MapInfo"
Test: QueryMethodProcessorTest
Change-Id: Icc4b50a029d33c49fbfe7265d10b6be1b15da9c3
diff --git a/room/room-common/api/current.txt b/room/room-common/api/current.txt
index 6323e9e..596a7b2 100644
--- a/room/room-common/api/current.txt
+++ b/room/room-common/api/current.txt
@@ -255,9 +255,13 @@
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface MapInfo {
method public abstract String keyColumn() default "";
+ method public abstract String keyTable() default "";
method public abstract String valueColumn() default "";
+ method public abstract String valueTable() default "";
property public abstract String keyColumn;
+ property public abstract String keyTable;
property public abstract String valueColumn;
+ property public abstract String valueTable;
}
@IntDef({androidx.room.OnConflictStrategy.Companion.NONE, androidx.room.OnConflictStrategy.Companion.REPLACE, androidx.room.OnConflictStrategy.Companion.ROLLBACK, androidx.room.OnConflictStrategy.Companion.ABORT, androidx.room.OnConflictStrategy.Companion.FAIL, androidx.room.OnConflictStrategy.Companion.IGNORE}) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface OnConflictStrategy {
diff --git a/room/room-common/api/public_plus_experimental_current.txt b/room/room-common/api/public_plus_experimental_current.txt
index 6323e9e..596a7b2 100644
--- a/room/room-common/api/public_plus_experimental_current.txt
+++ b/room/room-common/api/public_plus_experimental_current.txt
@@ -255,9 +255,13 @@
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface MapInfo {
method public abstract String keyColumn() default "";
+ method public abstract String keyTable() default "";
method public abstract String valueColumn() default "";
+ method public abstract String valueTable() default "";
property public abstract String keyColumn;
+ property public abstract String keyTable;
property public abstract String valueColumn;
+ property public abstract String valueTable;
}
@IntDef({androidx.room.OnConflictStrategy.Companion.NONE, androidx.room.OnConflictStrategy.Companion.REPLACE, androidx.room.OnConflictStrategy.Companion.ROLLBACK, androidx.room.OnConflictStrategy.Companion.ABORT, androidx.room.OnConflictStrategy.Companion.FAIL, androidx.room.OnConflictStrategy.Companion.IGNORE}) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface OnConflictStrategy {
diff --git a/room/room-common/api/restricted_current.txt b/room/room-common/api/restricted_current.txt
index 80ed020..b3db08c 100644
--- a/room/room-common/api/restricted_current.txt
+++ b/room/room-common/api/restricted_current.txt
@@ -255,9 +255,13 @@
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface MapInfo {
method public abstract String keyColumn() default "";
+ method public abstract String keyTable() default "";
method public abstract String valueColumn() default "";
+ method public abstract String valueTable() default "";
property public abstract String keyColumn;
+ property public abstract String keyTable;
property public abstract String valueColumn;
+ property public abstract String valueTable;
}
@IntDef({androidx.room.OnConflictStrategy.Companion.NONE, androidx.room.OnConflictStrategy.Companion.REPLACE, androidx.room.OnConflictStrategy.Companion.ROLLBACK, androidx.room.OnConflictStrategy.Companion.ABORT, androidx.room.OnConflictStrategy.Companion.FAIL, androidx.room.OnConflictStrategy.Companion.IGNORE}) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface OnConflictStrategy {
diff --git a/room/room-common/src/main/java/androidx/room/MapInfo.kt b/room/room-common/src/main/java/androidx/room/MapInfo.kt
index 4a17d362..526f949 100644
--- a/room/room-common/src/main/java/androidx/room/MapInfo.kt
+++ b/room/room-common/src/main/java/androidx/room/MapInfo.kt
@@ -55,9 +55,35 @@
val keyColumn: String = "",
/**
+ * The name of the table or alias to be used for the map's keys.
+ *
+ * Providing this value is optional. Useful for disambiguating between duplicate column names.
+ * For example, consider the following query:
+ * `SELECT * FROM Artist AS a JOIN Song AS s ON a.id == s.artistId`, then the `@MapInfo`
+ * for a return type `Map<String, List<Song>>` would be
+ * `@MapInfo(keyColumn = "id", keyTable ="a")`.
+ *
+ * @return The key table name.
+ */
+ val keyTable: String = "",
+
+ /**
* The name of the column to be used for the map's values.
*
* @return The value column name.
*/
- val valueColumn: String = ""
+ val valueColumn: String = "",
+
+ /**
+ * The name of the table or alias to be used for the map's values.
+ *
+ * Providing this value is optional. Useful for disambiguating between duplicate column names.
+ * For example, consider the following query:
+ * `SELECT * FROM Song AS s JOIN Artist AS a ON s.artistId == a.id`, then the `@MapInfo`
+ * for a return type `Map<Song, String>` would be
+ * `@MapInfo(valueColumn = "id", valueTable ="a")`.
+ *
+ * @return The key table name.
+ */
+ val valueTable: String = "",
)
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/parser/SqlParser.kt b/room/room-compiler/src/main/kotlin/androidx/room/parser/SqlParser.kt
index 7b9df5e..b76b817 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/parser/SqlParser.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/parser/SqlParser.kt
@@ -341,4 +341,4 @@
enum class FtsVersion {
FTS3,
FTS4;
-}
\ No newline at end of file
+}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/preconditions/Checks.kt b/room/room-compiler/src/main/kotlin/androidx/room/preconditions/Checks.kt
index 39fc569..3d0466c 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/preconditions/Checks.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/preconditions/Checks.kt
@@ -40,6 +40,13 @@
return predicate
}
+ fun check(predicate: Boolean, element: XElement, errorMsgProducer: () -> String): Boolean {
+ if (!predicate) {
+ logger.e(element, errorMsgProducer.invoke())
+ }
+ return predicate
+ }
+
fun hasAnnotation(
element: XElement,
annotation: KClass<out Annotation>,
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
index b8344d5..0df368f 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
@@ -147,25 +147,23 @@
" @Delete but does not have any parameters to delete."
fun cannotMapInfoSpecifiedColumn(column: String, columnsInQuery: List<String>) =
- "Column(s) specified in the provided @MapInfo annotation must be present in the query. " +
- "Provided: $column. Columns Found: ${columnsInQuery.joinToString(", ")}"
+ "Column specified in the provided @MapInfo annotation must be present in the query. " +
+ "Provided: $column. Columns found: ${columnsInQuery.joinToString(", ")}"
val MAP_INFO_MUST_HAVE_AT_LEAST_ONE_COLUMN_PROVIDED = "To use the @MapInfo annotation, you " +
"must provide either the key column name, value column name, or both."
fun keyMayNeedMapInfo(keyArg: TypeName): String {
return """
- Looks like you may need to use @MapInfo to clarify the 'keyColumnName' needed for
- the return type of a method. Type argument that needs
- @MapInfo: $keyArg
+ Looks like you may need to use @MapInfo to clarify the 'keyColumn' needed for
+ the return type of a method. Type argument that needs @MapInfo: $keyArg
""".trim()
}
fun valueMayNeedMapInfo(valueArg: TypeName): String {
return """
- Looks like you may need to use @MapInfo to clarify the 'valueColumnName' needed for
- the return type of a method. Type argument that needs
- @MapInfo: $valueArg
+ Looks like you may need to use @MapInfo to clarify the 'valueColumn' needed for
+ the return type of a method. Type argument that needs @MapInfo: $valueArg
""".trim()
}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/QueryMethodProcessor.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/QueryMethodProcessor.kt
index d9c5a0f..a3f5b03 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/QueryMethodProcessor.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/QueryMethodProcessor.kt
@@ -19,6 +19,7 @@
import androidx.room.Query
import androidx.room.SkipQueryVerification
import androidx.room.Transaction
+import androidx.room.compiler.processing.XAnnotationBox
import androidx.room.compiler.processing.XMethodElement
import androidx.room.compiler.processing.XType
import androidx.room.ext.isNotError
@@ -26,7 +27,9 @@
import androidx.room.parser.QueryType
import androidx.room.parser.SqlParser
import androidx.room.processor.ProcessorErrors.cannotMapInfoSpecifiedColumn
+import androidx.room.solver.TypeAdapterExtras
import androidx.room.solver.query.result.PojoRowAdapter
+import androidx.room.verifier.ColumnInfo
import androidx.room.verifier.DatabaseVerificationErrors
import androidx.room.verifier.DatabaseVerifier
import androidx.room.vo.MapInfo
@@ -210,28 +213,7 @@
): QueryMethod {
val resultBinder = delegate.findResultBinder(returnType, query) {
delegate.executableElement.getAnnotation(androidx.room.MapInfo::class)?.let {
- mapInfoAnnotation ->
- // Check if method is annotated with @MapInfo, parse annotation and put information in
- // bag of extras, it will be later used by the TypeAdapterStore
- val resultColumns = query.resultInfo?.columns?.map { it.name } ?: emptyList()
- val keyColumn = mapInfoAnnotation.value.keyColumn.toString()
- val valueColumn = mapInfoAnnotation.value.valueColumn.toString()
- context.checker.check(
- keyColumn.isEmpty() || resultColumns.contains(keyColumn),
- delegate.executableElement,
- cannotMapInfoSpecifiedColumn(keyColumn, resultColumns)
- )
- context.checker.check(
- valueColumn.isEmpty() || resultColumns.contains(valueColumn),
- delegate.executableElement,
- cannotMapInfoSpecifiedColumn(valueColumn, resultColumns)
- )
- context.checker.check(
- keyColumn.isNotEmpty() || valueColumn.isNotEmpty(),
- executableElement,
- ProcessorErrors.MAP_INFO_MUST_HAVE_AT_LEAST_ONE_COLUMN_PROVIDED
- )
- putData(MapInfo::class, MapInfo(keyColumn, valueColumn))
+ processMapInfo(it, query, delegate.executableElement, this)
}
}
context.checker.check(
@@ -292,6 +274,67 @@
)
}
+ /**
+ * Parse @MapInfo annotation, validate its inputs and put information in the bag of extras,
+ * it will be later used by the TypeAdapterStore.
+ */
+ private fun processMapInfo(
+ mapInfoAnnotation: XAnnotationBox<androidx.room.MapInfo>,
+ query: ParsedQuery,
+ queryExecutableElement: XMethodElement,
+ adapterExtras: TypeAdapterExtras,
+ ) {
+ val keyColumn = mapInfoAnnotation.value.keyColumn
+ val keyTable = mapInfoAnnotation.value.keyTable.ifEmpty { null }
+ val valueColumn = mapInfoAnnotation.value.valueColumn
+ val valueTable = mapInfoAnnotation.value.valueTable.ifEmpty { null }
+
+ val resultTableAliases = query.tables.associate { it.name to it.alias }
+ // Checks if this list of columns contains one with matching name and origin table.
+ // Takes into account that projection tables names might be aliased but originTable uses
+ // sqlite3_column_origin_name which is un-aliased.
+ fun List<ColumnInfo>.contains(
+ columnName: String,
+ tableName: String?
+ ) = any { resultColumn ->
+ val resultTableAlias = resultColumn.originTable?.let { resultTableAliases[it] ?: it }
+ resultColumn.name == columnName && (
+ if (tableName != null) {
+ resultTableAlias == tableName || resultColumn.originTable == tableName
+ } else true)
+ }
+
+ context.checker.check(
+ keyColumn.isNotEmpty() || valueColumn.isNotEmpty(),
+ queryExecutableElement,
+ ProcessorErrors.MAP_INFO_MUST_HAVE_AT_LEAST_ONE_COLUMN_PROVIDED
+ )
+
+ val resultColumns = query.resultInfo?.columns
+ if (resultColumns != null) {
+ context.checker.check(
+ keyColumn.isEmpty() || resultColumns.contains(keyColumn, keyTable),
+ queryExecutableElement
+ ) {
+ cannotMapInfoSpecifiedColumn(
+ (if (keyTable != null) "$keyTable." else "") + keyColumn,
+ resultColumns.map { it.name }
+ )
+ }
+ context.checker.check(
+ valueColumn.isEmpty() || resultColumns.contains(valueColumn, valueTable),
+ queryExecutableElement
+ ) {
+ cannotMapInfoSpecifiedColumn(
+ (if (valueTable != null) "$valueTable." else "") + valueColumn,
+ resultColumns.map { it.name }
+ )
+ }
+ }
+
+ adapterExtras.putData(MapInfo::class, MapInfo(keyColumn, valueColumn))
+ }
+
companion object {
val PREPARED_TYPES = arrayOf(QueryType.INSERT, QueryType.DELETE, QueryType.UPDATE)
}
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
index 35285cf..19c43eb 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
@@ -1467,6 +1467,68 @@
}
@Test
+ fun testUseMapInfoWithTableAndColumnName() {
+ if (!enableVerification) {
+ return
+ }
+ singleQueryMethod<ReadQueryMethod>(
+ """
+ @SuppressWarnings(
+ {RoomWarnings.CURSOR_MISMATCH, RoomWarnings.AMBIGUOUS_COLUMN_IN_RESULT}
+ )
+ @MapInfo(keyColumn = "uid", keyTable = "u")
+ @Query("SELECT * FROM User u JOIN Book b ON u.uid == b.uid")
+ abstract Map<Integer, Book> getMultimap();
+ """
+ ) { _, invocation ->
+ invocation.assertCompilationResult {
+ hasNoWarnings()
+ }
+ }
+ }
+
+ @Test
+ fun testUseMapInfoWithOriginalTableAndColumnName() {
+ if (!enableVerification) {
+ return
+ }
+ singleQueryMethod<ReadQueryMethod>(
+ """
+ @SuppressWarnings(
+ {RoomWarnings.CURSOR_MISMATCH, RoomWarnings.AMBIGUOUS_COLUMN_IN_RESULT}
+ )
+ @MapInfo(keyColumn = "uid", keyTable = "User")
+ @Query("SELECT * FROM User u JOIN Book b ON u.uid == b.uid")
+ abstract Map<Integer, Book> getMultimap();
+ """
+ ) { _, invocation ->
+ invocation.assertCompilationResult {
+ hasNoWarnings()
+ }
+ }
+ }
+
+ @Test
+ fun testUseMapInfoWithColumnAlias() {
+ if (!enableVerification) {
+ return
+ }
+ singleQueryMethod<ReadQueryMethod>(
+ """
+ @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
+ @MapInfo(keyColumn = "name", valueColumn = "bookCount")
+ @Query("SELECT name, (SELECT count(*) FROM User u JOIN Book b ON u.uid == b.uid) "
+ + "AS bookCount FROM User")
+ abstract Map<String, Integer> getMultimap();
+ """
+ ) { _, invocation ->
+ invocation.assertCompilationResult {
+ hasNoWarnings()
+ }
+ }
+ }
+
+ @Test
fun testDoesNotImplementEqualsAndHashcodeQuery() {
singleQueryMethod<ReadQueryMethod>(
"""
@@ -1671,11 +1733,11 @@
)
hasErrorCount(2)
hasErrorContaining(
- "Column(s) specified in the provided @MapInfo annotation must " +
+ "Column specified in the provided @MapInfo annotation must " +
"be present in the query. Provided: cat."
)
hasErrorContaining(
- "Column(s) specified in the provided @MapInfo annotation must " +
+ "Column specified in the provided @MapInfo annotation must " +
"be present in the query. Provided: dog."
)
}