Support many-to-many with @Relation using @Junction

Expand @Relation to contain a new method, 'associateBy' of type
@Junction that can be used to specify a junction table for resolving
relationships, mainly many-to-many.

Upon using @Junction the query
generated by the RelationCollector is of the form of:
SELECT *, <junction-parent-column>
  FROM <junction-table> AS _junction
  JOIN <entity-table> ON (<junction-entity-column> = <entity-column>)
  WHERE <junction-parent-column> IN (:args)

@Junction supports both an @Entity or @DatabaseView, the Junction table
itself doesn't have to strictly model a many-to-many relation. Both
parentColumn and entityColumn are optional and will be assumed to be
the same as the ones defined in @Relation, otherwise those methods can
be used to specify arbitrary columns in the junction table.

Room will also give a warning for those columns in the junction table
that are not part of a foreign key.

Bug: 63736353
Bug: 69201917
Test: ManyToManyRelationTest, PojoProcessorTest, and others...
Change-Id: I967cf77252391379377ebe0ebfeb23a4fd881ca5
diff --git a/room/common/api/2.2.0-alpha01.ignore b/room/common/api/2.2.0-alpha01.ignore
new file mode 100644
index 0000000..155e4e0
--- /dev/null
+++ b/room/common/api/2.2.0-alpha01.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+AddedAbstractMethod: androidx.room.Relation#associateBy():
+    Added method androidx.room.Relation.associateBy()
diff --git a/room/common/api/2.2.0-alpha01.txt b/room/common/api/2.2.0-alpha01.txt
index d2e3b28..a9643a9 100644
--- a/room/common/api/2.2.0-alpha01.txt
+++ b/room/common/api/2.2.0-alpha01.txt
@@ -120,6 +120,12 @@
     method @androidx.room.OnConflictStrategy public abstract int onConflict() default androidx.room.OnConflictStrategy.ABORT;
   }
 
