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