Merge "Adding support for nested map return types in DAO functions of Room." into androidx-main
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/MusicDao.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/MusicDao.kt
index a881249..4a8fea8 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/MusicDao.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/MusicDao.kt
@@ -298,4 +298,61 @@
@MapInfo(keyColumn = "mImageYear")
@RewriteQueriesToDropUnusedColumns
fun allAlbumCoverYearToArtistsWithIntSparseArray(): SparseArrayCompat<Artist>
+
+ @Query(
+ """
+ SELECT * FROM Artist
+ JOIN Album ON (Artist.mArtistName = Album.mAlbumArtist)
+ JOIN Song ON (Album.mAlbumName = Song.mAlbum)
+ """
+ )
+ @RewriteQueriesToDropUnusedColumns
+ fun getArtistToAlbumsMappedToSongs(): Map<Artist, Map<Album, List<Song>>>
+
+ @Query(
+ """
+ SELECT * FROM Image
+ JOIN Artist ON Image.mArtistInImage = Artist.mArtistName
+ JOIN Album ON Artist.mArtistName = Album.mAlbumArtist
+ JOIN Song ON Album.mAlbumName = Song.mAlbum
+ """
+ )
+ @RewriteQueriesToDropUnusedColumns
+ fun getImageToArtistToAlbumsMappedToSongs():
+ Map<Image, Map<Artist, Map<Album, List<Song>>>>
+
+ @Query(
+ """
+ SELECT * FROM Artist
+ LEFT JOIN Album ON (Artist.mArtistName = Album.mAlbumArtist)
+ LEFT JOIN Song ON (Album.mAlbumName = Song.mAlbum)
+ """
+ )
+ @MapInfo(valueColumn = "mTitle")
+ @RewriteQueriesToDropUnusedColumns
+ fun getArtistToAlbumsMappedToSongNamesMapInfoLeftJoin(): Map<Artist, Map<Album, String>>
+
+ @Query(
+ """
+ SELECT * FROM Image
+ LEFT JOIN Artist ON Image.mArtistInImage = Artist.mArtistName
+ LEFT JOIN Album ON Artist.mArtistName = Album.mAlbumArtist
+ LEFT JOIN Song ON Album.mAlbumName = Song.mAlbum
+ """
+ )
+ @MapInfo(keyColumn = "mImageYear")
+ @RewriteQueriesToDropUnusedColumns
+ fun getImageYearToArtistToAlbumsMappedToSongs(): Map<Long, Map<Artist, Map<Album, List<Song>>>>
+
+ @Query(
+ """
+ SELECT * FROM Image
+ LEFT JOIN Artist ON Image.mArtistInImage = Artist.mArtistName
+ LEFT JOIN Album ON Artist.mArtistName = Album.mAlbumArtist
+ LEFT JOIN Song ON Album.mAlbumName = Song.mAlbum
+ """
+ )
+ @MapInfo(keyColumn = "mImageYear", valueColumn = "mTitle")
+ @RewriteQueriesToDropUnusedColumns
+ fun getNestedMapWithMapInfoKeyAndValue(): Map<Long, Map<Artist, Map<Album, List<String>>>>
}
\ No newline at end of file
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/AmbiguousColumnResolverTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/AmbiguousColumnResolverTest.kt
index d54e191..93a76b0 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/AmbiguousColumnResolverTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/AmbiguousColumnResolverTest.kt
@@ -228,6 +228,12 @@
@Query("SELECT * FROM User LEFT JOIN Comment ON User.id = Comment.userId")
fun getLeftJoinUserCommentMap(): Map<User, List<Comment>>
+ @Query(
+ "SELECT * FROM User JOIN Avatar ON User.id = Avatar.userId JOIN " +
+ "Comment ON Avatar.userId = Comment.userId"
+ )
+ fun getLeftJoinUserNestedMap(): Map<User, Map<Avatar, List<Comment>>>
+
@Transaction
@Query("SELECT * FROM User JOIN Comment ON User.id = Comment.userId")
fun getUserAndAvatarCommentMap(): Map<UserAndAvatar, List<Comment>>
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/MultimapQueryTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/MultimapQueryTest.kt
index 5358f09..75797da 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/MultimapQueryTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/MultimapQueryTest.kt
@@ -96,6 +96,14 @@
443,
1973
)
+ private val mRhcpSong3: Song = Song(
+ 5,
+ "Parallel Universe",
+ "Red Hot Chili Peppers",
+ "Californication",
+ 529,
+ 1999
+ )
private val mRhcp: Artist = Artist(
1,
"Red Hot Chili Peppers",
@@ -173,6 +181,15 @@
ImageFormat.MPEG
)
+ private val mTheClashAlbumCover: Image = Image(
+ 3,
+ 1979L,
+ "The Clash",
+ "london_calling_image".toByteArray(),
+ Date(11873445200000L),
+ ImageFormat.MPEG
+ )
+
@JvmField
@Rule
var mExecutorRule = CountingTaskExecutorRule()
@@ -1132,6 +1149,191 @@
assertThat(artistNameToImagesMap[mRhcp]).isEqualTo(2006L)
}
+ @Test
+ fun testSingleNestedMap() {
+ mMusicDao.addArtists(mRhcp, mAcDc, mTheClash, mPinkFloyd)
+ mMusicDao.addAlbums(
+ mStadiumArcadium,
+ mCalifornication,
+ mTheDarkSideOfTheMoon,
+ mHighwayToHell,
+ mDreamland
+ )
+ mMusicDao.addSongs(mRhcpSong1, mRhcpSong2, mAcdcSong1, mPinkFloydSong1, mRhcpSong3)
+
+ val singleNestedMap = mMusicDao.getArtistToAlbumsMappedToSongs()
+ val rhcpMap = singleNestedMap.getValue(mRhcp)
+ val stadiumArcadiumList = rhcpMap.getValue(mStadiumArcadium)
+ val californicationList = rhcpMap.getValue(mCalifornication)
+
+ val stadiumArcadiumExpectedList = listOf(mRhcpSong1, mRhcpSong2)
+ val californicationExpectedList = listOf(mRhcpSong3)
+
+ assertThat(rhcpMap.keys).containsExactlyElementsIn(
+ listOf(mCalifornication, mStadiumArcadium)
+ )
+ assertThat(stadiumArcadiumList).containsExactlyElementsIn(stadiumArcadiumExpectedList)
+ assertThat(californicationList).containsExactlyElementsIn(californicationExpectedList)
+ }
+
+ @Test
+ fun testDoubleNestedMap() {
+ mMusicDao.addArtists(mRhcp, mAcDc, mTheClash, mPinkFloyd)
+ mMusicDao.addAlbums(
+ mStadiumArcadium,
+ mCalifornication,
+ mTheDarkSideOfTheMoon,
+ mHighwayToHell,
+ mDreamland
+ )
+ mMusicDao.addSongs(mRhcpSong1, mRhcpSong2, mAcdcSong1, mPinkFloydSong1, mRhcpSong3)
+ mMusicDao.addImages(mPinkFloydAlbumCover, mRhcpAlbumCover)
+
+ val doubleNestedMap = mMusicDao.getImageToArtistToAlbumsMappedToSongs()
+ val rhcpImageMap = doubleNestedMap.getValue(mRhcpAlbumCover)
+ val rhcpMap = rhcpImageMap.getValue(mRhcp)
+ val stadiumArcadiumList = rhcpMap.getValue(mStadiumArcadium)
+ val californicationList = rhcpMap.getValue(mCalifornication)
+
+ val stadiumArcadiumExpectedList = listOf(mRhcpSong1, mRhcpSong2)
+ val californicationExpectedList = listOf(mRhcpSong3)
+
+ assertThat(doubleNestedMap.keys).containsExactlyElementsIn(
+ listOf(mPinkFloydAlbumCover, mRhcpAlbumCover)
+ )
+ assertThat(rhcpImageMap.keys).containsExactly(mRhcp)
+ assertThat(rhcpMap.keys).containsExactlyElementsIn(
+ listOf(mCalifornication, mStadiumArcadium)
+ )
+ assertThat(stadiumArcadiumList).containsExactlyElementsIn(stadiumArcadiumExpectedList)
+ assertThat(californicationList).containsExactlyElementsIn(californicationExpectedList)
+ }
+
+ @Test
+ fun testSingleNestedMapWithMapInfoLeftJoin() {
+ mMusicDao.addArtists(mRhcp, mAcDc, mTheClash, mPinkFloyd)
+ mMusicDao.addAlbums(
+ mStadiumArcadium,
+ mCalifornication,
+ mTheDarkSideOfTheMoon,
+ mHighwayToHell,
+ mDreamland
+ )
+ mMusicDao.addSongs(mRhcpSong1, mAcdcSong1, mPinkFloydSong1, mRhcpSong3)
+
+ val singleNestedMap = mMusicDao.getArtistToAlbumsMappedToSongNamesMapInfoLeftJoin()
+ val rhcpMap = singleNestedMap.getValue(mRhcp)
+
+ assertThat(rhcpMap.keys).containsExactlyElementsIn(
+ listOf(mCalifornication, mStadiumArcadium)
+ )
+ assertThat(rhcpMap[mStadiumArcadium]).isEqualTo(mRhcpSong1.mTitle)
+ assertThat(rhcpMap[mCalifornication]).isEqualTo(mRhcpSong3.mTitle)
+
+ // LEFT JOIN Checks
+ assertThat(singleNestedMap[mTheClash]).isEmpty()
+ }
+
+ @Test
+ fun testDoubleNestedMapWithMapInfoKeyLeftJoin() {
+ mMusicDao.addArtists(mRhcp, mAcDc, mPinkFloyd)
+ mMusicDao.addAlbums(
+ mStadiumArcadium,
+ mCalifornication,
+ mTheDarkSideOfTheMoon,
+ mHighwayToHell,
+ mDreamland
+ )
+ mMusicDao.addSongs(mRhcpSong1, mRhcpSong2, mAcdcSong1, mRhcpSong3)
+ mMusicDao.addImages(mPinkFloydAlbumCover, mRhcpAlbumCover, mTheClashAlbumCover)
+
+ val doubleNestedMap = mMusicDao.getImageYearToArtistToAlbumsMappedToSongs()
+ val rhcpImageMap = doubleNestedMap.getValue(mRhcpAlbumCover.mImageYear)
+ val rhcpMap = rhcpImageMap.getValue(mRhcp)
+ val stadiumArcadiumList = rhcpMap.getValue(mStadiumArcadium)
+ val californicationList = rhcpMap.getValue(mCalifornication)
+
+ val stadiumArcadiumExpectedList = listOf(mRhcpSong1, mRhcpSong2)
+ val californicationExpectedList = listOf(mRhcpSong3)
+
+ assertThat(doubleNestedMap.keys).containsExactlyElementsIn(
+ listOf(
+ mPinkFloydAlbumCover.mImageYear,
+ mRhcpAlbumCover.mImageYear,
+ mTheClashAlbumCover.mImageYear
+ )
+ )
+ assertThat(rhcpImageMap.keys).containsExactly(mRhcp)
+ assertThat(rhcpMap.keys).containsExactlyElementsIn(
+ listOf(mCalifornication, mStadiumArcadium)
+ )
+ assertThat(stadiumArcadiumList).containsExactlyElementsIn(stadiumArcadiumExpectedList)
+ assertThat(californicationList).containsExactlyElementsIn(californicationExpectedList)
+
+ // LEFT JOIN Checks
+ assertThat(doubleNestedMap).containsKey(mTheClashAlbumCover.mImageYear)
+ assertThat(doubleNestedMap[mTheClashAlbumCover.mImageYear]).isEmpty()
+ assertThat(doubleNestedMap).containsKey(mPinkFloydAlbumCover.mImageYear)
+ assertThat(doubleNestedMap[mPinkFloydAlbumCover.mImageYear]).containsKey(mPinkFloyd)
+ assertThat(doubleNestedMap[mPinkFloydAlbumCover.mImageYear]!![mPinkFloyd])
+ .containsKey(mTheDarkSideOfTheMoon)
+ assertThat(
+ doubleNestedMap[mPinkFloydAlbumCover.mImageYear]
+ !![mPinkFloyd]
+ !![mTheDarkSideOfTheMoon]
+ ).isEmpty()
+ }
+
+ @Test
+ fun testNestedMapWithMapInfoKeyAndValue() {
+ mMusicDao.addArtists(mRhcp, mAcDc, mPinkFloyd)
+ mMusicDao.addAlbums(
+ mStadiumArcadium,
+ mCalifornication,
+ mTheDarkSideOfTheMoon,
+ mHighwayToHell,
+ mDreamland
+ )
+ mMusicDao.addSongs(mRhcpSong1, mRhcpSong2, mAcdcSong1, mRhcpSong3)
+ mMusicDao.addImages(mPinkFloydAlbumCover, mRhcpAlbumCover, mTheClashAlbumCover)
+
+ val doubleNestedMap = mMusicDao.getNestedMapWithMapInfoKeyAndValue()
+ val rhcpImageMap = doubleNestedMap.getValue(mRhcpAlbumCover.mImageYear)
+ val rhcpMap = rhcpImageMap.getValue(mRhcp)
+ val stadiumArcadiumList = rhcpMap.getValue(mStadiumArcadium)
+ val californicationList = rhcpMap.getValue(mCalifornication)
+
+ val stadiumArcadiumExpectedList = listOf(mRhcpSong1.mTitle, mRhcpSong2.mTitle)
+ val californicationExpectedList = listOf(mRhcpSong3.mTitle)
+
+ assertThat(doubleNestedMap.keys).containsExactlyElementsIn(
+ listOf(
+ mPinkFloydAlbumCover.mImageYear,
+ mRhcpAlbumCover.mImageYear,
+ mTheClashAlbumCover.mImageYear
+ )
+ )
+ assertThat(rhcpImageMap.keys).containsExactly(mRhcp)
+ assertThat(rhcpMap.keys).containsExactlyElementsIn(
+ listOf(mCalifornication, mStadiumArcadium)
+ )
+ assertThat(stadiumArcadiumList).containsExactlyElementsIn(stadiumArcadiumExpectedList)
+ assertThat(californicationList).containsExactlyElementsIn(californicationExpectedList)
+
+ // LEFT JOIN Checks
+ assertThat(doubleNestedMap).containsKey(mTheClashAlbumCover.mImageYear)
+ assertThat(doubleNestedMap[mTheClashAlbumCover.mImageYear]).isEmpty()
+ assertThat(doubleNestedMap).containsKey(mPinkFloydAlbumCover.mImageYear)
+ assertThat(doubleNestedMap[mPinkFloydAlbumCover.mImageYear]).containsKey(mPinkFloyd)
+ assertThat(doubleNestedMap[mPinkFloydAlbumCover.mImageYear]!![mPinkFloyd])
+ .containsKey(mTheDarkSideOfTheMoon)
+ assertThat(
+ doubleNestedMap[mPinkFloydAlbumCover.mImageYear]
+ !![mPinkFloyd]
+ !![mTheDarkSideOfTheMoon]
+ ).isEmpty()
+ }
+
/**
* Checks that the contents of the map are as expected.
*
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/Image.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/Image.kt
index d6493a4..d4cf95c 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/Image.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/Image.kt
@@ -21,7 +21,7 @@
@Entity
class Image(
- @field:PrimaryKey val mImageId: Int,
+ @PrimaryKey val mImageId: Int,
val mImageYear: Long,
val mArtistInImage: String,
val mAlbumCover: ByteArray,
@@ -33,4 +33,19 @@
init {
mFormat = format
}
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || javaClass != other.javaClass) return false
+ val image = other as Image
+ if (mImageId != image.mImageId) return false
+ return mArtistInImage == image.mArtistInImage
+ }
+
+ override fun hashCode(): Int {
+ var result = mImageId
+ result = (31 * result + mImageYear).toInt()
+ result = 31 * result + mArtistInImage.hashCode()
+ return result
+ }
}
\ No newline at end of file
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 1612b0c..bb1e0bd 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
@@ -271,8 +271,9 @@
return MISSING_PARAMETER_FOR_BIND.format(bindVarName.joinToString(", "))
}
- fun valueCollectionMustBeListOrSet(mapValueTypeName: String): String {
- return "Multimap 'value' collection type must be a List or Set. Found $mapValueTypeName."
+ fun valueCollectionMustBeListOrSetOrMap(mapValueTypeName: String): String {
+ return "Multimap 'value' collection type must be a List, Set or Map. " +
+ "Found $mapValueTypeName."
}
private val UNUSED_QUERY_METHOD_PARAMETER = "Unused parameter%s: %s"
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
index 39bc50e..9007f8e 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
@@ -38,7 +38,6 @@
import androidx.room.ext.isUUID
import androidx.room.parser.ParsedQuery
import androidx.room.parser.SQLTypeAffinity
-import androidx.room.preconditions.checkTypeOrNull
import androidx.room.processor.Context
import androidx.room.processor.EntityProcessor
import androidx.room.processor.FieldProcessor
@@ -76,8 +75,10 @@
import androidx.room.solver.query.result.ImmutableMapQueryResultAdapter
import androidx.room.solver.query.result.ListQueryResultAdapter
import androidx.room.solver.query.result.MapQueryResultAdapter
+import androidx.room.solver.query.result.MapValueResultAdapter
import androidx.room.solver.query.result.MultimapQueryResultAdapter
-import androidx.room.solver.query.result.MultimapQueryResultAdapter.Companion.validateMapTypeArgs
+import androidx.room.solver.query.result.MultimapQueryResultAdapter.Companion.validateMapKeyTypeArg
+import androidx.room.solver.query.result.MultimapQueryResultAdapter.Companion.validateMapValueTypeArg
import androidx.room.solver.query.result.MultimapQueryResultAdapter.MapType.Companion.isSparseArray
import androidx.room.solver.query.result.OptionalQueryResultAdapter
import androidx.room.solver.query.result.PojoRowAdapter
@@ -635,11 +636,15 @@
columnName = mapInfo?.valueColumnName
) ?: return null
- validateMapTypeArgs(
+ validateMapKeyTypeArg(
context = context,
keyTypeArg = keyTypeArg,
- valueTypeArg = valueTypeArg,
keyReader = findCursorValueReader(keyTypeArg, null),
+ mapInfo = mapInfo
+ )
+ validateMapValueTypeArg(
+ context = context,
+ valueTypeArg = valueTypeArg,
valueReader = findCursorValueReader(valueTypeArg, null),
mapInfo = mapInfo
)
@@ -648,8 +653,8 @@
parsedQuery = query,
keyTypeArg = keyTypeArg,
valueTypeArg = valueTypeArg,
- keyRowAdapter = checkTypeOrNull(keyRowAdapter) ?: return null,
- valueRowAdapter = checkTypeOrNull(valueRowAdapter) ?: return null,
+ keyRowAdapter = keyRowAdapter,
+ valueRowAdapter = valueRowAdapter,
immutableClassName = immutableClassName
)
} else if (typeMirror.isTypeOf(java.util.Map::class) ||
@@ -686,94 +691,38 @@
)
return null
}
- // TODO: Handle nested collection values in the map
// Get @MapInfo info if any (this might be null)
val mapInfo = extras.getData(MapInfo::class)
- val collectionTypeRaw = context.COMMON_TYPES.READONLY_COLLECTION.rawType
- if (collectionTypeRaw.isAssignableFrom(mapValueTypeArg.rawType)) {
- // The Map's value type argument is assignable to a Collection, we need to make
- // sure it is either a list or a set.
- val listTypeRaw = context.COMMON_TYPES.LIST.rawType
- val setTypeRaw = context.COMMON_TYPES.SET.rawType
- val collectionValueType = when {
- mapValueTypeArg.rawType.isAssignableFrom(listTypeRaw) ->
- MultimapQueryResultAdapter.CollectionValueType.LIST
- mapValueTypeArg.rawType.isAssignableFrom(setTypeRaw) ->
- MultimapQueryResultAdapter.CollectionValueType.SET
- else -> {
- context.logger.e(
- ProcessorErrors.valueCollectionMustBeListOrSet(
- mapValueTypeArg.asTypeName().toString(context.codeLanguage)
- )
- )
- return null
- }
- }
- val valueTypeArg = mapValueTypeArg.typeArguments.single().extendsBoundOrSelf()
+ val keyRowAdapter = findRowAdapter(
+ typeMirror = keyTypeArg,
+ query = query,
+ columnName = mapInfo?.keyColumnName
+ ) ?: return null
- val keyRowAdapter = findRowAdapter(
- typeMirror = keyTypeArg,
- query = query,
- columnName = mapInfo?.keyColumnName
- ) ?: return null
+ validateMapKeyTypeArg(
+ context = context,
+ keyTypeArg = keyTypeArg,
+ keyReader = findCursorValueReader(keyTypeArg, null),
+ mapInfo = mapInfo
+ )
- val valueRowAdapter = findRowAdapter(
- typeMirror = valueTypeArg,
- query = query,
- columnName = mapInfo?.valueColumnName
- ) ?: return null
-
- validateMapTypeArgs(
- context = context,
+ val mapValueResultAdapter = findMapValueResultAdapter(
+ query = query,
+ mapInfo = mapInfo,
+ mapValueTypeArg = mapValueTypeArg
+ ) ?: return null
+ return MapQueryResultAdapter(
+ context = context,
+ parsedQuery = query,
+ mapValueResultAdapter = MapValueResultAdapter.NestedMapValueResultAdapter(
+ keyRowAdapter = keyRowAdapter,
keyTypeArg = keyTypeArg,
- valueTypeArg = valueTypeArg,
- keyReader = findCursorValueReader(keyTypeArg, null),
- valueReader = findCursorValueReader(valueTypeArg, null),
- mapInfo = mapInfo
+ mapType = mapType,
+ mapValueResultAdapter = mapValueResultAdapter
)
- return MapQueryResultAdapter(
- context = context,
- parsedQuery = query,
- keyTypeArg = keyTypeArg,
- valueTypeArg = valueTypeArg,
- keyRowAdapter = checkTypeOrNull(keyRowAdapter) ?: return null,
- valueRowAdapter = checkTypeOrNull(valueRowAdapter) ?: return null,
- valueCollectionType = collectionValueType,
- mapType = mapType
- )
- } else {
- val keyRowAdapter = findRowAdapter(
- typeMirror = keyTypeArg,
- query = query,
- columnName = mapInfo?.keyColumnName
- ) ?: return null
- val valueRowAdapter = findRowAdapter(
- typeMirror = mapValueTypeArg,
- query = query,
- columnName = mapInfo?.valueColumnName
- ) ?: return null
-
- validateMapTypeArgs(
- context = context,
- keyTypeArg = keyTypeArg,
- valueTypeArg = mapValueTypeArg,
- keyReader = findCursorValueReader(keyTypeArg, null),
- valueReader = findCursorValueReader(mapValueTypeArg, null),
- mapInfo = mapInfo
- )
- return MapQueryResultAdapter(
- context = context,
- parsedQuery = query,
- keyTypeArg = keyTypeArg,
- valueTypeArg = mapValueTypeArg,
- keyRowAdapter = checkTypeOrNull(keyRowAdapter) ?: return null,
- valueRowAdapter = checkTypeOrNull(valueRowAdapter) ?: return null,
- valueCollectionType = null,
- mapType = mapType
- )
- }
+ )
}
return null
}
@@ -825,6 +774,96 @@
}
}
+ private fun findMapValueResultAdapter(
+ query: ParsedQuery,
+ mapInfo: MapInfo?,
+ mapValueTypeArg: XType
+ ): MapValueResultAdapter? {
+ val collectionTypeRaw = context.COMMON_TYPES.READONLY_COLLECTION.rawType
+ if (collectionTypeRaw.isAssignableFrom(mapValueTypeArg.rawType)) {
+ // The Map's value type argument is assignable to a Collection, we need to make
+ // sure it is either a list or a set.
+ val listTypeRaw = context.COMMON_TYPES.LIST.rawType
+ val setTypeRaw = context.COMMON_TYPES.SET.rawType
+ val collectionValueType = when {
+ mapValueTypeArg.rawType.isAssignableFrom(listTypeRaw) ->
+ MultimapQueryResultAdapter.CollectionValueType.LIST
+ mapValueTypeArg.rawType.isAssignableFrom(setTypeRaw) ->
+ MultimapQueryResultAdapter.CollectionValueType.SET
+ else -> {
+ context.logger.e(
+ ProcessorErrors.valueCollectionMustBeListOrSetOrMap(
+ mapValueTypeArg.asTypeName().toString(context.codeLanguage)
+ )
+ )
+ return null
+ }
+ }
+
+ val valueTypeArg = mapValueTypeArg.typeArguments.single().extendsBoundOrSelf()
+ val valueRowAdapter = findRowAdapter(
+ typeMirror = valueTypeArg,
+ query = query,
+ columnName = mapInfo?.valueColumnName
+ ) ?: return null
+
+ validateMapValueTypeArg(
+ context = context,
+ valueTypeArg = valueTypeArg,
+ valueReader = findCursorValueReader(valueTypeArg, null),
+ mapInfo = mapInfo
+ )
+
+ return MapValueResultAdapter.EndMapValueResultAdapter(
+ valueRowAdapter = valueRowAdapter,
+ valueTypeArg = valueTypeArg,
+ valueCollectionType = collectionValueType
+ )
+ } else if (mapValueTypeArg.isTypeOf(java.util.Map::class)) {
+ val keyTypeArg = mapValueTypeArg.typeArguments[0].extendsBoundOrSelf()
+ validateMapKeyTypeArg(
+ context = context,
+ keyTypeArg = keyTypeArg,
+ keyReader = findCursorValueReader(keyTypeArg, null),
+ mapInfo = mapInfo
+ )
+
+ val keyRowAdapter = findRowAdapter(
+ typeMirror = keyTypeArg,
+ query = query,
+ columnName = null
+ ) ?: return null
+ val valueTypeArg = mapValueTypeArg.typeArguments[1].extendsBoundOrSelf()
+ val valueMapAdapter = findMapValueResultAdapter(
+ query, mapInfo, valueTypeArg
+ ) ?: return null
+ return MapValueResultAdapter.NestedMapValueResultAdapter(
+ keyRowAdapter = keyRowAdapter,
+ keyTypeArg = keyTypeArg,
+ mapType = MultimapQueryResultAdapter.MapType.DEFAULT,
+ mapValueResultAdapter = valueMapAdapter
+ )
+ } else {
+ val valueRowAdapter = findRowAdapter(
+ typeMirror = mapValueTypeArg,
+ query = query,
+ columnName = mapInfo?.valueColumnName
+ ) ?: return null
+
+ validateMapValueTypeArg(
+ context = context,
+ valueTypeArg = mapValueTypeArg,
+ valueReader = findCursorValueReader(mapValueTypeArg, null),
+ mapInfo = mapInfo
+ )
+ return MapValueResultAdapter.EndMapValueResultAdapter(
+ valueRowAdapter = valueRowAdapter,
+ valueTypeArg = mapValueTypeArg,
+ valueCollectionType = null
+ )
+ }
+ }
+
/**
* 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.
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaImmutableMultimapQueryResultAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaImmutableMultimapQueryResultAdapter.kt
index d57eb7d..e517dc6 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaImmutableMultimapQueryResultAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaImmutableMultimapQueryResultAdapter.kt
@@ -27,10 +27,10 @@
class GuavaImmutableMultimapQueryResultAdapter(
context: Context,
private val parsedQuery: ParsedQuery,
- override val keyTypeArg: XType,
- override val valueTypeArg: XType,
- private val keyRowAdapter: QueryMappedRowAdapter,
- private val valueRowAdapter: QueryMappedRowAdapter,
+ private val keyTypeArg: XType,
+ private val valueTypeArg: XType,
+ private val keyRowAdapter: RowAdapter,
+ private val valueRowAdapter: RowAdapter,
private val immutableClassName: XClassName,
) : MultimapQueryResultAdapter(context, parsedQuery, listOf(keyRowAdapter, valueRowAdapter)) {
private val mapType = immutableClassName.parametrizedBy(
@@ -103,6 +103,7 @@
// Iterate over all matched fields to check if all are null. If so, we continue in
// the while loop to the next iteration.
+ check(valueRowAdapter is QueryMappedRowAdapter)
val valueIndexVars =
dupeColumnsIndexAdapter?.getIndexVarsForMapping(valueRowAdapter.mapping)
?: valueRowAdapter.getDefaultIndexAdapter().getIndexVars()
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/ImmutableMapQueryResultAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/ImmutableMapQueryResultAdapter.kt
index 3f99950..dd58f92 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/ImmutableMapQueryResultAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/ImmutableMapQueryResultAdapter.kt
@@ -26,8 +26,8 @@
class ImmutableMapQueryResultAdapter(
context: Context,
parsedQuery: ParsedQuery,
- override val keyTypeArg: XType,
- override val valueTypeArg: XType,
+ private val keyTypeArg: XType,
+ private val valueTypeArg: XType,
private val resultAdapter: QueryResultAdapter
) : MultimapQueryResultAdapter(context, parsedQuery, resultAdapter.rowAdapters) {
override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MapQueryResultAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MapQueryResultAdapter.kt
index 821523c..ee6ee3a 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MapQueryResultAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MapQueryResultAdapter.kt
@@ -16,193 +16,58 @@
package androidx.room.solver.query.result
-import androidx.room.compiler.codegen.CodeLanguage
import androidx.room.compiler.codegen.XCodeBlock
-import androidx.room.compiler.processing.XNullability
-import androidx.room.compiler.processing.XType
-import androidx.room.ext.CommonTypeNames
import androidx.room.parser.ParsedQuery
import androidx.room.processor.Context
import androidx.room.solver.CodeGenScope
-import androidx.room.solver.query.result.MultimapQueryResultAdapter.MapType.Companion.isSparseArray
class MapQueryResultAdapter(
context: Context,
- private val parsedQuery: ParsedQuery,
- override val keyTypeArg: XType,
- override val valueTypeArg: XType,
- private val keyRowAdapter: QueryMappedRowAdapter,
- private val valueRowAdapter: QueryMappedRowAdapter,
- private val valueCollectionType: CollectionValueType?,
- private val mapType: MapType
-) : MultimapQueryResultAdapter(context, parsedQuery, listOf(keyRowAdapter, valueRowAdapter)) {
-
- // The type name of the result map value
- // For Map<Foo, Bar> it is Bar
- // for Map<Foo, List<Bar> it is List<Bar>
- private val valueTypeName = if (valueCollectionType != null) {
- valueCollectionType.className.parametrizedBy(valueTypeArg.asTypeName())
- } else {
- valueTypeArg.asTypeName()
- }
-
- // The type name of the concrete result map value
- // For Map<Foo, Bar> it is Bar
- // For Map<Foo, List<Bar> it is ArrayList<Bar>
- private val implValueTypeName = when (valueCollectionType) {
- CollectionValueType.LIST ->
- CommonTypeNames.ARRAY_LIST.parametrizedBy(valueTypeArg.asTypeName())
- CollectionValueType.SET ->
- CommonTypeNames.HASH_SET.parametrizedBy(valueTypeArg.asTypeName())
- else ->
- valueTypeArg.asTypeName()
- }
-
- // The type name of the result map
- private val mapTypeName = when (mapType) {
- MapType.DEFAULT, MapType.ARRAY_MAP ->
- mapType.className.parametrizedBy(keyTypeArg.asTypeName(), valueTypeName)
- MapType.LONG_SPARSE, MapType.INT_SPARSE ->
- mapType.className.parametrizedBy(valueTypeName)
- }
-
- // The type name of the concrete result map
- private val implMapTypeName = when (mapType) {
- MapType.DEFAULT ->
- // LinkedHashMap is used as impl to preserve key ordering for ordered query results.
- CommonTypeNames.LINKED_HASH_MAP.parametrizedBy(
- keyTypeArg.asTypeName(), valueTypeName
- )
- MapType.ARRAY_MAP ->
- mapType.className.parametrizedBy(keyTypeArg.asTypeName(), valueTypeName)
- MapType.LONG_SPARSE, MapType.INT_SPARSE ->
- mapType.className.parametrizedBy(valueTypeName)
- }
+ parsedQuery: ParsedQuery,
+ private val mapValueResultAdapter: MapValueResultAdapter.NestedMapValueResultAdapter,
+) : MultimapQueryResultAdapter(context, parsedQuery, mapValueResultAdapter.rowAdapters) {
override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
scope.builder.apply {
- val dupeColumnsIndexAdapter: AmbiguousColumnIndexAdapter?
- if (duplicateColumns.isNotEmpty()) {
- // There are duplicate columns in the result objects, generate code that provides
- // us with the indices resolved and pass it to the adapters so it can retrieve
- // the index of each column used by it.
- dupeColumnsIndexAdapter = AmbiguousColumnIndexAdapter(mappings, parsedQuery)
- dupeColumnsIndexAdapter.onCursorReady(cursorVarName, scope)
- rowAdapters.forEach {
- check(it is QueryMappedRowAdapter)
- val indexVarNames = dupeColumnsIndexAdapter.getIndexVarsForMapping(it.mapping)
- it.onCursorReady(
- indices = indexVarNames,
- cursorVarName = cursorVarName,
- scope = scope
- )
- }
- } else {
- dupeColumnsIndexAdapter = null
- rowAdapters.forEach {
- it.onCursorReady(cursorVarName = cursorVarName, scope = scope)
- }
- }
-
+ generateCursorIndexes(cursorVarName, scope)
addLocalVariable(
name = outVarName,
- typeName = mapTypeName,
- assignExpr = XCodeBlock.ofNewInstance(language, implMapTypeName)
- )
-
- val tmpKeyVarName = scope.getTmpVar("_key")
- val tmpValueVarName = scope.getTmpVar("_value")
- beginControlFlow("while (%L.moveToNext())", cursorVarName).apply {
- addLocalVariable(tmpKeyVarName, keyTypeArg.asTypeName())
- keyRowAdapter.convert(tmpKeyVarName, cursorVarName, scope)
-
- val valueIndexVars =
- dupeColumnsIndexAdapter?.getIndexVarsForMapping(valueRowAdapter.mapping)
- ?: valueRowAdapter.getDefaultIndexAdapter().getIndexVars()
- val columnNullCheckCodeBlock = getColumnNullCheckCode(
- language = language,
- cursorVarName = cursorVarName,
- indexVars = valueIndexVars
+ typeName = mapValueResultAdapter.getDeclarationTypeName(),
+ assignExpr = XCodeBlock.ofNewInstance(
+ language,
+ mapValueResultAdapter.getInstantiationTypeName()
)
+ )
+ beginControlFlow("while (%L.moveToNext())", cursorVarName).apply {
+ mapValueResultAdapter.convert(
+ scope,
+ outVarName,
+ cursorVarName,
+ dupeColumnsIndexAdapter,
+ )
+ }.endControlFlow()
+ }
+ }
- // If valueCollectionType is null, this means that we have a 1-to-1 mapping, as
- // opposed to a 1-to-many mapping.
- if (valueCollectionType != null) {
- val tmpCollectionVarName = scope.getTmpVar("_values")
- addLocalVariable(tmpCollectionVarName, valueTypeName)
-
- if (mapType.isSparseArray()) {
- beginControlFlow("if (%L.get(%L) != null)", outVarName, tmpKeyVarName)
- } else {
- beginControlFlow("if (%L.containsKey(%L))", outVarName, tmpKeyVarName)
- }.apply {
- val getFunction = when (language) {
- CodeLanguage.JAVA -> "get"
- CodeLanguage.KOTLIN ->
- if (mapType.isSparseArray()) "get" else "getValue"
- }
- addStatement(
- "%L = %L.%L(%L)",
- tmpCollectionVarName,
- outVarName,
- getFunction,
- tmpKeyVarName
- )
- }.nextControlFlow("else").apply {
- addStatement(
- "%L = %L",
- tmpCollectionVarName,
- XCodeBlock.ofNewInstance(language, implValueTypeName)
- )
- addStatement(
- "%L.put(%L, %L)",
- outVarName,
- tmpKeyVarName,
- tmpCollectionVarName
- )
- }.endControlFlow()
-
- // Perform value columns null check, in a 1-to-many mapping we still add the key
- // with an empty collection as the value entry.
- beginControlFlow("if (%L)", columnNullCheckCodeBlock).apply {
- addStatement("continue")
- }.endControlFlow()
-
- addLocalVariable(tmpValueVarName, valueTypeArg.asTypeName())
- valueRowAdapter.convert(tmpValueVarName, cursorVarName, scope)
- addStatement("%L.add(%L)", tmpCollectionVarName, tmpValueVarName)
- } else {
- // Perform value columns null check, in a 1-to-1 mapping we still add the key
- // with a null value entry if permitted.
- beginControlFlow("if (%L)", columnNullCheckCodeBlock).apply {
- if (
- language == CodeLanguage.KOTLIN &&
- valueTypeArg.nullability == XNullability.NONNULL
- ) {
- // TODO(b/249984504): Generate / output a better message.
- addStatement("error(%S)", "Missing value for a key.")
- } else {
- addStatement("%L.put(%L, null)", outVarName, tmpKeyVarName)
- addStatement("continue")
- }
- }.endControlFlow()
-
- addLocalVariable(tmpValueVarName, valueTypeArg.asTypeName())
- valueRowAdapter.convert(tmpValueVarName, cursorVarName, scope)
-
- // For consistency purposes, in the one-to-one object mapping case, if
- // multiple values are encountered for the same key, we will only consider
- // the first ever encountered mapping.
- if (mapType.isSparseArray()) {
- beginControlFlow("if (%L.get(%L) == null)", outVarName, tmpKeyVarName)
- } else {
- beginControlFlow("if (!%L.containsKey(%L))", outVarName, tmpKeyVarName)
- }.apply {
- addStatement("%L.put(%L, %L)", outVarName, tmpKeyVarName, tmpValueVarName)
- }.endControlFlow()
- }
+ private fun generateCursorIndexes(cursorVarName: String, scope: CodeGenScope) {
+ if (dupeColumnsIndexAdapter != null) {
+ // There are duplicate columns in the result objects, generate code that provides
+ // us with the indices resolved and pass it to the adapters so it can retrieve
+ // the index of each column used by it.
+ dupeColumnsIndexAdapter.onCursorReady(cursorVarName, scope)
+ rowAdapters.forEach {
+ check(it is QueryMappedRowAdapter)
+ val indexVarNames = dupeColumnsIndexAdapter.getIndexVarsForMapping(it.mapping)
+ it.onCursorReady(
+ indices = indexVarNames,
+ cursorVarName = cursorVarName,
+ scope = scope
+ )
}
- endControlFlow()
+ } else {
+ rowAdapters.forEach {
+ it.onCursorReady(cursorVarName = cursorVarName, scope = scope)
+ }
}
}
}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MapValueResultAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MapValueResultAdapter.kt
new file mode 100644
index 0000000..3d77078
--- /dev/null
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MapValueResultAdapter.kt
@@ -0,0 +1,421 @@
+/*
+ * Copyright 2023 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.XCodeBlock
+import androidx.room.compiler.codegen.XTypeName
+import androidx.room.compiler.processing.XNullability
+import androidx.room.compiler.processing.XType
+import androidx.room.ext.CommonTypeNames
+import androidx.room.solver.CodeGenScope
+import androidx.room.solver.query.result.MultimapQueryResultAdapter.MapType.Companion.isSparseArray
+import androidx.room.vo.ColumnIndexVar
+
+/**
+ * This is an intermediary adapter class that enables nested multimap return types in DAOs.
+ *
+ * The [MapValueResultAdapter] sealed class is extended by 2 classes, [NestedMapValueResultAdapter]
+ * and [EndMapValueResultAdapter]. These adapters are wrappers for the adapters at different levels
+ * of nested maps. Each level of nesting of a map is represented by a [NestedMapValueResultAdapter],
+ * except the innermost level which is represented by an [EndMapValueResultAdapter].
+ *
+ * For example, if a DAO method returns a `Map<A, Map<B, Map<C, D>>>`, `Map<C, D>` is represented
+ * by an [EndMapValueResultAdapter], and the outer 2 levels are represented by a
+ * [NestedMapValueResultAdapter] each.
+ *
+ * A [NestedMapValueResultAdapter] can wrap either another [NestedMapValueResultAdapter] or an
+ * [EndMapValueResultAdapter], whereas an [EndMapValueResultAdapter] does not wrap another adapter
+ * and only contains row adapters for the innermost map.
+ */
+sealed class MapValueResultAdapter(
+ val rowAdapters: List<RowAdapter>
+) {
+
+ /**
+ * True if this adapters requires key checking due to its values being passed by reference.
+ */
+ abstract fun requiresContainsKeyCheck(): Boolean
+
+ /**
+ * Left-Hand-Side of a Map value type arg initialization.
+ */
+ abstract fun getDeclarationTypeName(): XTypeName
+
+ /**
+ * Right-Hand-Side of a Map value type arg initialization.
+ */
+ abstract fun getInstantiationTypeName(): XTypeName
+
+ abstract fun convert(
+ scope: CodeGenScope,
+ valuesVarName: String,
+ cursorVarName: String,
+ dupeColumnsIndexAdapter: AmbiguousColumnIndexAdapter?,
+ genPutValueCode: (String, Boolean) -> Unit = { _, _ -> }
+ )
+
+ abstract fun generateContinueColumnCheck(
+ scope: CodeGenScope,
+ cursorVarName: String,
+ dupeColumnsIndexAdapter: AmbiguousColumnIndexAdapter?
+ )
+
+ /**
+ * A [NestedMapValueResultAdapter] contains the key information and the value map information
+ * of any level of a nested map that is not the innermost "End" map.
+ *
+ * The [convert] function implementation for a [NestedMapValueResultAdapter] generates code that
+ * resolves the key of the map and delegates to the value map's [NestedMapValueResultAdapter] or
+ * [EndMapValueResultAdapter] (based on the level of nesting) to resolve the value map
+ * conversion.
+ */
+ class NestedMapValueResultAdapter(
+ private val keyRowAdapter: RowAdapter,
+ private val keyTypeArg: XType,
+ private val mapType: MultimapQueryResultAdapter.MapType,
+ private val mapValueResultAdapter: MapValueResultAdapter
+ ) : MapValueResultAdapter(
+ rowAdapters = listOf(keyRowAdapter) + mapValueResultAdapter.rowAdapters
+ ) {
+
+ private val keyTypeName = keyTypeArg.asTypeName()
+
+ override fun requiresContainsKeyCheck(): Boolean = true
+
+ override fun getDeclarationTypeName() = when (val typeOfMap = this.mapType) {
+ MultimapQueryResultAdapter.MapType.DEFAULT,
+ MultimapQueryResultAdapter.MapType.ARRAY_MAP ->
+ typeOfMap.className.parametrizedBy(
+ keyTypeName,
+ mapValueResultAdapter.getDeclarationTypeName()
+ )
+
+ MultimapQueryResultAdapter.MapType.LONG_SPARSE,
+ MultimapQueryResultAdapter.MapType.INT_SPARSE ->
+ typeOfMap.className.parametrizedBy(
+ mapValueResultAdapter.getDeclarationTypeName()
+ )
+ }
+
+ override fun getInstantiationTypeName() = when (val typeOfMap = this.mapType) {
+ MultimapQueryResultAdapter.MapType.DEFAULT ->
+ // LinkedHashMap is used as impl to preserve key ordering for ordered
+ // query results.
+ CommonTypeNames.LINKED_HASH_MAP.parametrizedBy(
+ keyTypeName,
+ mapValueResultAdapter.getDeclarationTypeName()
+ )
+
+ MultimapQueryResultAdapter.MapType.ARRAY_MAP ->
+ typeOfMap.className.parametrizedBy(
+ keyTypeName,
+ mapValueResultAdapter.getDeclarationTypeName()
+ )
+
+ MultimapQueryResultAdapter.MapType.LONG_SPARSE,
+ MultimapQueryResultAdapter.MapType.INT_SPARSE ->
+ typeOfMap.className.parametrizedBy(
+ mapValueResultAdapter.getDeclarationTypeName()
+ )
+ }
+
+ override fun convert(
+ scope: CodeGenScope,
+ valuesVarName: String,
+ cursorVarName: String,
+ dupeColumnsIndexAdapter: AmbiguousColumnIndexAdapter?,
+ genPutValueCode: (String, Boolean) -> Unit
+ ) {
+ scope.builder.apply {
+ // Read map key
+ val tmpKeyVarName = scope.getTmpVar("_key")
+ addLocalVariable(tmpKeyVarName, keyTypeArg.asTypeName())
+ keyRowAdapter.convert(tmpKeyVarName, cursorVarName, scope)
+
+ // Generate map key check if the next value adapter is by reference
+ // (nested map case or collection end value)
+ @Suppress("NAME_SHADOWING") // On purpose to avoid miss using param
+ val valuesVarName = if (mapValueResultAdapter.requiresContainsKeyCheck()) {
+ scope.getTmpVar("_values").also { tmpValuesVarName ->
+ addLocalVariable(
+ tmpValuesVarName,
+ mapValueResultAdapter.getDeclarationTypeName()
+ )
+ if (mapType.isSparseArray()) {
+ beginControlFlow(
+ "if (%L.get(%L) != null)",
+ valuesVarName,
+ tmpKeyVarName
+ )
+ } else {
+ beginControlFlow(
+ "if (%L.containsKey(%L))",
+ valuesVarName,
+ tmpKeyVarName
+ )
+ }.apply {
+ val getFunction = when (language) {
+ CodeLanguage.JAVA ->
+ "get"
+ CodeLanguage.KOTLIN ->
+ if (mapType.isSparseArray()) "get" else "getValue"
+ }
+ addStatement(
+ "%L = %L.%L(%L)",
+ tmpValuesVarName,
+ valuesVarName,
+ getFunction,
+ tmpKeyVarName
+ )
+ }.nextControlFlow("else").apply {
+ addStatement(
+ "%L = %L",
+ tmpValuesVarName,
+ XCodeBlock.ofNewInstance(
+ language,
+ mapValueResultAdapter.getInstantiationTypeName()
+ )
+ )
+ addStatement(
+ "%L.put(%L, %L)",
+ valuesVarName,
+ tmpKeyVarName,
+ tmpValuesVarName
+ )
+ }.endControlFlow()
+
+ // Perform key columns null check, in a nested mapping we still add
+ // the key with an empty map as the value entry.
+ mapValueResultAdapter.generateContinueColumnCheck(
+ scope,
+ cursorVarName,
+ dupeColumnsIndexAdapter
+ )
+ }
+ } else {
+ valuesVarName
+ }
+ @Suppress("NAME_SHADOWING") // On purpose, to avoid using param
+ val genPutValueCode: (String, Boolean) -> Unit = { tmpValueVarName, doKeyCheck ->
+ if (doKeyCheck) {
+ // For consistency purposes, in the one-to-one object mapping case, if
+ // multiple values are encountered for the same key, we will only
+ // consider the first ever encountered mapping.
+ if (mapType.isSparseArray()) {
+ beginControlFlow(
+ "if (%L.get(%L) == null)",
+ valuesVarName, tmpKeyVarName
+ )
+ } else {
+ beginControlFlow(
+ "if (!%L.containsKey(%L))",
+ valuesVarName, tmpKeyVarName
+ )
+ }.apply {
+ addStatement(
+ "%L.put(%L, %L)",
+ valuesVarName, tmpKeyVarName, tmpValueVarName
+ )
+ }.endControlFlow()
+ } else {
+ addStatement(
+ "%L.put(%L, %L)",
+ valuesVarName, tmpKeyVarName, tmpValueVarName
+ )
+ }
+ }
+ mapValueResultAdapter.convert(
+ scope = scope,
+ valuesVarName = valuesVarName,
+ cursorVarName = cursorVarName,
+ dupeColumnsIndexAdapter = dupeColumnsIndexAdapter,
+ genPutValueCode = genPutValueCode
+ )
+ }
+ }
+
+ override fun generateContinueColumnCheck(
+ scope: CodeGenScope,
+ cursorVarName: String,
+ dupeColumnsIndexAdapter: AmbiguousColumnIndexAdapter?
+ ) {
+ scope.builder.add(
+ getContinueColumnNullCheck(
+ language = scope.language,
+ cursorVarName = cursorVarName,
+ rowAdapter = keyRowAdapter,
+ dupeColumnsIndexAdapter = dupeColumnsIndexAdapter
+ )
+ )
+ }
+ }
+
+ /**
+ * An [EndMapValueResultAdapter] contains only the value information regarding the innermost
+ * map of the returned nested map.
+ *
+ * The [convert] function implementation for an [EndMapValueResultAdapter] uses the value row
+ * adapter to innermost value map's value, regardless of whether it is a collection type or not.
+ */
+ class EndMapValueResultAdapter(
+ private val valueRowAdapter: RowAdapter,
+ private val valueTypeArg: XType,
+ private val valueCollectionType: MultimapQueryResultAdapter.CollectionValueType?
+ ) : MapValueResultAdapter(
+ rowAdapters = listOf(valueRowAdapter)
+ ) {
+ override fun requiresContainsKeyCheck(): Boolean = valueCollectionType != null
+
+ // The type name of the concrete result map value
+ // For Map<Foo, Bar> it is Bar
+ // For Map<Foo, List<Bar> it is ArrayList<Bar>
+ override fun getDeclarationTypeName(): XTypeName {
+ return valueCollectionType?.className?.parametrizedBy(valueTypeArg.asTypeName())
+ ?: valueTypeArg.asTypeName()
+ }
+
+ // The type name of the result map value
+ // For Map<Foo, Bar> it is Bar
+ // for Map<Foo, List<Bar> it is List<Bar>
+ override fun getInstantiationTypeName(): XTypeName {
+ return when (valueCollectionType) {
+ MultimapQueryResultAdapter.CollectionValueType.LIST ->
+ CommonTypeNames.ARRAY_LIST.parametrizedBy(valueTypeArg.asTypeName())
+ MultimapQueryResultAdapter.CollectionValueType.SET ->
+ CommonTypeNames.HASH_SET.parametrizedBy(valueTypeArg.asTypeName())
+ else ->
+ valueTypeArg.asTypeName()
+ }
+ }
+
+ override fun convert(
+ scope: CodeGenScope,
+ valuesVarName: String,
+ cursorVarName: String,
+ dupeColumnsIndexAdapter: AmbiguousColumnIndexAdapter?,
+ genPutValueCode: (String, Boolean) -> Unit
+ ) {
+ scope.builder.apply {
+ val tmpValueVarName = scope.getTmpVar("_value")
+
+ // If we have a collection type, then this means that we have a 1-to-many mapping
+ // as opposed to a 1-to-many mapping.
+ if (valueCollectionType != null) {
+ addLocalVariable(
+ tmpValueVarName,
+ valueTypeArg.asTypeName()
+ )
+ valueRowAdapter.convert(tmpValueVarName, cursorVarName, scope)
+ addStatement("%L.add(%L)", valuesVarName, tmpValueVarName)
+ } else {
+ check(valueRowAdapter is QueryMappedRowAdapter)
+ val valueIndexVars =
+ dupeColumnsIndexAdapter?.getIndexVarsForMapping(valueRowAdapter.mapping)
+ ?: valueRowAdapter.getDefaultIndexAdapter().getIndexVars()
+ val columnNullCheckCodeBlock = getColumnNullCheckCode(
+ language = scope.language,
+ cursorVarName = cursorVarName,
+ indexVars = valueIndexVars
+ )
+
+ // Perform value columns null check, in a 1-to-1 mapping we still add the key
+ // with a null value entry if permitted.
+ beginControlFlow("if (%L)", columnNullCheckCodeBlock).apply {
+ if (
+ language == CodeLanguage.KOTLIN &&
+ valueTypeArg.nullability == XNullability.NONNULL
+ ) {
+ // TODO(b/249984504): Generate / output a better message.
+ addStatement("error(%S)", "Missing value for a key.")
+ } else {
+ genPutValueCode.invoke("null", false)
+ addStatement("continue")
+ }
+ }.endControlFlow()
+
+ addLocalVariable(tmpValueVarName, valueTypeArg.asTypeName())
+ valueRowAdapter.convert(tmpValueVarName, cursorVarName, scope)
+ genPutValueCode.invoke(tmpValueVarName, true)
+ }
+ }
+ }
+
+ override fun generateContinueColumnCheck(
+ scope: CodeGenScope,
+ cursorVarName: String,
+ dupeColumnsIndexAdapter: AmbiguousColumnIndexAdapter?
+ ) {
+ scope.builder.add(
+ getContinueColumnNullCheck(
+ language = scope.language,
+ cursorVarName = cursorVarName,
+ rowAdapter = valueRowAdapter,
+ dupeColumnsIndexAdapter = dupeColumnsIndexAdapter
+ )
+ )
+ }
+ }
+
+ /**
+ * Utility method that returns a code block containing the code expression that verifies if all
+ * matched fields are null.
+ */
+ protected fun getContinueColumnNullCheck(
+ language: CodeLanguage,
+ rowAdapter: RowAdapter,
+ cursorVarName: String,
+ dupeColumnsIndexAdapter: AmbiguousColumnIndexAdapter?
+ ) = XCodeBlock.builder(language).apply {
+ check(rowAdapter is QueryMappedRowAdapter)
+ val valueIndexVars =
+ dupeColumnsIndexAdapter?.getIndexVarsForMapping(rowAdapter.mapping)
+ ?: rowAdapter.getDefaultIndexAdapter().getIndexVars()
+ val columnNullCheckCodeBlock = getColumnNullCheckCode(
+ language = language,
+ cursorVarName = cursorVarName,
+ indexVars = valueIndexVars
+ )
+ beginControlFlow("if (%L)", columnNullCheckCodeBlock).apply {
+ addStatement("continue")
+ }.endControlFlow()
+ }.build()
+
+ /**
+ * Generates a code expression that verifies if all matched fields are null.
+ */
+ protected 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()
+}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MultimapQueryResultAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MultimapQueryResultAdapter.kt
index 810202e..c5791df 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MultimapQueryResultAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MultimapQueryResultAdapter.kt
@@ -42,8 +42,6 @@
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.
@@ -51,6 +49,8 @@
// but the resolver will still produce correct results based on the result columns at runtime.
val duplicateColumns: Set<String>
+ val dupeColumnsIndexAdapter: AmbiguousColumnIndexAdapter?
+
init {
val resultColumns =
parsedQuery.resultInfo?.columns?.map { it.name } ?: mappings.flatMap { it.usedColumns }
@@ -63,6 +63,11 @@
}
}
}
+ dupeColumnsIndexAdapter = if (duplicateColumns.isNotEmpty()) {
+ AmbiguousColumnIndexAdapter(mappings, parsedQuery)
+ } else {
+ null
+ }
if (parsedQuery.resultInfo != null && duplicateColumns.isNotEmpty()) {
// If there are duplicate columns and one of the result object is for a single column
@@ -113,18 +118,15 @@
companion object {
/**
- * Checks if the @MapInfo annotation is needed for clarification regarding the return type
- * of a Dao method.
+ * Checks if the @MapInfo annotation is needed for clarification regarding the key type
+ * arg of a Map return type.
*/
- fun validateMapTypeArgs(
+ fun validateMapKeyTypeArg(
context: Context,
keyTypeArg: XType,
- valueTypeArg: XType,
keyReader: CursorValueReader?,
- valueReader: CursorValueReader?,
mapInfo: MapInfo?,
) {
-
if (!keyTypeArg.implementsEqualsAndHashcode()) {
context.logger.w(
Warning.DOES_NOT_IMPLEMENT_EQUALS_HASHCODE,
@@ -142,7 +144,18 @@
)
)
}
+ }
+ /**
+ * Checks if the @MapInfo annotation is needed for clarification regarding the value type
+ * arg of a Map return type.
+ */
+ fun validateMapValueTypeArg(
+ context: Context,
+ valueTypeArg: XType,
+ valueReader: CursorValueReader?,
+ mapInfo: MapInfo?,
+ ) {
val hasValueColumnName = mapInfo?.valueColumnName?.isNotEmpty() ?: false
if (!hasValueColumnName && valueReader != null) {
context.logger.e(
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 c3d1f05..0f8ed9b 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
@@ -1406,7 +1406,7 @@
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorCount(2)
- hasErrorContaining("Multimap 'value' collection type must be a List or Set.")
+ hasErrorContaining("Multimap 'value' collection type must be a List, Set or Map.")
hasErrorContaining("Not sure how to convert a Cursor to this method's return type")
}
}
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt
index fbf60ac..a3af92e 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt
@@ -1518,6 +1518,79 @@
}
@Test
+ fun queryResultAdapter_nestedMap() {
+ val testName = object {}.javaClass.enclosingMethod!!.name
+ val src = Source.kotlin(
+ "MyDao.kt",
+ """
+ import androidx.room.*
+
+ @Database(
+ entities = [Artist::class, Song::class, Album::class, Playlist::class],
+ version = 1,
+ exportSchema = false
+ )
+ abstract class MyDatabase : RoomDatabase() {
+ abstract fun getDao(): MyDao
+ }
+
+ @Dao
+ interface MyDao {
+ @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
+ @Query(
+ "SELECT * FROM Artist JOIN (Album JOIN Song ON Album.albumName = Song.album) " +
+ "ON Artist.artistName = Album.albumArtist"
+ )
+ fun singleNested(): Map<Artist, Map<Album, List<Song>>>
+
+ @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
+ @Query(
+ "SELECT * FROM Playlist JOIN (Artist JOIN (Album JOIN Song " +
+ "ON Album.albumName = Song.album) " +
+ "ON Artist.artistName = Album.albumArtist)" +
+ "ON Playlist.playlistArtist = Artist.artistName"
+ )
+ fun doubleNested(): Map<Playlist, Map<Artist, Map<Album, List<Song>>>>
+ }
+
+ @Entity
+ data class Artist(
+ @PrimaryKey
+ val artistId: String,
+ val artistName: String,
+ )
+
+ @Entity
+ data class Album(
+ @PrimaryKey
+ val albumId: String,
+ val albumName: String,
+ val albumArtist: String
+ )
+
+ @Entity
+ data class Playlist(
+ @PrimaryKey
+ val playlistId: String,
+ val playlistArtist: String,
+ )
+
+ @Entity
+ data class Song(
+ @PrimaryKey
+ val songId: String,
+ val album: String,
+ val songArtist: String
+ )
+ """.trimIndent()
+ )
+ runTest(
+ sources = listOf(src),
+ expectedFilePath = getTestGoldenPath(testName)
+ )
+ }
+
+ @Test
fun queryResultAdapter_guavaImmutableMultimap() {
val testName = object {}.javaClass.enclosingMethod!!.name
val src = Source.kotlin(
@@ -1648,6 +1721,60 @@
}
@Test
+ fun queryResultAdapter_nestedMap_ambiguousIndexAdapter() {
+ val testName = object {}.javaClass.enclosingMethod!!.name
+ val src = Source.kotlin(
+ "MyDao.kt",
+ """
+ import androidx.room.*
+ import java.nio.ByteBuffer
+
+ @Database(
+ entities = [User::class, Comment::class, Avatar::class],
+ version = 1,
+ exportSchema = false
+ )
+ abstract class MyDatabase : RoomDatabase() {
+ abstract fun getDao(): MyDao
+ }
+
+ @Dao
+ interface MyDao {
+ @Query(
+ "SELECT * FROM User JOIN Avatar ON User.id = Avatar.userId JOIN " +
+ "Comment ON Avatar.userId = Comment.userId"
+ )
+ fun getLeftJoinUserNestedMap(): Map<User, Map<Avatar, List<Comment>>>
+ }
+
+ @Entity
+ data class User(
+ @PrimaryKey val id: Int,
+ val name: String,
+ )
+
+ @Entity
+ data class Comment(
+ @PrimaryKey val id: Int,
+ val userId: Int,
+ val text: String,
+ )
+
+ @Entity
+ data class Avatar(
+ @PrimaryKey val userId: Int,
+ val url: String,
+ val data: ByteBuffer,
+ )
+ """.trimIndent()
+ )
+ runTest(
+ sources = listOf(src),
+ expectedFilePath = getTestGoldenPath(testName)
+ )
+ }
+
+ @Test
fun entityRowAdapter() {
val testName = object {}.javaClass.enclosingMethod!!.name
val src = Source.kotlin(
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_nestedMap.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_nestedMap.kt
new file mode 100644
index 0000000..ac074fb
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_nestedMap.kt
@@ -0,0 +1,194 @@
+import android.database.Cursor
+import androidx.room.RoomDatabase
+import androidx.room.RoomSQLiteQuery
+import androidx.room.RoomSQLiteQuery.Companion.acquire
+import androidx.room.util.getColumnIndexOrThrow
+import androidx.room.util.query
+import java.lang.Class
+import java.util.ArrayList
+import java.util.LinkedHashMap
+import javax.`annotation`.processing.Generated
+import kotlin.Int
+import kotlin.String
+import kotlin.Suppress
+import kotlin.collections.List
+import kotlin.collections.Map
+import kotlin.collections.MutableList
+import kotlin.collections.MutableMap
+import kotlin.jvm.JvmStatic
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["UNCHECKED_CAST", "DEPRECATION", "REDUNDANT_PROJECTION"])
+public class MyDao_Impl(
+ __db: RoomDatabase,
+) : MyDao {
+ private val __db: RoomDatabase
+ init {
+ this.__db = __db
+ }
+
+ public override fun singleNested(): Map<Artist, Map<Album, List<Song>>> {
+ val _sql: String =
+ "SELECT * FROM Artist JOIN (Album JOIN Song ON Album.albumName = Song.album) ON Artist.artistName = Album.albumArtist"
+ val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+ __db.assertNotSuspendingTransaction()
+ val _cursor: Cursor = query(__db, _statement, false, null)
+ try {
+ val _cursorIndexOfArtistId: Int = getColumnIndexOrThrow(_cursor, "artistId")
+ val _cursorIndexOfArtistName: Int = getColumnIndexOrThrow(_cursor, "artistName")
+ val _cursorIndexOfAlbumId: Int = getColumnIndexOrThrow(_cursor, "albumId")
+ val _cursorIndexOfAlbumName: Int = getColumnIndexOrThrow(_cursor, "albumName")
+ val _cursorIndexOfAlbumArtist: Int = getColumnIndexOrThrow(_cursor, "albumArtist")
+ val _cursorIndexOfSongId: Int = getColumnIndexOrThrow(_cursor, "songId")
+ val _cursorIndexOfAlbum: Int = getColumnIndexOrThrow(_cursor, "album")
+ val _cursorIndexOfSongArtist: Int = getColumnIndexOrThrow(_cursor, "songArtist")
+ val _result: MutableMap<Artist, MutableMap<Album, MutableList<Song>>> =
+ LinkedHashMap<Artist, MutableMap<Album, MutableList<Song>>>()
+ while (_cursor.moveToNext()) {
+ val _key: Artist
+ val _tmpArtistId: String
+ _tmpArtistId = _cursor.getString(_cursorIndexOfArtistId)
+ val _tmpArtistName: String
+ _tmpArtistName = _cursor.getString(_cursorIndexOfArtistName)
+ _key = Artist(_tmpArtistId,_tmpArtistName)
+ val _values: MutableMap<Album, MutableList<Song>>
+ if (_result.containsKey(_key)) {
+ _values = _result.getValue(_key)
+ } else {
+ _values = LinkedHashMap<Album, MutableList<Song>>()
+ _result.put(_key, _values)
+ }
+ if (_cursor.isNull(_cursorIndexOfAlbumId) && _cursor.isNull(_cursorIndexOfAlbumName) &&
+ _cursor.isNull(_cursorIndexOfAlbumArtist)) {
+ continue
+ }
+ val _key_1: Album
+ val _tmpAlbumId: String
+ _tmpAlbumId = _cursor.getString(_cursorIndexOfAlbumId)
+ val _tmpAlbumName: String
+ _tmpAlbumName = _cursor.getString(_cursorIndexOfAlbumName)
+ val _tmpAlbumArtist: String
+ _tmpAlbumArtist = _cursor.getString(_cursorIndexOfAlbumArtist)
+ _key_1 = Album(_tmpAlbumId,_tmpAlbumName,_tmpAlbumArtist)
+ val _values_1: MutableList<Song>
+ if (_values.containsKey(_key_1)) {
+ _values_1 = _values.getValue(_key_1)
+ } else {
+ _values_1 = ArrayList<Song>()
+ _values.put(_key_1, _values_1)
+ }
+ if (_cursor.isNull(_cursorIndexOfSongId) && _cursor.isNull(_cursorIndexOfAlbum) &&
+ _cursor.isNull(_cursorIndexOfSongArtist)) {
+ continue
+ }
+ val _value: Song
+ val _tmpSongId: String
+ _tmpSongId = _cursor.getString(_cursorIndexOfSongId)
+ val _tmpAlbum: String
+ _tmpAlbum = _cursor.getString(_cursorIndexOfAlbum)
+ val _tmpSongArtist: String
+ _tmpSongArtist = _cursor.getString(_cursorIndexOfSongArtist)
+ _value = Song(_tmpSongId,_tmpAlbum,_tmpSongArtist)
+ _values_1.add(_value)
+ }
+ return _result
+ } finally {
+ _cursor.close()
+ _statement.release()
+ }
+ }
+
+ public override fun doubleNested(): Map<Playlist, Map<Artist, Map<Album, List<Song>>>> {
+ val _sql: String =
+ "SELECT * FROM Playlist JOIN (Artist JOIN (Album JOIN Song ON Album.albumName = Song.album) ON Artist.artistName = Album.albumArtist)ON Playlist.playlistArtist = Artist.artistName"
+ val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+ __db.assertNotSuspendingTransaction()
+ val _cursor: Cursor = query(__db, _statement, false, null)
+ try {
+ val _cursorIndexOfPlaylistId: Int = getColumnIndexOrThrow(_cursor, "playlistId")
+ val _cursorIndexOfPlaylistArtist: Int = getColumnIndexOrThrow(_cursor, "playlistArtist")
+ val _cursorIndexOfArtistId: Int = getColumnIndexOrThrow(_cursor, "artistId")
+ val _cursorIndexOfArtistName: Int = getColumnIndexOrThrow(_cursor, "artistName")
+ val _cursorIndexOfAlbumId: Int = getColumnIndexOrThrow(_cursor, "albumId")
+ val _cursorIndexOfAlbumName: Int = getColumnIndexOrThrow(_cursor, "albumName")
+ val _cursorIndexOfAlbumArtist: Int = getColumnIndexOrThrow(_cursor, "albumArtist")
+ val _cursorIndexOfSongId: Int = getColumnIndexOrThrow(_cursor, "songId")
+ val _cursorIndexOfAlbum: Int = getColumnIndexOrThrow(_cursor, "album")
+ val _cursorIndexOfSongArtist: Int = getColumnIndexOrThrow(_cursor, "songArtist")
+ val _result: MutableMap<Playlist, MutableMap<Artist, MutableMap<Album, MutableList<Song>>>> =
+ LinkedHashMap<Playlist, MutableMap<Artist, MutableMap<Album, MutableList<Song>>>>()
+ while (_cursor.moveToNext()) {
+ val _key: Playlist
+ val _tmpPlaylistId: String
+ _tmpPlaylistId = _cursor.getString(_cursorIndexOfPlaylistId)
+ val _tmpPlaylistArtist: String
+ _tmpPlaylistArtist = _cursor.getString(_cursorIndexOfPlaylistArtist)
+ _key = Playlist(_tmpPlaylistId,_tmpPlaylistArtist)
+ val _values: MutableMap<Artist, MutableMap<Album, MutableList<Song>>>
+ if (_result.containsKey(_key)) {
+ _values = _result.getValue(_key)
+ } else {
+ _values = LinkedHashMap<Artist, MutableMap<Album, MutableList<Song>>>()
+ _result.put(_key, _values)
+ }
+ if (_cursor.isNull(_cursorIndexOfArtistId) && _cursor.isNull(_cursorIndexOfArtistName)) {
+ continue
+ }
+ val _key_1: Artist
+ val _tmpArtistId: String
+ _tmpArtistId = _cursor.getString(_cursorIndexOfArtistId)
+ val _tmpArtistName: String
+ _tmpArtistName = _cursor.getString(_cursorIndexOfArtistName)
+ _key_1 = Artist(_tmpArtistId,_tmpArtistName)
+ val _values_1: MutableMap<Album, MutableList<Song>>
+ if (_values.containsKey(_key_1)) {
+ _values_1 = _values.getValue(_key_1)
+ } else {
+ _values_1 = LinkedHashMap<Album, MutableList<Song>>()
+ _values.put(_key_1, _values_1)
+ }
+ if (_cursor.isNull(_cursorIndexOfAlbumId) && _cursor.isNull(_cursorIndexOfAlbumName) &&
+ _cursor.isNull(_cursorIndexOfAlbumArtist)) {
+ continue
+ }
+ val _key_2: Album
+ val _tmpAlbumId: String
+ _tmpAlbumId = _cursor.getString(_cursorIndexOfAlbumId)
+ val _tmpAlbumName: String
+ _tmpAlbumName = _cursor.getString(_cursorIndexOfAlbumName)
+ val _tmpAlbumArtist: String
+ _tmpAlbumArtist = _cursor.getString(_cursorIndexOfAlbumArtist)
+ _key_2 = Album(_tmpAlbumId,_tmpAlbumName,_tmpAlbumArtist)
+ val _values_2: MutableList<Song>
+ if (_values_1.containsKey(_key_2)) {
+ _values_2 = _values_1.getValue(_key_2)
+ } else {
+ _values_2 = ArrayList<Song>()
+ _values_1.put(_key_2, _values_2)
+ }
+ if (_cursor.isNull(_cursorIndexOfSongId) && _cursor.isNull(_cursorIndexOfAlbum) &&
+ _cursor.isNull(_cursorIndexOfSongArtist)) {
+ continue
+ }
+ val _value: Song
+ val _tmpSongId: String
+ _tmpSongId = _cursor.getString(_cursorIndexOfSongId)
+ val _tmpAlbum: String
+ _tmpAlbum = _cursor.getString(_cursorIndexOfAlbum)
+ val _tmpSongArtist: String
+ _tmpSongArtist = _cursor.getString(_cursorIndexOfSongArtist)
+ _value = Song(_tmpSongId,_tmpAlbum,_tmpSongArtist)
+ _values_2.add(_value)
+ }
+ return _result
+ } finally {
+ _cursor.close()
+ _statement.release()
+ }
+ }
+
+ public companion object {
+ @JvmStatic
+ public fun getRequiredConverters(): List<Class<*>> = emptyList()
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_nestedMap_ambiguousIndexAdapter.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_nestedMap_ambiguousIndexAdapter.kt
new file mode 100644
index 0000000..b29ed2e
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_nestedMap_ambiguousIndexAdapter.kt
@@ -0,0 +1,103 @@
+import android.database.Cursor
+import androidx.room.AmbiguousColumnResolver
+import androidx.room.RoomDatabase
+import androidx.room.RoomSQLiteQuery
+import androidx.room.RoomSQLiteQuery.Companion.acquire
+import androidx.room.util.query
+import java.lang.Class
+import java.nio.ByteBuffer
+import java.util.ArrayList
+import java.util.LinkedHashMap
+import javax.`annotation`.processing.Generated
+import kotlin.Array
+import kotlin.Int
+import kotlin.IntArray
+import kotlin.String
+import kotlin.Suppress
+import kotlin.collections.List
+import kotlin.collections.Map
+import kotlin.collections.MutableList
+import kotlin.collections.MutableMap
+import kotlin.jvm.JvmStatic
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["UNCHECKED_CAST", "DEPRECATION", "REDUNDANT_PROJECTION"])
+public class MyDao_Impl(
+ __db: RoomDatabase,
+) : MyDao {
+ private val __db: RoomDatabase
+ init {
+ this.__db = __db
+ }
+
+ public override fun getLeftJoinUserNestedMap(): Map<User, Map<Avatar, List<Comment>>> {
+ val _sql: String =
+ "SELECT * FROM User JOIN Avatar ON User.id = Avatar.userId JOIN Comment ON Avatar.userId = Comment.userId"
+ val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+ __db.assertNotSuspendingTransaction()
+ val _cursor: Cursor = query(__db, _statement, false, null)
+ try {
+ val _cursorIndices: Array<IntArray> =
+ AmbiguousColumnResolver.resolve(_cursor.getColumnNames(), arrayOf(arrayOf("id", "name"),
+ arrayOf("userId", "url", "data"), arrayOf("id", "userId", "text")))
+ val _result: MutableMap<User, MutableMap<Avatar, MutableList<Comment>>> =
+ LinkedHashMap<User, MutableMap<Avatar, MutableList<Comment>>>()
+ while (_cursor.moveToNext()) {
+ val _key: User
+ val _tmpId: Int
+ _tmpId = _cursor.getInt(_cursorIndices[0][0])
+ val _tmpName: String
+ _tmpName = _cursor.getString(_cursorIndices[0][1])
+ _key = User(_tmpId,_tmpName)
+ val _values: MutableMap<Avatar, MutableList<Comment>>
+ if (_result.containsKey(_key)) {
+ _values = _result.getValue(_key)
+ } else {
+ _values = LinkedHashMap<Avatar, MutableList<Comment>>()
+ _result.put(_key, _values)
+ }
+ if (_cursor.isNull(_cursorIndices[1][0]) && _cursor.isNull(_cursorIndices[1][1]) &&
+ _cursor.isNull(_cursorIndices[1][2])) {
+ continue
+ }
+ val _key_1: Avatar
+ val _tmpUserId: Int
+ _tmpUserId = _cursor.getInt(_cursorIndices[1][0])
+ val _tmpUrl: String
+ _tmpUrl = _cursor.getString(_cursorIndices[1][1])
+ val _tmpData: ByteBuffer
+ _tmpData = ByteBuffer.wrap(_cursor.getBlob(_cursorIndices[1][2]))
+ _key_1 = Avatar(_tmpUserId,_tmpUrl,_tmpData)
+ val _values_1: MutableList<Comment>
+ if (_values.containsKey(_key_1)) {
+ _values_1 = _values.getValue(_key_1)
+ } else {
+ _values_1 = ArrayList<Comment>()
+ _values.put(_key_1, _values_1)
+ }
+ if (_cursor.isNull(_cursorIndices[2][0]) && _cursor.isNull(_cursorIndices[2][1]) &&
+ _cursor.isNull(_cursorIndices[2][2])) {
+ continue
+ }
+ val _value: Comment
+ val _tmpId_1: Int
+ _tmpId_1 = _cursor.getInt(_cursorIndices[2][0])
+ val _tmpUserId_1: Int
+ _tmpUserId_1 = _cursor.getInt(_cursorIndices[2][1])
+ val _tmpText: String
+ _tmpText = _cursor.getString(_cursorIndices[2][2])
+ _value = Comment(_tmpId_1,_tmpUserId_1,_tmpText)
+ _values_1.add(_value)
+ }
+ return _result
+ } finally {
+ _cursor.close()
+ _statement.release()
+ }
+ }
+
+ public companion object {
+ @JvmStatic
+ public fun getRequiredConverters(): List<Class<*>> = emptyList()
+ }
+}
\ No newline at end of file