+  @java.lang.annotation.Target({}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface Junction {
+    method public abstract String entityColumn() default "";
+    method public abstract String parentColumn() default "";
+    method public abstract Class value();
+  }
+
   @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @IntDef({androidx.room.OnConflictStrategy.REPLACE, androidx.room.OnConflictStrategy.ROLLBACK, androidx.room.OnConflictStrategy.ABORT, androidx.room.OnConflictStrategy.FAIL, androidx.room.OnConflictStrategy.IGNORE}) public @interface OnConflictStrategy {
     field public static final int ABORT = 3; // 0x3
     field @Deprecated public static final int FAIL = 4; // 0x4
@@ -141,6 +147,7 @@
   }
 
   @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface Relation {
+    method public abstract androidx.room.Junction associateBy() default @androidx.room.Junction;
     method public abstract Class entity() default java.lang.Object.class;
     method public abstract String entityColumn();
     method public abstract String parentColumn();
diff --git a/room/common/api/current.txt b/room/common/api/current.txt
index d2e3b28..a9643a9 100644
--- a/room/common/api/current.txt
+++ b/room/common/api/current.txt
@@ -120,6 +120,12 @@
     method @androidx.room.OnConflictStrategy public abstract int onConflict() default androidx.room.OnConflictStrategy.ABORT;
   }
 
+  @java.lang.annotation.Target({}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface Junction {
+    method public abstract String entityColumn() default "";
+    method public abstract String parentColumn() default "";
+    method public abstract Class value();
+  }
+
   @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @IntDef({androidx.room.OnConflictStrategy.REPLACE, androidx.room.OnConflictStrategy.ROLLBACK, androidx.room.OnConflictStrategy.ABORT, androidx.room.OnConflictStrategy.FAIL, androidx.room.OnConflictStrategy.IGNORE}) public @interface OnConflictStrategy {
     field public static final int ABORT = 3; // 0x3
     field @Deprecated public static final int FAIL = 4; // 0x4
@@ -141,6 +147,7 @@
   }
 
   @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface Relation {
+    method public abstract androidx.room.Junction associateBy() default @androidx.room.Junction;
     method public abstract Class entity() default java.lang.Object.class;
     method public abstract String entityColumn();
     method public abstract String parentColumn();
diff --git a/room/common/src/main/java/androidx/room/Junction.java b/room/common/src/main/java/androidx/room/Junction.java
new file mode 100644
index 0000000..6de1661
--- /dev/null
+++ b/room/common/src/main/java/androidx/room/Junction.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2019 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;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Declares a junction to be used for joining a relationship.
+ * <p>
+ * If a {@link Relation} should use an associative table (also know as junction table or join
+ * table) then you can use this annotation to reference such table. This is useful for fetching
+ * many-to-many relations.
+ * <pre>
+ * {@literal @}Entity(primaryKeys = {"pId", "sId"})
+ * public class PlaylistSongXRef {
+ *     int pId;
+ *     int sId;
+ * }
+ * public class PlaylistWithSongs {
+ *     {@literal @}Embedded
+ *     Playlist playlist;
+ *     {@literal @}Relation(
+ *             parentColumn = "playlistId",
+ *             entity = Song.class,
+ *             entityColumn = "songId",
+ *             associateBy = {@literal @}Junction(
+ *                     value = PlaylistSongXRef.class,
+ *                     parentColumn = "pId",
+ *                     entityColumn = "sId)
+ *     )
+ *     List&lt;String&gt; songs;
+ * }
+ * {@literal @}Dao
+ * public interface MusicDao {
+ *     {@literal @}Query("SELECT * FROM Playlist")
+ *     List&lt;PlaylistWithSongs&gt; getAllPlaylistsWithSongs();
+ * }
+ * </pre>
+ * <p>
+ * In the above example the many-to-many relationship between {@code Song} and {@code Playlist} has
+ * an associative table defined by the entity {@code PlaylistSongXRef}.
+ *
+ * @see Relation
+ */
+@Target({})
+@Retention(RetentionPolicy.CLASS)
+public @interface Junction {
+    /**
+     * An entity or entity or database view to be used as a junction table when fetching the
+     * relating entities.
+     *
+     * @return The entity or database view to be used as a junction table.
+     */
+    Class value();
+
+    /**
+     * The junction column that will be used to match against the {@link Relation#parentColumn()}.
+     * <p>
+     * If not specified it defaults to {@link Relation#parentColumn()}.
+     */
+    String parentColumn() default "";
+
+    /**
+     * The junction column that will be used to match against the {@link Relation#entityColumn()}.
+     * <p>
+     * If not specified it defaults to {@link Relation#entityColumn()}.
+     */
+    String entityColumn() default "";
+}
diff --git a/room/common/src/main/java/androidx/room/Relation.java b/room/common/src/main/java/androidx/room/Relation.java
index 10221a1..a231b2d 100644
--- a/room/common/src/main/java/androidx/room/Relation.java
+++ b/room/common/src/main/java/androidx/room/Relation.java
@@ -27,24 +27,24 @@
  *
  * <pre>
  * {@literal @}Entity
- * public class Pet {
+ * public class Song {
  *     {@literal @} PrimaryKey
- *     int id;
- *     int userId;
+ *     int songId;
+ *     int albumId;
  *     String name;
  *     // other fields
  * }
- * public class UserNameAndAllPets {
- *   public int id;
- *   public String name;
- *   {@literal @}Relation(parentColumn = "id", entityColumn = "userId")
- *   public List&lt;Pet&gt; pets;
+ * public class AlbumNameAndAllSongs {
+ *     int id;
+ *     String name;
+ *     {@literal @}Relation(parentColumn = "id", entityColumn = "albumId")
+ *     List&lt;Song&gt; songs;
  * }
  *
  * {@literal @}Dao
- * public interface UserPetDao {
- *     {@literal @}Query("SELECT id, name from User")
- *     public List&lt;UserNameAndAllPets&gt; loadUserAndPets();
+ * public interface MusicDao {
+ *     {@literal @}Query("SELECT id, name FROM Album")
+ *     List&lt;AlbumNameAndAllSongs&gt; loadAlbumAndSongs();
  * }
  * </pre>
  * <p>
@@ -53,51 +53,56 @@
  * If you would like to return a different object, you can specify the {@link #entity()} property
  * in the annotation.
  * <pre>
- * public class User {
+ * public class Album {
  *     int id;
  *     // other fields
  * }
- * public class PetNameAndId {
- *     int id;
+ * public class SongNameAndId {
+ *     int songId;
  *     String name;
  * }
- * public class UserAllPets {
- *   {@literal @}Embedded
- *   public User user;
- *   {@literal @}Relation(parentColumn = "id", entityColumn = "userId", entity = Pet.class)
- *   public List&lt;PetNameAndId&gt; pets;
+ * public class AlbumAllSongs {
+ *     {@literal @}Embedded
+ *     Album album;
+ *     {@literal @}Relation(parentColumn = "id", entityColumn = "albumId", entity = Song.class)
+ *     List&lt;SongNameAndId&gt; songs;
  * }
  * {@literal @}Dao
- * public interface UserPetDao {
- *     {@literal @}Query("SELECT * from User")
- *     public List&lt;UserAllPets&gt; loadUserAndPets();
+ * public interface MusicDao {
+ *     {@literal @}Query("SELECT * from Album")
+ *     List&lt;AlbumAllSongs&gt; loadAlbumAndSongs();
  * }
  * </pre>
  * <p>
- * In the example above, {@code PetNameAndId} is a regular Pojo but all of fields are fetched
- * from the {@code entity} defined in the {@code @Relation} annotation (<i>Pet</i>).
- * {@code PetNameAndId} could also define its own relations all of which would also be fetched
+ * In the example above, {@code SongNameAndId} is a regular Pojo but all of fields are fetched
+ * from the {@code entity} defined in the {@code @Relation} annotation (<i>Song</i>).
+ * {@code SongNameAndId} could also define its own relations all of which would also be fetched
  * automatically.
  * <p>
  * If you would like to specify which columns are fetched from the child {@link Entity}, you can
  * use {@link #projection()} property in the {@code Relation} annotation.
  * <pre>
- * public class UserAndAllPets {
- *   {@literal @}Embedded
- *   public User user;
- *   {@literal @}Relation(parentColumn = "id", entityColumn = "userId", entity = Pet.class,
- *           projection = {"name"})
- *   public List&lt;String&gt; petNames;
+ * public class AlbumAndAllSongs {
+ *     {@literal @}Embedded
+ *     Album album;
+ *     {@literal @}Relation(
+ *             parentColumn = "id",
+ *             entityColumn = "albumId",
+ *             entity = Song.class,
+ *             projection = {"name"})
+ *     List&lt;String&gt; songNames;
  * }
  * </pre>
  * <p>
+ * If the relationship is defined by an associative table (also know as junction table) then you can
+ * use {@link #associateBy()} to specify it. This is useful for fetching many-to-many relations.
+ * <p>
  * Note that {@code @Relation} annotation can be used only in Pojo classes, an {@link Entity} class
  * cannot have relations. This is a design decision to avoid common pitfalls in {@link Entity}
  * setups. You can read more about it in the main Room documentation. When loading data, you can
  * simply work around this limitation by creating Pojo classes that extend the {@link Entity}.
- * <p>
- * Note that the {@code @Relation} annotated field cannot be a constructor parameter, it must be
- * public or have a public setter.
+ *
+ * @see Junction
  */
 @Target({ElementType.FIELD, ElementType.METHOD})
 @Retention(RetentionPolicy.CLASS)
@@ -111,28 +116,38 @@
     Class entity() default Object.class;
 
     /**
-     * Reference field in the parent Pojo.
+     * Reference column in the parent Pojo.
      * <p>
-     * If you would like to access to a sub item of a {@link Embedded}d field, you can use
-     * the {@code .} notation.
-     * <p>
-     * For instance, if you have a {@link Embedded}d field named {@code user} with a sub field
-     * {@code id}, you can reference it via {@code user.id}.
-     * <p>
-     * This value will be matched against the value defined in {@link #entityColumn()}.
+     * In a one-to-one or one-to-many relation, this value will be matched against the column
+     * defined in {@link #entityColumn()}. In a many-to-many using {@link #associateBy()} then
+     * this value will be matched against the {@link Junction#parentColumn()}
      *
-     * @return The field reference in the parent object.
+     * @return The column reference in the parent object.
      */
     String parentColumn();
 
     /**
-     * The field path to match in the {@link #entity()}. This value will be matched against the
-     * value defined in {@link #parentColumn()}.
+     * The column to match in the {@link #entity()}.
+     * <p>
+     * In a one-to-one or one-to-many relation, this value will be matched against the column
+     * defined in {@link #parentColumn()} ()}. In a many-to-many using {@link #associateBy()} then
+     * this value will be matched against the {@link Junction#entityColumn()}
      */
     String entityColumn();
 
     /**
-     * If sub fields should be fetched from the entity, you can specify them using this field.
+     * The entity or view to be used as a associative table (also known as a junction table) when
+     * fetching the relating entities.
+     *
+     * @return The junction describing the associative table. By default, no junction is specified
+     * and none will be used.
+     *
+     * @see Junction
+     */
+    Junction associateBy() default @Junction(Object.class);
+
+    /**
+     * If sub columns should be fetched from the entity, you can specify them using this field.
      * <p>
      * By default, inferred from the the return type.
      *
diff --git a/room/compiler/src/main/kotlin/androidx/room/ext/element_ext.kt b/room/compiler/src/main/kotlin/androidx/room/ext/element_ext.kt
index e6a7edb..3fb705f 100644
--- a/room/compiler/src/main/kotlin/androidx/room/ext/element_ext.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/ext/element_ext.kt
@@ -98,13 +98,14 @@
 interface ClassGetter {
     fun getAsTypeMirror(methodName: String): TypeMirror?
     fun getAsTypeMirrorList(methodName: String): List<TypeMirror>
-    fun <T : Annotation> getAsAnnotationBox(methodName: String): Array<AnnotationBox<T>>
+    fun <T : Annotation> getAsAnnotationBox(methodName: String): AnnotationBox<T>
+    fun <T : Annotation> getAsAnnotationBoxArray(methodName: String): Array<AnnotationBox<T>>
 }
 
 /**
  * Class that helps to read values from annotations. Simple types as string, int, lists can
  * be read from [value]. If you need to read classes or another annotations from annotation use
- * [getAsTypeMirror] and [getAsAnnotationBox] correspondingly.
+ * [getAsTypeMirror], [getAsAnnotationBox] and [getAsAnnotationBoxArray] correspondingly.
  */
 class AnnotationBox<T : Annotation>(private val obj: Any) : ClassGetter by (obj as ClassGetter) {
     @Suppress("UNCHECKED_CAST")
@@ -133,6 +134,10 @@
                 }
             }
             returnType == Int::class.java -> value.getAsInt(defaultValue as Int?)
+            returnType.isAnnotation -> {
+                @Suppress("UNCHECKED_CAST")
+                AnnotationClassVisitor(returnType as Class<out Annotation>).visit(value)
+            }
             returnType.isArray && returnType.componentType.isAnnotation -> {
                 @Suppress("UNCHECKED_CAST")
                 ListVisitor(returnType.componentType as Class<out Annotation>).visit(value)
@@ -151,6 +156,7 @@
             ClassGetter::getAsTypeMirror.name -> map[args[0]]
             ClassGetter::getAsTypeMirrorList.name -> map[args[0]]
             "getAsAnnotationBox" -> map[args[0]]
+            "getAsAnnotationBoxArray" -> map[args[0]]
             else -> map[method.name]
         }
     })
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/EntityProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/EntityProcessor.kt
index 0ae258e..5b7b809 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/EntityProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/EntityProcessor.kt
@@ -41,7 +41,7 @@
         }
 
         fun extractIndices(annotation: AnnotationBox<Entity>, tableName: String): List<IndexInput> {
-            return annotation.getAsAnnotationBox<androidx.room.Index>("indices").map {
+            return annotation.getAsAnnotationBoxArray<androidx.room.Index>("indices").map {
                 val indexAnnotation = it.value
                 val nameValue = indexAnnotation.name
                 val name = if (nameValue == "") {
@@ -58,7 +58,7 @@
         }
 
         fun extractForeignKeys(annotation: AnnotationBox<Entity>): List<ForeignKeyInput> {
-            return annotation.getAsAnnotationBox<ForeignKey>("foreignKeys")
+            return annotation.getAsAnnotationBoxArray<ForeignKey>("foreignKeys")
                     .mapNotNull { annotationBox ->
                 val foreignKey = annotationBox.value
                 val parent = annotationBox.getAsTypeMirror("entity")
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/PojoProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/PojoProcessor.kt
index fdd3941..4da95da3 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/PojoProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/PojoProcessor.kt
@@ -19,6 +19,7 @@
 import androidx.room.ColumnInfo
 import androidx.room.Embedded
 import androidx.room.Ignore
+import androidx.room.Junction
 import androidx.room.PrimaryKey
 import androidx.room.Relation
 import androidx.room.ext.extendsBoundOrSelf
@@ -41,6 +42,7 @@
 import androidx.room.vo.CallType
 import androidx.room.vo.Constructor
 import androidx.room.vo.EmbeddedField
+import androidx.room.vo.Entity
 import androidx.room.vo.EntityOrView
 import androidx.room.vo.Field
 import androidx.room.vo.FieldGetter
@@ -491,7 +493,6 @@
 
         // now find the field in the entity.
         val entityField = entity.findFieldByColumnName(annotation.value.entityColumn)
-
         if (entityField == null) {
             context.logger.e(relationElement,
                     ProcessorErrors.relationCannotFindEntityField(
@@ -501,6 +502,82 @@
             return null
         }
 
+        // do we have a join entity?
+        val junctionAnnotation = annotation.getAsAnnotationBox<Junction>("associateBy")
+        val junctionClassInput = junctionAnnotation.getAsTypeMirror("value")
+        val junctionElement: TypeElement? = if (junctionClassInput != null &&
+                !MoreTypes.isTypeOf(Any::class.java, junctionClassInput)) {
+            junctionClassInput.asTypeElement()
+        } else {
+            null
+        }
+        val junction = junctionElement?.let {
+            val entityOrView = EntityOrViewProcessor(context, it, referenceStack).process()
+
+            fun findAndValidateJunctionColumn(
+                columnName: String,
+                onMissingField: () -> Unit
+            ): Field? {
+                val field = entityOrView.findFieldByColumnName(columnName)
+                if (field == null) {
+                    onMissingField()
+                    return null
+                }
+                if (entityOrView is Entity) {
+                    // warn about not having indices in the junction columns, only considering
+                    // 1st column in composite primary key and indices, since order matters.
+                    val coveredColumns = entityOrView.primaryKey.fields.columnNames.first() +
+                            entityOrView.indices.map { it.columnNames.first() }
+                    if (!coveredColumns.contains(field.columnName)) {
+                        context.logger.w(Warning.MISSING_INDEX_ON_JUNCTION, field.element,
+                            ProcessorErrors.junctionColumnWithoutIndex(
+                                entityName = entityOrView.typeName.toString(),
+                                columnName = columnName))
+                    }
+                }
+                return field
+            }
+
+            val junctionParentColumn = if (junctionAnnotation.value.parentColumn.isNotEmpty()) {
+                junctionAnnotation.value.parentColumn
+            } else {
+                parentField.columnName
+            }
+            val junctionParentField = findAndValidateJunctionColumn(
+                columnName = junctionParentColumn,
+                onMissingField = {
+                    context.logger.e(junctionElement,
+                        ProcessorErrors.relationCannotFindJunctionParentField(
+                            entityName = entityOrView.typeName.toString(),
+                            columnName = junctionParentColumn,
+                            availableColumns = entityOrView.columnNames))
+                })
+
+            val junctionEntityColumn = if (junctionAnnotation.value.entityColumn.isNotEmpty()) {
+                junctionAnnotation.value.entityColumn
+            } else {
+                entityField.columnName
+            }
+            val junctionEntityField = findAndValidateJunctionColumn(
+                columnName = junctionEntityColumn,
+                onMissingField = {
+                    context.logger.e(junctionElement,
+                        ProcessorErrors.relationCannotFindJunctionEntityField(
+                            entityName = entityOrView.typeName.toString(),
+                            columnName = junctionEntityColumn,
+                            availableColumns = entityOrView.columnNames))
+                })
+
+            if (junctionParentField == null || junctionEntityField == null) {
+                return null
+            }
+
+            androidx.room.vo.Junction(
+                entity = entityOrView,
+                parentField = junctionParentField,
+                entityField = junctionEntityField)
+        }
+
         val field = Field(
                 element = relationElement,
                 name = relationElement.simpleName.toString(),
@@ -523,6 +600,7 @@
                 field = field,
                 parentField = parentField,
                 entityField = entityField,
+                junction = junction,
                 projection = projection
         )
     }
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt b/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
index f70ec21..93b3435 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
@@ -378,6 +378,30 @@
                 " Options: ${availableColumns.joinToString(", ")}"
     }
 
+    fun relationCannotFindJunctionEntityField(
+        entityName: String,
+        columnName: String,
+        availableColumns: List<String>
+    ): String {
+        return "Cannot find the child entity referencing column `$columnName` in the junction " +
+                "$entityName. Options: ${availableColumns.joinToString(", ")}"
+    }
+
+    fun relationCannotFindJunctionParentField(
+        entityName: String,
+        columnName: String,
+        availableColumns: List<String>
+    ): String {
+        return "Cannot find the parent entity referencing column `$columnName` in the junction " +
+                "$entityName. Options: ${availableColumns.joinToString(", ")}"
+    }
+
+    fun junctionColumnWithoutIndex(entityName: String, columnName: String) =
+            "The column $columnName in the junction entity $entityName is being used to resolve " +
+                    "a relationship but it is not covered by any index. This might cause a " +
+                    "full table scan when resolving the relationship, it is highly advised to " +
+                    "create an index that covers this column."
+
     val RELATION_IN_ENTITY = "Entities cannot have relations."
 
     val CANNOT_FIND_TYPE = "Cannot find type."
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/Junction.kt b/room/compiler/src/main/kotlin/androidx/room/vo/Junction.kt
new file mode 100644
index 0000000..d07bc1d
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/Junction.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2019 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.vo
+
+/**
+ * Value object defining a junction table for a [Relation].
+ */
+data class Junction(
+    val entity: EntityOrView,
+    val parentField: Field,
+    val entityField: Field
+)
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/Relation.kt b/room/compiler/src/main/kotlin/androidx/room/vo/Relation.kt
index 562b376..233a707 100644
--- a/room/compiler/src/main/kotlin/androidx/room/vo/Relation.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/Relation.kt
@@ -33,20 +33,33 @@
     // the field referenced for querying. does not need to be in the response but the query
     // we generate always has it in the response.
     val entityField: Field,
+    // Used for joining on a many-to-many relation
+    val junction: Junction?,
     // the projection for the query
     val projection: List<String>
 ) {
-
     val pojoTypeName by lazy { pojoType.typeName() }
 
     fun createLoadAllSql(): String {
-        val resultFields = projection.toSet() + entityField.columnName
+        val resultFields = projection.toSet()
         return createSelect(resultFields)
     }
 
-    private fun createSelect(resultFields: Set<String>): String {
-        return "SELECT ${resultFields.joinToString(",") {"`$it`"}}" +
-                " FROM `${entity.tableName}`" +
-                " WHERE `${entityField.columnName}` IN (:args)"
+    private fun createSelect(resultFields: Set<String>) = buildString {
+        if (junction != null) {
+            val resultColumns = resultFields.map { "`${entity.tableName}`.`$it` AS `$it`" } +
+                    "_junction.`${junction.parentField.columnName}`"
+            append("SELECT ${resultColumns.joinToString(",")}")
+            append(" FROM `${junction.entity.tableName}` AS _junction")
+            append(" INNER JOIN `${entity.tableName}` ON" +
+                    " (_junction.`${junction.entityField.columnName}`" +
+                    " = `${entity.tableName}`.`${entityField.columnName}`)")
+            append(" WHERE _junction.`${junction.parentField.columnName}` IN (:args)")
+        } else {
+            val resultColumns = resultFields.map { "`$it`" }.toSet() + "`${entityField.columnName}`"
+            append("SELECT ${resultColumns.joinToString(",")}")
+            append(" FROM `${entity.tableName}`")
+            append(" WHERE `${entityField.columnName}` IN (:args)")
+        }
     }
 }
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/Warning.kt b/room/compiler/src/main/kotlin/androidx/room/vo/Warning.kt
index c92ce97..0abd15e9 100644
--- a/room/compiler/src/main/kotlin/androidx/room/vo/Warning.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/Warning.kt
@@ -35,6 +35,7 @@
     RELATION_QUERY_WITHOUT_TRANSACTION("ROOM_RELATION_QUERY_WITHOUT_TRANSACTION"),
     DEFAULT_CONSTRUCTOR("ROOM_DEFAULT_CONSTRUCTOR"),
     MISSING_COPY_ANNOTATIONS("MISSING_COPY_ANNOTATIONS"),
+    MISSING_INDEX_ON_JUNCTION("MISSING_INDEX_ON_JUNCTION"),
     JDK_VERSION_HAS_BUG("JDK_VERSION_HAS_BUG");
 
     companion object {
diff --git a/room/compiler/src/main/kotlin/androidx/room/writer/RelationCollectorMethodWriter.kt b/room/compiler/src/main/kotlin/androidx/room/writer/RelationCollectorMethodWriter.kt
index b4860c4..be346d1 100644
--- a/room/compiler/src/main/kotlin/androidx/room/writer/RelationCollectorMethodWriter.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/writer/RelationCollectorMethodWriter.kt
@@ -142,9 +142,20 @@
                     if (shouldCopyCursor) "true" else "false")
 
             beginControlFlow("try").apply {
-                addStatement("final $T $L = $T.getColumnIndex($L, $S)",
-                    TypeName.INT, itemKeyIndexVar, RoomTypeNames.CURSOR_UTIL, cursorVar,
-                    relation.entityField.columnName)
+                if (relation.junction != null) {
+                    // when using a junction table the relationship map is keyed on the parent
+                    // reference column of the junction table, the same column used in the WHERE IN
+                    // clause, this column is the rightmost column in the generated SELECT
+                    // clause.
+                    val junctionParentColumnIndex = relation.projection.size
+                    addStatement("final $T $L = $L; // $L",
+                        TypeName.INT, itemKeyIndexVar, junctionParentColumnIndex,
+                        relation.junction.parentField.columnName)
+                } else {
+                    addStatement("final $T $L = $T.getColumnIndex($L, $S)",
+                        TypeName.INT, itemKeyIndexVar, RoomTypeNames.CURSOR_UTIL, cursorVar,
+                        relation.entityField.columnName)
+                }
 
                 beginControlFlow("if ($L == -1)", itemKeyIndexVar).apply {
                     addStatement("return")
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/PojoProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/PojoProcessorTest.kt
index 4c860dc..f6e8e50 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/PojoProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/PojoProcessorTest.kt
@@ -23,7 +23,10 @@
 import androidx.room.processor.ProcessorErrors.CANNOT_FIND_TYPE
 import androidx.room.processor.ProcessorErrors.POJO_FIELD_HAS_DUPLICATE_COLUMN_NAME
 import androidx.room.processor.ProcessorErrors.RELATION_NOT_COLLECTION
+import androidx.room.processor.ProcessorErrors.junctionColumnWithoutIndex
 import androidx.room.processor.ProcessorErrors.relationCannotFindEntityField
+import androidx.room.processor.ProcessorErrors.relationCannotFindJunctionEntityField
+import androidx.room.processor.ProcessorErrors.relationCannotFindJunctionParentField
 import androidx.room.processor.ProcessorErrors.relationCannotFindParentEntityField
 import androidx.room.testing.TestInvocation
 import androidx.room.vo.CallType
@@ -577,6 +580,266 @@
     }
 
     @Test
+    fun relation_associateBy() {
+        val junctionEntity = """
+            package foo.bar;
+
+            import androidx.room.*;
+
+            @Entity(
+                primaryKeys = {"uid","friendId"},
+                foreignKeys = {
+                    @ForeignKey(
+                            entity = User.class,
+                            parentColumns = "uid",
+                            childColumns = "uid",
+                            onDelete = ForeignKey.CASCADE),
+                    @ForeignKey(
+                            entity = User.class,
+                            parentColumns = "uid",
+                            childColumns = "friendId",
+                            onDelete = ForeignKey.CASCADE),
+                },
+                indices = { @Index("uid"), @Index("friendId") }
+            )
+            public class UserFriendsXRef {
+                public int uid;
+                public int friendId;
+            }
+        """.toJFO("foo.bar.UserFriendsXRef")
+        singleRun(
+            """
+                int id;
+                @Relation(
+                    parentColumn = "id", entityColumn = "uid",
+                    associateBy = @Junction(
+                        value = UserFriendsXRef.class,
+                        parentColumn = "uid", entityColumn = "friendId")
+                )
+                public List<User> user;
+                """, COMMON.USER, junctionEntity
+        ) { pojo ->
+            assertThat(pojo.relations.size, `is`(1))
+            assertThat(pojo.relations.first().junction, notNullValue())
+            assertThat(pojo.relations.first().junction!!.parentField.columnName,
+                `is`("uid"))
+            assertThat(pojo.relations.first().junction!!.entityField.columnName,
+                `is`("friendId"))
+        }.compilesWithoutError().withWarningCount(0)
+    }
+
+    @Test
+    fun relation_associateBy_withView() {
+        val junctionEntity = """
+            package foo.bar;
+
+            import androidx.room.*;
+
+            @DatabaseView("SELECT 1, 2, FROM User")
+            public class UserFriendsXRefView {
+                public int uid;
+                public int friendId;
+            }
+        """.toJFO("foo.bar.UserFriendsXRefView")
+        singleRun(
+            """
+                int id;
+                @Relation(
+                    parentColumn = "id", entityColumn = "uid",
+                    associateBy = @Junction(
+                        value = UserFriendsXRefView.class,
+                        parentColumn = "uid", entityColumn = "friendId")
+                )
+                public List<User> user;
+                """, COMMON.USER, junctionEntity
+        ) { _ ->
+        }.compilesWithoutError().withWarningCount(0)
+    }
+
+    @Test
+    fun relation_associateBy_defaultColumns() {
+        val junctionEntity = """
+            package foo.bar;
+
+            import androidx.room.*;
+
+            @Entity(
+                primaryKeys = {"uid","friendId"},
+                foreignKeys = {
+                    @ForeignKey(
+                            entity = User.class,
+                            parentColumns = "uid",
+                            childColumns = "uid",
+                            onDelete = ForeignKey.CASCADE),
+                    @ForeignKey(
+                            entity = User.class,
+                            parentColumns = "uid",
+                            childColumns = "friendId",
+                            onDelete = ForeignKey.CASCADE),
+                },
+                indices = { @Index("uid"), @Index("friendId") }
+            )
+            public class UserFriendsXRef {
+                public int uid;
+                public int friendId;
+            }
+        """.toJFO("foo.bar.UserFriendsXRef")
+        singleRun(
+            """
+                int friendId;
+                @Relation(
+                    parentColumn = "friendId", entityColumn = "uid",
+                    associateBy = @Junction(UserFriendsXRef.class))
+                public List<User> user;
+                """, COMMON.USER, junctionEntity
+        ) { _ ->
+        }.compilesWithoutError().withWarningCount(0)
+    }
+
+    @Test
+    fun relation_associateBy_missingParentColumn() {
+        val junctionEntity = """
+            package foo.bar;
+
+            import androidx.room.*;
+
+            @Entity(primaryKeys = {"friendFrom","uid"})
+            public class UserFriendsXRef {
+                public int friendFrom;
+                public int uid;
+            }
+        """.toJFO("foo.bar.UserFriendsXRef")
+        singleRun(
+            """
+                int id;
+                @Relation(
+                    parentColumn = "id", entityColumn = "uid",
+                    associateBy = @Junction(UserFriendsXRef.class)
+                )
+                public List<User> user;
+                """, COMMON.USER, junctionEntity
+        ) { _ ->
+        }.failsToCompile().withErrorContaining(relationCannotFindJunctionParentField(
+            "foo.bar.UserFriendsXRef", "id", listOf("friendFrom", "uid")))
+    }
+
+    @Test
+    fun relation_associateBy_missingEntityColumn() {
+        val junctionEntity = """
+            package foo.bar;
+
+            import androidx.room.*;
+
+            @Entity(primaryKeys = {"friendA","friendB"})
+            public class UserFriendsXRef {
+                public int friendA;
+                public int friendB;
+            }
+        """.toJFO("foo.bar.UserFriendsXRef")
+        singleRun(
+            """
+                int friendA;
+                @Relation(
+                    parentColumn = "friendA", entityColumn = "uid",
+                    associateBy = @Junction(UserFriendsXRef.class)
+                )
+                public List<User> user;
+                """, COMMON.USER, junctionEntity
+        ) { _ ->
+        }.failsToCompile().withErrorContaining(relationCannotFindJunctionEntityField(
+            "foo.bar.UserFriendsXRef", "uid", listOf("friendA", "friendB"))
+        )
+    }
+
+    @Test
+    fun relation_associateBy_missingSpecifiedParentColumn() {
+        val junctionEntity = """
+            package foo.bar;
+
+            import androidx.room.*;
+
+            @Entity(primaryKeys = {"friendA","friendB"})
+            public class UserFriendsXRef {
+                public int friendA;
+                public int friendB;
+            }
+        """.toJFO("foo.bar.UserFriendsXRef")
+        singleRun(
+            """
+                int friendA;
+                @Relation(
+                    parentColumn = "friendA", entityColumn = "uid",
+                    associateBy = @Junction(
+                        value = UserFriendsXRef.class,
+                        parentColumn = "bad_col")
+                )
+                public List<User> user;
+                """, COMMON.USER, junctionEntity
+        ) { _ ->
+        }.failsToCompile().withErrorContaining(relationCannotFindJunctionParentField(
+            "foo.bar.UserFriendsXRef", "bad_col", listOf("friendA", "friendB"))
+        )
+    }
+
+    @Test
+    fun relation_associateBy_missingSpecifiedEntityColumn() {
+        val junctionEntity = """
+            package foo.bar;
+
+            import androidx.room.*;
+
+            @Entity(primaryKeys = {"friendA","friendB"})
+            public class UserFriendsXRef {
+                public int friendA;
+                public int friendB;
+            }
+        """.toJFO("foo.bar.UserFriendsXRef")
+        singleRun(
+            """
+                int friendA;
+                @Relation(
+                    parentColumn = "friendA", entityColumn = "uid",
+                    associateBy = @Junction(
+                        value = UserFriendsXRef.class,
+                        entityColumn = "bad_col")
+                )
+                public List<User> user;
+                """, COMMON.USER, junctionEntity
+        ) { _ ->
+        }.failsToCompile().withErrorContaining(relationCannotFindJunctionEntityField(
+            "foo.bar.UserFriendsXRef", "bad_col", listOf("friendA", "friendB"))
+        )
+    }
+
+    @Test
+    fun relation_associateBy_warnIndexOnJunctionColumn() {
+        val junctionEntity = """
+            package foo.bar;
+
+            import androidx.room.*;
+
+            @Entity
+            public class UserFriendsXRef {
+                @PrimaryKey(autoGenerate = true)
+                public long rowid;
+                public int uid;
+                public int friendId;
+            }
+        """.toJFO("foo.bar.UserFriendsXRef")
+        singleRun(
+            """
+                int friendId;
+                @Relation(
+                    parentColumn = "friendId", entityColumn = "uid",
+                    associateBy = @Junction(UserFriendsXRef.class))
+                public List<User> user;
+                """, COMMON.USER, junctionEntity
+        ) { _ ->
+        }.compilesWithoutError().withWarningCount(2).withWarningContaining(
+                junctionColumnWithoutIndex("foo.bar.UserFriendsXRef", "uid"))
+    }
+
+    @Test
     fun cache() {
         val pojo = """
             $HEADER
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/MusicTestDatabase.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/MusicTestDatabase.java
new file mode 100644
index 0000000..22917de
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/MusicTestDatabase.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2019 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.integration.testapp;
+
+import androidx.room.Database;
+import androidx.room.RoomDatabase;
+import androidx.room.integration.testapp.dao.MusicDao;
+import androidx.room.integration.testapp.vo.Playlist;
+import androidx.room.integration.testapp.vo.PlaylistMultiSongXRefView;
+import androidx.room.integration.testapp.vo.PlaylistSongXRef;
+import androidx.room.integration.testapp.vo.Song;
+
+@Database(
+        entities = {Song.class, Playlist.class, PlaylistSongXRef.class},
+        views = {PlaylistMultiSongXRefView.class},
+        version = 1,
+        exportSchema = false)
+public abstract class MusicTestDatabase extends RoomDatabase {
+    public abstract MusicDao getDao();
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/TestDatabase.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/TestDatabase.java
index 3dace5b..a6c6aba 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/TestDatabase.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/TestDatabase.java
@@ -35,6 +35,7 @@
 import androidx.room.integration.testapp.dao.WithClauseDao;
 import androidx.room.integration.testapp.vo.BlobEntity;
 import androidx.room.integration.testapp.vo.Day;
+import androidx.room.integration.testapp.vo.FriendsJunction;
 import androidx.room.integration.testapp.vo.FunnyNamedEntity;
 import androidx.room.integration.testapp.vo.House;
 import androidx.room.integration.testapp.vo.Pet;
@@ -50,7 +51,8 @@
 import java.util.Set;
 
 @Database(entities = {User.class, Pet.class, School.class, PetCouple.class, Toy.class,
-        BlobEntity.class, Product.class, FunnyNamedEntity.class, House.class},
+        BlobEntity.class, Product.class, FunnyNamedEntity.class, House.class,
+        FriendsJunction.class},
         views = {PetWithUser.class},
         version = 1, exportSchema = false)
 @TypeConverters(TestDatabase.Converters.class)
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/MusicDao.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/MusicDao.java
new file mode 100644
index 0000000..957fbda
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/MusicDao.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2019 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.integration.testapp.dao;
+
+import androidx.lifecycle.LiveData;
+import androidx.room.Dao;
+import androidx.room.Insert;
+import androidx.room.Query;
+import androidx.room.Transaction;
+import androidx.room.integration.testapp.vo.MultiSongPlaylistWithSongs;
+import androidx.room.integration.testapp.vo.Playlist;
+import androidx.room.integration.testapp.vo.PlaylistSongXRef;
+import androidx.room.integration.testapp.vo.PlaylistWithSongTitles;
+import androidx.room.integration.testapp.vo.PlaylistWithSongs;
+import androidx.room.integration.testapp.vo.Song;
+
+import java.util.List;
+
+@Dao
+public interface MusicDao {
+
+    @Insert
+    void addSongs(Song... songs);
+
+    @Insert
+    void addPlaylists(Playlist... playlists);
+
+    @Insert
+    void addPlaylistSongRelation(PlaylistSongXRef... relations);
+
+    @Transaction
+    @Query("SELECT * FROM Playlist")
+    List<PlaylistWithSongs> getAllPlaylistWithSongs();
+
+    @Transaction
+    @Query("SELECT * FROM Playlist WHERE mPlaylistId = :id")
+    LiveData<PlaylistWithSongs> getPlaylistsWithSongsLiveData(int id);
+
+    @Transaction
+    @Query("SELECT * FROM Playlist WHERE mPlaylistId = :playlistId")
+    PlaylistWithSongTitles getPlaylistWithSongTitles(int playlistId);
+
+    @Transaction
+    @Query("SELECT * FROM Playlist")
+    List<MultiSongPlaylistWithSongs> getAllMultiSongPlaylistWithSongs();
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/UserDao.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/UserDao.java
index 7422e71..1827eba 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/UserDao.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/UserDao.java
@@ -33,6 +33,7 @@
 import androidx.room.integration.testapp.vo.Day;
 import androidx.room.integration.testapp.vo.NameAndLastName;
 import androidx.room.integration.testapp.vo.User;
+import androidx.room.integration.testapp.vo.UserAndFriends;
 import androidx.room.integration.testapp.vo.UserSummary;
 import androidx.sqlite.db.SupportSQLiteQuery;
 
@@ -84,6 +85,9 @@
     @Query("select * from user where custommm = :customField")
     public abstract List<User> findByCustomField(String customField);
 
+    @Query("select * from user")
+    public abstract List<UserAndFriends> loadUserAndFriends();
+
     @Insert
     public abstract void insert(User user);
 
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/UserPetDao.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/UserPetDao.java
index f3b5ef3..14c3cc1 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/UserPetDao.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/UserPetDao.java
@@ -29,6 +29,7 @@
 import androidx.room.integration.testapp.vo.Pet;
 import androidx.room.integration.testapp.vo.User;
 import androidx.room.integration.testapp.vo.UserAndAllPets;
+import androidx.room.integration.testapp.vo.UserAndAllPetsViaJunction;
 import androidx.room.integration.testapp.vo.UserAndGenericPet;
 import androidx.room.integration.testapp.vo.UserAndPet;
 import androidx.room.integration.testapp.vo.UserAndPetAdoptionDates;
@@ -68,6 +69,10 @@
     @Query("SELECT * FROM User u")
     List<UserAndAllPets> loadAllUsersWithTheirPets();
 
+    @Transaction
+    @Query("SELECT * FROM User u")
+    List<UserAndAllPetsViaJunction> loadAllUsersWithTheirPetsViaJunction();
+
     @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
     @Transaction
     @Query("SELECT * FROM User u")
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/LiveDataQueryTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/LiveDataQueryTest.java
index 511f292..2978b70 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/LiveDataQueryTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/LiveDataQueryTest.java
@@ -32,13 +32,18 @@
 import androidx.room.InvalidationTrackerTrojan;
 import androidx.room.Room;
 import androidx.room.integration.testapp.FtsTestDatabase;
+import androidx.room.integration.testapp.MusicTestDatabase;
 import androidx.room.integration.testapp.dao.MailDao;
+import androidx.room.integration.testapp.dao.MusicDao;
 import androidx.room.integration.testapp.dao.SongDao;
 import androidx.room.integration.testapp.vo.AvgWeightByAge;
 import androidx.room.integration.testapp.vo.Mail;
 import androidx.room.integration.testapp.vo.Pet;
 import androidx.room.integration.testapp.vo.PetWithUser;
 import androidx.room.integration.testapp.vo.PetsToys;
+import androidx.room.integration.testapp.vo.Playlist;
+import androidx.room.integration.testapp.vo.PlaylistSongXRef;
+import androidx.room.integration.testapp.vo.PlaylistWithSongs;
 import androidx.room.integration.testapp.vo.Song;
 import androidx.room.integration.testapp.vo.SongDescription;
 import androidx.room.integration.testapp.vo.Toy;
@@ -267,6 +272,56 @@
     }
 
     @Test
+    public void withRelationAndJunction() throws ExecutionException, InterruptedException,
+            TimeoutException {
+        Context context = ApplicationProvider.getApplicationContext();
+        final MusicTestDatabase db = Room.inMemoryDatabaseBuilder(context, MusicTestDatabase.class)
+                .build();
+        final MusicDao musicDao = db.getDao();
+
+        final Song mSong1 = new Song(
+                1,
+                "I Know Places",
+                "Taylor Swift",
+                "1989",
+                195,
+                2014);
+        final Song mSong2 = new Song(
+                2,
+                "Blank Space",
+                "Taylor Swift",
+                "1989",
+                241,
+                2014);
+
+        final Playlist mPlaylist1 = new Playlist(1);
+        final Playlist mPlaylist2 = new Playlist(2);
+
+        musicDao.addSongs(mSong1, mSong2);
+        musicDao.addPlaylists(mPlaylist1, mPlaylist2);
+
+        musicDao.addPlaylistSongRelation(new PlaylistSongXRef(1, 1));
+
+        LiveData<PlaylistWithSongs> liveData = musicDao.getPlaylistsWithSongsLiveData(1);
+
+        final TestLifecycleOwner lifecycleOwner = new TestLifecycleOwner();
+        lifecycleOwner.handleEvent(Lifecycle.Event.ON_START);
+        final TestObserver<PlaylistWithSongs> observer = new MyTestObserver<>();
+        TestUtil.observeOnMainThread(liveData, lifecycleOwner, observer);
+
+        assertThat(observer.get().songs.size(), is(1));
+        assertThat(observer.get().songs.get(0), is(mSong1));
+
+        observer.reset();
+
+        musicDao.addPlaylistSongRelation(new PlaylistSongXRef(1, 2));
+
+        assertThat(observer.get().songs.size(), is(2));
+        assertThat(observer.get().songs.get(0), is(mSong1));
+        assertThat(observer.get().songs.get(1), is(mSong2));
+    }
+
+    @Test
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
     public void withWithClause() throws ExecutionException, InterruptedException,
             TimeoutException {
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/ManyToManyRelationTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/ManyToManyRelationTest.java
new file mode 100644
index 0000000..169a1f9
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/ManyToManyRelationTest.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2019 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.integration.testapp.test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+import android.content.Context;
+
+import androidx.room.Room;
+import androidx.room.integration.testapp.MusicTestDatabase;
+import androidx.room.integration.testapp.dao.MusicDao;
+import androidx.room.integration.testapp.vo.MultiSongPlaylistWithSongs;
+import androidx.room.integration.testapp.vo.Playlist;
+import androidx.room.integration.testapp.vo.PlaylistSongXRef;
+import androidx.room.integration.testapp.vo.PlaylistWithSongTitles;
+import androidx.room.integration.testapp.vo.PlaylistWithSongs;
+import androidx.room.integration.testapp.vo.Song;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class ManyToManyRelationTest {
+
+    private MusicDao mMusicDao;
+
+    private final Song mSong1 = new Song(
+            1,
+            "Desde el Corazón",
+            "Bad Bunny",
+            "BPPR Single",
+            127,
+            2019);
+    private final Song mSong2 = new Song(
+            2,
+            "Cambumbo",
+            "Tego Calderon",
+            "El Abayarde",
+            180,
+            2002);
+
+    private final Playlist mPlaylist1 = new Playlist(1);
+    private final Playlist mPlaylist2 = new Playlist(2);
+
+    @Before
+    public void createDb() {
+        Context context = ApplicationProvider.getApplicationContext();
+        MusicTestDatabase db = Room.inMemoryDatabaseBuilder(context, MusicTestDatabase.class)
+                .build();
+        mMusicDao = db.getDao();
+    }
+
+    @Test
+    public void relationWithJumpTable() {
+        mMusicDao.addSongs(mSong1, mSong2);
+        mMusicDao.addPlaylists(mPlaylist1, mPlaylist2);
+
+        mMusicDao.addPlaylistSongRelation(
+                new PlaylistSongXRef(1, 1),
+                new PlaylistSongXRef(1, 2),
+                new PlaylistSongXRef(2, 1)
+        );
+
+        List<PlaylistWithSongs> playlistWithSongs = mMusicDao.getAllPlaylistWithSongs();
+        assertThat(playlistWithSongs.size(), is(2));
+
+        assertThat(playlistWithSongs.get(0).playlist, is(mPlaylist1));
+        assertThat(playlistWithSongs.get(0).songs.size(), is(2));
+        assertThat(playlistWithSongs.get(0).songs.get(0), is(mSong1));
+        assertThat(playlistWithSongs.get(0).songs.get(1), is(mSong2));
+
+        assertThat(playlistWithSongs.get(1).playlist, is(mPlaylist2));
+        assertThat(playlistWithSongs.get(1).songs.size(), is(1));
+        assertThat(playlistWithSongs.get(1).songs.get(0), is(mSong1));
+    }
+
+    @Test
+    public void relationWithJumpTable_projection() {
+        mMusicDao.addSongs(mSong1, mSong2);
+        mMusicDao.addPlaylists(mPlaylist1, mPlaylist2);
+
+        mMusicDao.addPlaylistSongRelation(
+                new PlaylistSongXRef(1, 1),
+                new PlaylistSongXRef(1, 2),
+                new PlaylistSongXRef(2, 1)
+        );
+
+        PlaylistWithSongTitles playlistWithSongTitles = mMusicDao.getPlaylistWithSongTitles(1);
+
+        assertThat(playlistWithSongTitles.playlist, is(mPlaylist1));
+        assertThat(playlistWithSongTitles.titles.size(), is(2));
+        assertThat(playlistWithSongTitles.titles.get(0), is(mSong1.mTitle));
+        assertThat(playlistWithSongTitles.titles.get(1), is(mSong2.mTitle));
+    }
+
+    @Test
+    public void relationWithJumpView() {
+        mMusicDao.addSongs(mSong1, mSong2);
+        mMusicDao.addPlaylists(mPlaylist1, mPlaylist2);
+
+        mMusicDao.addPlaylistSongRelation(
+                new PlaylistSongXRef(1, 1),
+                new PlaylistSongXRef(1, 2),
+                new PlaylistSongXRef(2, 1)
+        );
+
+        List<MultiSongPlaylistWithSongs> playlistWithSongs =
+                mMusicDao.getAllMultiSongPlaylistWithSongs();
+        assertThat(playlistWithSongs.size(), is(2));
+
+        assertThat(playlistWithSongs.get(0).playlist, is(mPlaylist1));
+        assertThat(playlistWithSongs.get(0).songs.size(), is(2));
+        assertThat(playlistWithSongs.get(0).songs.get(0), is(mSong1));
+        assertThat(playlistWithSongs.get(0).songs.get(1), is(mSong2));
+
+        assertThat(playlistWithSongs.get(1).playlist, is(mPlaylist2));
+        assertThat(playlistWithSongs.get(1).songs.size(), is(0));
+    }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/PojoWithRelationTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/PojoWithRelationTest.java
index fd303aa..13fccd8 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/PojoWithRelationTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/PojoWithRelationTest.java
@@ -27,6 +27,7 @@
 import androidx.room.integration.testapp.vo.Toy;
 import androidx.room.integration.testapp.vo.User;
 import androidx.room.integration.testapp.vo.UserAndAllPets;
+import androidx.room.integration.testapp.vo.UserAndAllPetsViaJunction;
 import androidx.room.integration.testapp.vo.UserAndPetAdoptionDates;
 import androidx.room.integration.testapp.vo.UserAndPetsAndHouses;
 import androidx.room.integration.testapp.vo.UserIdAndPetNames;
@@ -261,4 +262,25 @@
             assertThat(result.get(i).getHouses(), is(houses.get(i)));
         }
     }
+
+    @Test
+    public void viaJunction() {
+        User[] users = TestUtil.createUsersArray(1, 2, 3);
+        Pet[][] userPets = new Pet[3][];
+        mUserDao.insertAll(users);
+        for (User user : users) {
+            Pet[] pets = TestUtil.createPetsForUser(user.getId(), user.getId() * 10,
+                    user.getId() - 1);
+            mPetDao.insertAll(pets);
+            userPets[user.getId() - 1] = pets;
+        }
+        List<UserAndAllPets> usersAndPets = mUserPetDao.loadAllUsersWithTheirPets();
+        List<UserAndAllPetsViaJunction> userAndPetsViaJunctions =
+                mUserPetDao.loadAllUsersWithTheirPetsViaJunction();
+        assertThat(usersAndPets.size(), is(userAndPetsViaJunctions.size()));
+        for (int i = 0; i < usersAndPets.size(); i++) {
+            assertThat(usersAndPets.get(i).user, is(userAndPetsViaJunctions.get(i).user));
+            assertThat(usersAndPets.get(i).pets, is(userAndPetsViaJunctions.get(i).pets));
+        }
+    }
 }
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/FriendsJunction.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/FriendsJunction.java
new file mode 100644
index 0000000..41e47b1
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/FriendsJunction.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2019 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.integration.testapp.vo;
+
+import static androidx.room.ForeignKey.CASCADE;
+
+import androidx.room.Entity;
+import androidx.room.ForeignKey;
+
+@Entity(
+        primaryKeys = {"friendA", "friendB"},
+        foreignKeys = {
+                @ForeignKey(
+                        entity = User.class,
+                        parentColumns = "mId",
+                        childColumns = "friendA",
+                        onDelete = CASCADE),
+                @ForeignKey(
+                        entity = User.class,
+                        parentColumns = "mId",
+                        childColumns = "friendB",
+                        onDelete = CASCADE),
+        })
+public class FriendsJunction {
+    public final int friendA;
+    public final int friendB;
+
+    public FriendsJunction(int friendA, int friendB) {
+        this.friendA = friendA;
+        this.friendB = friendB;
+    }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/MultiSongPlaylistWithSongs.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/MultiSongPlaylistWithSongs.java
new file mode 100644
index 0000000..91853bb
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/MultiSongPlaylistWithSongs.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2019 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.integration.testapp.vo;
+
+import androidx.room.Embedded;
+import androidx.room.Junction;
+import androidx.room.Relation;
+
+import java.util.List;
+
+public class MultiSongPlaylistWithSongs {
+    @Embedded
+    public Playlist playlist;
+    @Relation(
+            parentColumn = "mPlaylistId",
+            entity = Song.class,
+            entityColumn = "mSongId",
+            associateBy = @Junction(PlaylistMultiSongXRefView.class))
+    public List<Song> songs;
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/Playlist.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/Playlist.java
new file mode 100644
index 0000000..7c0fe62
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/Playlist.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2019 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.integration.testapp.vo;
+
+import androidx.room.Entity;
+import androidx.room.PrimaryKey;
+
+@Entity
+public class Playlist {
+    @PrimaryKey
+    public final int mPlaylistId;
+
+    public Playlist(int playlistId) {
+        mPlaylistId = playlistId;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        Playlist playlist = (Playlist) o;
+
+        return mPlaylistId == playlist.mPlaylistId;
+    }
+
+    @Override
+    public int hashCode() {
+        return mPlaylistId;
+    }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/PlaylistMultiSongXRefView.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/PlaylistMultiSongXRefView.java
new file mode 100644
index 0000000..067ca320
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/PlaylistMultiSongXRefView.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2019 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.integration.testapp.vo;
+
+import androidx.room.DatabaseView;
+
+// View of join table with playlists with more than 1 song
+@DatabaseView("SELECT * FROM PlaylistSongXRef WHERE mPlaylistId IN (SELECT mPlaylistId FROM"
+        + " PlaylistSongXRef GROUP BY mPlaylistId HAVING COUNT(mSongId) > 1)")
+public class PlaylistMultiSongXRefView {
+    public final int mPlaylistId;
+    public final int mSongId;
+
+    public PlaylistMultiSongXRefView(int playlistId, int songId) {
+        mPlaylistId = playlistId;
+        mSongId = songId;
+    }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/PlaylistSongXRef.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/PlaylistSongXRef.java
new file mode 100644
index 0000000..9f0e768
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/PlaylistSongXRef.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2019 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.integration.testapp.vo;
+
+import static androidx.room.ForeignKey.CASCADE;
+
+import androidx.room.Entity;
+import androidx.room.ForeignKey;
+
+@Entity(
+        primaryKeys = {"mPlaylistId", "mSongId"},
+        foreignKeys = {
+                @ForeignKey(
+                        entity = Playlist.class,
+                        parentColumns = "mPlaylistId",
+                        childColumns = "mPlaylistId",
+                        onDelete = CASCADE),
+                @ForeignKey(
+                        entity = Song.class,
+                        parentColumns = "mSongId",
+                        childColumns = "mSongId",
+                        onDelete = CASCADE),
+        }
+)
+public class PlaylistSongXRef {
+    public final int mPlaylistId;
+    public final int mSongId;
+
+    public PlaylistSongXRef(int playlistId, int songId) {
+        mPlaylistId = playlistId;
+        mSongId = songId;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        PlaylistSongXRef that = (PlaylistSongXRef) o;
+
+        if (mPlaylistId != that.mPlaylistId) return false;
+        return mSongId == that.mSongId;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = mPlaylistId;
+        result = 31 * result + mSongId;
+        return result;
+    }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/PlaylistWithSongTitles.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/PlaylistWithSongTitles.java
new file mode 100644
index 0000000..799c9e7b
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/PlaylistWithSongTitles.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2019 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.integration.testapp.vo;
+
+import androidx.room.Embedded;
+import androidx.room.Junction;
+import androidx.room.Relation;
+
+import java.util.List;
+
+public class PlaylistWithSongTitles {
+    @Embedded
+    public Playlist playlist;
+    @Relation(
+            parentColumn = "mPlaylistId",
+            entity = Song.class,
+            entityColumn = "mSongId",
+            associateBy = @Junction(PlaylistSongXRef.class),
+            projection = "mTitle")
+    public List<String> titles;
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/PlaylistWithSongs.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/PlaylistWithSongs.java
new file mode 100644
index 0000000..975cfc18
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/PlaylistWithSongs.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2019 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.integration.testapp.vo;
+
+import androidx.room.Embedded;
+import androidx.room.Junction;
+import androidx.room.Relation;
+
+import java.util.List;
+
+public class PlaylistWithSongs {
+    @Embedded
+    public Playlist playlist;
+    @Relation(
+            parentColumn = "mPlaylistId",
+            entity = Song.class,
+            entityColumn = "mSongId",
+            associateBy = @Junction(PlaylistSongXRef.class))
+    public List<Song> songs;
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/UserAndAllPetsViaJunction.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/UserAndAllPetsViaJunction.java
new file mode 100644
index 0000000..c9b244a
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/UserAndAllPetsViaJunction.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2019 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.integration.testapp.vo;
+
+import androidx.room.Embedded;
+import androidx.room.Junction;
+import androidx.room.Relation;
+
+import java.util.List;
+
+public class UserAndAllPetsViaJunction {
+    @Embedded
+    public User user;
+    @Relation(
+            parentColumn = "mId", entityColumn = "mPetId",
+            associateBy = @Junction(
+                    value = Pet.class,
+                    parentColumn = "mUserId",
+                    entityColumn = "mPetId"
+            )
+    )
+    public List<Pet> pets;
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/UserAndFriends.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/UserAndFriends.java
new file mode 100644
index 0000000..93b20c9
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/UserAndFriends.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2019 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.integration.testapp.vo;
+
+import androidx.room.Embedded;
+import androidx.room.Junction;
+import androidx.room.Relation;
+
+import java.util.List;
+
+public class UserAndFriends {
+    @Embedded
+    public final User user;
+
+    @Relation(
+            entity = User.class,
+            parentColumn = "mId",
+            entityColumn = "mId",
+            associateBy = @Junction(
+                    value = FriendsJunction.class,
+                    parentColumn = "friendA",
+                    entityColumn = "friendB")
+    )
+    public final List<User> friends;
+
+    public UserAndFriends(User user, List<User> friends) {
+        this.user = user;
+        this.friends = friends;
+    }
+}