Adding support for nested map return types in DAO functions of Room.

This change involves a refactoring of MapQueryResultAdapter to handle both regular maps (with or without collection value types) as well as nested map values. There is no limit set to the amount of nesting, and the algorithm is able to handle as many nested maps as possible.

Currently, @MapInfo support is limited to non-nested map return types and this functionality will be available in a follow-up CL. In addition, support for ArrayMap, LongSparseArray or IntSparseArray will be added in a future CL.

Bug: 203008711
Test: MultimapQueryTest.kt, DaoKotlinCodeGenTest.kt
Change-Id: I13f488d2d26b5f41cebf14c2cc789eb291aa064c
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