Merge "Delete unused JDBCOpenHelper" into oc-mr1-support-27.0-dev
diff --git a/lifecycle/extensions/src/main/java/android/arch/lifecycle/MediatorLiveData.java b/lifecycle/extensions/src/main/java/android/arch/lifecycle/MediatorLiveData.java
index 672b3a3..718c79e 100644
--- a/lifecycle/extensions/src/main/java/android/arch/lifecycle/MediatorLiveData.java
+++ b/lifecycle/extensions/src/main/java/android/arch/lifecycle/MediatorLiveData.java
@@ -24,11 +24,43 @@
 import java.util.Map;
 
 /**
- * {@link LiveData} subclass which may observer other {@code LiveData} objects and react on
+ * {@link LiveData} subclass which may observe other {@code LiveData} objects and react on
  * {@code OnChanged} events from them.
  * <p>
  * This class correctly propagates its active/inactive states down to source {@code LiveData}
  * objects.
+ * <p>
+ * Consider the following scenario: we have 2 instances of {@code LiveData}, let's name them
+ * {@code liveData1} and {@code liveData2}, and we want to merge their emissions in one object:
+ * {@code liveDataMerger}. Then, {@code liveData1} and {@code liveData2} will become sources for
+ * the {@code MediatorLiveData liveDataMerger} and every time {@code onChanged} callback
+ * is called for either of them, we set a new value in {@code liveDataMerger}.
+ *
+ * <pre>
+ * LiveData<Integer> liveData1 = ...;
+ * LiveData<Integer> liveData2 = ...;
+ *
+ * MediatorLiveData<Integer> liveDataMerger = new MediatorLiveData<>();
+ * liveDataMerger.addSource(liveData1, value -> liveDataMerger.setValue(value));
+ * liveDataMerger.addSource(liveData2, value -> liveDataMerger.setValue(value));
+ * </pre>
+ * <p>
+ * Let's consider that we only want 10 values emitted by {@code liveData1}, to be
+ * merged in the {@code liveDataMerger}. Then, after 10 values, we can stop listening to {@code
+ * liveData1} and remove it as a source.
+ * <pre>
+ * liveDataMerger.addSource(liveData1, new Observer<Integer>() {
+ *      private int count = 1;
+ *
+ *      {@literal @}Override public void onChanged(@Nullable Integer s) {
+ *          count++;
+ *          liveDataMerger.setValue(s);
+ *          if (count > 10) {
+ *              liveDataMerger.removeSource(liveData1);
+ *          }
+ *      }
+ * });
+ * </pre>
  *
  * @param <T> The type of data hold by this instance
  */
diff --git a/room/common/src/main/java/android/arch/persistence/room/RoomWarnings.java b/room/common/src/main/java/android/arch/persistence/room/RoomWarnings.java
index c64be96..f05e6be 100644
--- a/room/common/src/main/java/android/arch/persistence/room/RoomWarnings.java
+++ b/room/common/src/main/java/android/arch/persistence/room/RoomWarnings.java
@@ -125,4 +125,12 @@
      * annotation.
      */
     public static final String DEFAULT_CONSTRUCTOR = "ROOM_DEFAULT_CONSTRUCTOR";
+
+    /**
+     * Reported when a @Query method returns a Pojo that has relations but the method is not
+     * annotated with @Transaction. Relations are run as separate queries and if the query is not
+     * run inside a transaction, it might return inconsistent results from the database.
+     */
+    public static final String RELATION_QUERY_WITHOUT_TRANSACTION =
+            "ROOM_RELATION_QUERY_WITHOUT_TRANSACTION";
 }
diff --git a/room/common/src/main/java/android/arch/persistence/room/Transaction.java b/room/common/src/main/java/android/arch/persistence/room/Transaction.java
index 914e4f4..3b6ede9 100644
--- a/room/common/src/main/java/android/arch/persistence/room/Transaction.java
+++ b/room/common/src/main/java/android/arch/persistence/room/Transaction.java
@@ -22,9 +22,10 @@
 import java.lang.annotation.Target;
 
 /**
- * Marks a method in an abstract {@link Dao} class as a transaction method.
+ * Marks a method in a {@link Dao} class as a transaction method.
  * <p>
- * The derived implementation of the method will execute the super method in a database transaction.
+ * When used on a non-abstract method of an abstract {@link Dao} class,
+ * the derived implementation of the method will execute the super method in a database transaction.
  * All the parameters and return types are preserved. The transaction will be marked as successful
  * unless an exception is thrown in the method body.
  * <p>
@@ -44,6 +45,38 @@
  *     }
  * }
  * </pre>
+ * <p>
+ * When used on a {@link Query} method that has a {@code Select} statement, the generated code for
+ * the Query will be run in a transaction. There are 2 main cases where you may want to do that:
+ * <ol>
+ *     <li>If the result of the query is fairly big, it is better to run it inside a transaction
+ *     to receive a consistent result. Otherwise, if the query result does not fit into a single
+ *     {@link android.database.CursorWindow CursorWindow}, the query result may be corrupted due to
+ *     changes in the database in between cursor window swaps.
+ *     <li>If the result of the query is a Pojo with {@link Relation} fields, these fields are
+ *     queried separately. To receive consistent results between these queries, you probably want
+ *     to run them in a single transaction.
+ * </ol>
+ * Example:
+ * <pre>
+ * class ProductWithReviews extends Product {
+ *     {@literal @}Relation(parentColumn = "id", entityColumn = "productId", entity = Review.class)
+ *     public List&lt;Review> reviews;
+ * }
+ * {@literal @}Dao
+ * public interface ProductDao {
+ *     {@literal @}Transaction {@literal @}Query("SELECT * from products")
+ *     public List&lt;ProductWithReviews> loadAll();
+ * }
+ * </pre>
+ * If the query is an async query (e.g. returns a {@link android.arch.lifecycle.LiveData LiveData}
+ * or RxJava Flowable, the transaction is properly handled when the query is run, not when the
+ * method is called.
+ * <p>
+ * Putting this annotation on an {@link Insert}, {@link Update} or {@link Delete} method has no
+ * impact because they are always run inside a transaction. Similarly, if it is annotated with
+ * {@link Query} but runs an update or delete statement, it is automatically wrapped in a
+ * transaction.
  */
 @Target({ElementType.METHOD})
 @Retention(RetentionPolicy.CLASS)
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/DaoProcessor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/DaoProcessor.kt
index 2093d91..c705eef 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/DaoProcessor.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/DaoProcessor.kt
@@ -110,9 +110,10 @@
                     executableElement = it).process()
         } ?: emptyList()
 
-        val transactionMethods = allMembers.filter {
-            it.hasAnnotation(Transaction::class)
-                    && it.kind == ElementKind.METHOD
+        val transactionMethods = allMembers.filter { member ->
+            member.hasAnnotation(Transaction::class)
+                    && member.kind == ElementKind.METHOD
+                    && PROCESSED_ANNOTATIONS.none { member.hasAnnotation(it) }
             // TODO: Exclude abstract methods and let @Query handle that case
         }.map {
             TransactionMethodProcessor(
@@ -161,5 +162,4 @@
                     element.toString(), dbType.toString()))
         }
     }
-
 }
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/ProcessorErrors.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/ProcessorErrors.kt
index be26297..4752478 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/ProcessorErrors.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/ProcessorErrors.kt
@@ -37,6 +37,8 @@
     val INVALID_ON_CONFLICT_VALUE = "On conflict value must be one of @OnConflictStrategy values."
     val INVALID_INSERTION_METHOD_RETURN_TYPE = "Methods annotated with @Insert can return either" +
             " void, long, Long, long[], Long[] or List<Long>."
+    val TRANSACTION_REFERENCE_DOCS = "https://developer.android.com/reference/android/arch/" +
+            "persistence/room/Transaction.html"
 
     fun insertionMethodReturnTypeMismatch(definedReturn : TypeName,
                                           expectedReturnTypes : List<TypeName>) : String {
@@ -122,7 +124,13 @@
             " @Update but does not have any parameters to update."
 
     val TRANSACTION_METHOD_MODIFIERS = "Method annotated with @Transaction must not be " +
-            "private, final, or abstract."
+            "private, final, or abstract. It can be abstract only if the method is also" +
+            " annotated with @Query."
+
+    val TRANSACTION_MISSING_ON_RELATION = "The return value includes a Pojo with a @Relation." +
+            " It is usually desired to annotate this method with @Transaction to avoid" +
+            " possibility of inconsistent results between the Pojo and its relations. See " +
+            TRANSACTION_REFERENCE_DOCS + " for details."
 
     val CANNOT_FIND_ENTITY_FOR_SHORTCUT_QUERY_PARAMETER = "Type of the parameter must be a class " +
             "annotated with @Entity or a collection/array of it."
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/QueryMethodProcessor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/QueryMethodProcessor.kt
index fc60074..ebd586a 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/QueryMethodProcessor.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/QueryMethodProcessor.kt
@@ -18,15 +18,18 @@
 
 import android.arch.persistence.room.Query
 import android.arch.persistence.room.SkipQueryVerification
+import android.arch.persistence.room.Transaction
 import android.arch.persistence.room.ext.hasAnnotation
 import android.arch.persistence.room.parser.ParsedQuery
 import android.arch.persistence.room.parser.QueryType
 import android.arch.persistence.room.parser.SqlParser
 import android.arch.persistence.room.solver.query.result.LiveDataQueryResultBinder
+import android.arch.persistence.room.solver.query.result.PojoRowAdapter
 import android.arch.persistence.room.verifier.DatabaseVerificaitonErrors
 import android.arch.persistence.room.verifier.DatabaseVerifier
 import android.arch.persistence.room.vo.QueryMethod
 import android.arch.persistence.room.vo.QueryParameter
+import android.arch.persistence.room.vo.Warning
 import com.google.auto.common.AnnotationMirrors
 import com.google.auto.common.MoreElements
 import com.google.auto.common.MoreTypes
@@ -91,6 +94,22 @@
                     ProcessorErrors.LIVE_DATA_QUERY_WITHOUT_SELECT)
         }
 
+        val inTransaction = when (query.type) {
+            QueryType.SELECT -> executableElement.hasAnnotation(Transaction::class)
+            else -> true
+        }
+
+        if (query.type == QueryType.SELECT && !inTransaction) {
+            // put a warning if it is has relations and not annotated w/ transaction
+            resultBinder.adapter?.rowAdapter?.let { rowAdapter ->
+                if (rowAdapter is PojoRowAdapter
+                        && rowAdapter.relationCollectors.isNotEmpty()) {
+                    context.logger.w(Warning.RELATION_QUERY_WITHOUT_TRANSACTION,
+                            executableElement, ProcessorErrors.TRANSACTION_MISSING_ON_RELATION)
+                }
+            }
+        }
+
         val queryMethod = QueryMethod(
                 element = executableElement,
                 query = query,
@@ -101,6 +120,7 @@
                                 baseContext = context,
                                 containing = containing,
                                 element = it).process() },
+                inTransaction = inTransaction,
                 queryResultBinder = resultBinder)
 
         val missing = queryMethod.sectionToParamMapping
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/TransactionMethodProcessor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/TransactionMethodProcessor.kt
index f861ebe..256304a 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/TransactionMethodProcessor.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/TransactionMethodProcessor.kt
@@ -20,9 +20,9 @@
 import android.arch.persistence.room.vo.TransactionMethod
 import javax.lang.model.element.ExecutableElement
 import javax.lang.model.element.Modifier.ABSTRACT
-import javax.lang.model.type.DeclaredType
 import javax.lang.model.element.Modifier.FINAL
 import javax.lang.model.element.Modifier.PRIVATE
+import javax.lang.model.type.DeclaredType
 
 class TransactionMethodProcessor(baseContext: Context,
                                  val containing: DeclaredType,
@@ -31,7 +31,6 @@
     val context = baseContext.fork(executableElement)
 
     fun process(): TransactionMethod {
-        // TODO: Remove abstract check
         context.checker.check(!executableElement.hasAnyOf(PRIVATE, FINAL, ABSTRACT),
                 executableElement, ProcessorErrors.TRANSACTION_METHOD_MODIFIERS)
 
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/RxCallableQueryResultBinderProvider.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/RxCallableQueryResultBinderProvider.kt
index 84c71eb..0bd3fb1 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/RxCallableQueryResultBinderProvider.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/RxCallableQueryResultBinderProvider.kt
@@ -38,8 +38,7 @@
     override fun provide(declared: DeclaredType, query: ParsedQuery): QueryResultBinder {
         val typeArg = declared.typeArguments.first()
         val adapter = context.typeAdapterStore.findQueryResultAdapter(typeArg, query)
-        return RxCallableQueryResultBinder(rxType,
-                typeArg, InstantQueryResultBinder(adapter), adapter)
+        return RxCallableQueryResultBinder(rxType, typeArg, adapter)
     }
 
     override fun matches(declared: DeclaredType): Boolean =
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/BaseObservableQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/BaseObservableQueryResultBinder.kt
index 7872c33..883180c 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/BaseObservableQueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/BaseObservableQueryResultBinder.kt
@@ -22,6 +22,7 @@
 import android.arch.persistence.room.ext.T
 import android.arch.persistence.room.solver.CodeGenScope
 import android.arch.persistence.room.writer.DaoWriter
+import com.squareup.javapoet.FieldSpec
 import com.squareup.javapoet.MethodSpec
 import javax.lang.model.element.Modifier
 
@@ -42,9 +43,17 @@
 
     protected fun createRunQueryAndReturnStatements(builder: MethodSpec.Builder,
                                                     roomSQLiteQueryVar: String,
+                                                    dbField: FieldSpec,
+                                                    inTransaction: Boolean,
                                                     scope: CodeGenScope) {
+        val transactionWrapper = if (inTransaction) {
+            builder.transactionWrapper(dbField)
+        } else {
+            null
+        }
         val outVar = scope.getTmpVar("_result")
         val cursorVar = scope.getTmpVar("_cursor")
+        transactionWrapper?.beginTransactionWithControlFlow()
         builder.apply {
             addStatement("final $T $L = $N.query($L)", AndroidTypeNames.CURSOR, cursorVar,
                     DaoWriter.dbField, roomSQLiteQueryVar)
@@ -52,6 +61,7 @@
                 val adapterScope = scope.fork()
                 adapter?.convert(outVar, cursorVar, adapterScope)
                 addCode(adapterScope.builder().build())
+                transactionWrapper?.commitTransaction()
                 addStatement("return $L", outVar)
             }
             nextControlFlow("finally").apply {
@@ -59,5 +69,6 @@
             }
             endControlFlow()
         }
+        transactionWrapper?.endTransactionWithControlFlow()
     }
 }
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/CursorQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/CursorQueryResultBinder.kt
index cd8ea74..06ec339 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/CursorQueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/CursorQueryResultBinder.kt
@@ -16,19 +16,34 @@
 
 package android.arch.persistence.room.solver.query.result
 
+import android.arch.persistence.room.ext.AndroidTypeNames
 import android.arch.persistence.room.ext.L
 import android.arch.persistence.room.ext.N
+import android.arch.persistence.room.ext.T
 import android.arch.persistence.room.solver.CodeGenScope
 import android.arch.persistence.room.writer.DaoWriter
 import com.squareup.javapoet.FieldSpec
 
 class CursorQueryResultBinder : QueryResultBinder(NO_OP_RESULT_ADAPTER) {
-    override fun convertAndReturn(roomSQLiteQueryVar: String, dbField: FieldSpec,
+    override fun convertAndReturn(roomSQLiteQueryVar: String,
+                                  dbField: FieldSpec,
+                                  inTransaction : Boolean,
                                   scope: CodeGenScope) {
-        scope.builder().apply {
-            addStatement("return $N.query($L)", DaoWriter.dbField, roomSQLiteQueryVar)
+        val builder = scope.builder()
+        val transactionWrapper = if (inTransaction) {
+            builder.transactionWrapper(dbField)
+        } else {
+            null
         }
+        transactionWrapper?.beginTransactionWithControlFlow()
+        val resultName = scope.getTmpVar("_tmpResult")
+        builder.addStatement("final $T $L = $N.query($L)", AndroidTypeNames.CURSOR, resultName,
+                dbField, roomSQLiteQueryVar)
+        transactionWrapper?.commitTransaction()
+        builder.addStatement("return $L", resultName)
+        transactionWrapper?.endTransactionWithControlFlow()
     }
+
     companion object {
         private val NO_OP_RESULT_ADAPTER = object : QueryResultAdapter(null) {
             override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/FlowableQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/FlowableQueryResultBinder.kt
index 5c8e3e7..e1a75b5 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/FlowableQueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/FlowableQueryResultBinder.kt
@@ -37,7 +37,9 @@
 class FlowableQueryResultBinder(val typeArg: TypeMirror, val queryTableNames: Set<String>,
                                 adapter: QueryResultAdapter?)
     : BaseObservableQueryResultBinder(adapter) {
-    override fun convertAndReturn(roomSQLiteQueryVar: String, dbField: FieldSpec,
+    override fun convertAndReturn(roomSQLiteQueryVar: String,
+                                  dbField: FieldSpec,
+                                  inTransaction : Boolean,
                                   scope: CodeGenScope) {
         val callableImpl = TypeSpec.anonymousClassBuilder("").apply {
             val typeName = typeArg.typeName()
@@ -47,7 +49,11 @@
                 returns(typeName)
                 addException(Exception::class.typeName())
                 addModifiers(Modifier.PUBLIC)
-                createRunQueryAndReturnStatements(this, roomSQLiteQueryVar, scope)
+                createRunQueryAndReturnStatements(builder = this,
+                        roomSQLiteQueryVar = roomSQLiteQueryVar,
+                        inTransaction = inTransaction,
+                        dbField = dbField,
+                        scope = scope)
             }.build())
             addMethod(createFinalizeMethod(roomSQLiteQueryVar))
         }.build()
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/InstantQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/InstantQueryResultBinder.kt
index ff15252..c18bf2d 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/InstantQueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/InstantQueryResultBinder.kt
@@ -27,8 +27,16 @@
  * Instantly runs and returns the query.
  */
 class InstantQueryResultBinder(adapter: QueryResultAdapter?) : QueryResultBinder(adapter) {
-    override fun convertAndReturn(roomSQLiteQueryVar : String, dbField: FieldSpec,
+    override fun convertAndReturn(roomSQLiteQueryVar : String,
+                                  dbField: FieldSpec,
+                                  inTransaction : Boolean,
                                   scope: CodeGenScope) {
+        val transactionWrapper = if (inTransaction) {
+            scope.builder().transactionWrapper(dbField)
+        } else {
+            null
+        }
+        transactionWrapper?.beginTransactionWithControlFlow()
         scope.builder().apply {
             val outVar = scope.getTmpVar("_result")
             val cursorVar = scope.getTmpVar("_cursor")
@@ -36,6 +44,7 @@
                     DaoWriter.dbField, roomSQLiteQueryVar)
             beginControlFlow("try").apply {
                 adapter?.convert(outVar, cursorVar, scope)
+                transactionWrapper?.commitTransaction()
                 addStatement("return $L", outVar)
             }
             nextControlFlow("finally").apply {
@@ -44,5 +53,6 @@
             }
             endControlFlow()
         }
+        transactionWrapper?.endTransactionWithControlFlow()
     }
 }
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/LiveDataQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/LiveDataQueryResultBinder.kt
index a385a94..0ef8a93 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/LiveDataQueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/LiveDataQueryResultBinder.kt
@@ -40,7 +40,9 @@
                                 adapter: QueryResultAdapter?)
     : BaseObservableQueryResultBinder(adapter) {
     @Suppress("JoinDeclarationAndAssignment")
-    override fun convertAndReturn(roomSQLiteQueryVar : String, dbField: FieldSpec,
+    override fun convertAndReturn(roomSQLiteQueryVar : String,
+                                  dbField: FieldSpec,
+                                  inTransaction : Boolean,
                                   scope: CodeGenScope) {
         val typeName = typeArg.typeName()
 
@@ -55,6 +57,7 @@
                     typeName = typeName,
                     roomSQLiteQueryVar = roomSQLiteQueryVar,
                     dbField = dbField,
+                    inTransaction = inTransaction,
                     scope = scope
             ))
             addMethod(createFinalizeMethod(roomSQLiteQueryVar))
@@ -66,6 +69,7 @@
 
     private fun createComputeMethod(roomSQLiteQueryVar: String, typeName: TypeName,
                                     observerField: FieldSpec, dbField: FieldSpec,
+                                    inTransaction: Boolean,
                                     scope: CodeGenScope): MethodSpec {
         return MethodSpec.methodBuilder("compute").apply {
             addAnnotation(Override::class.java)
@@ -79,7 +83,11 @@
             }
             endControlFlow()
 
-            createRunQueryAndReturnStatements(this, roomSQLiteQueryVar, scope)
+            createRunQueryAndReturnStatements(builder = this,
+                    roomSQLiteQueryVar = roomSQLiteQueryVar,
+                    dbField = dbField,
+                    inTransaction = inTransaction,
+                    scope = scope)
         }.build()
     }
 
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/LivePagedListQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/LivePagedListQueryResultBinder.kt
index 39ea32b..c10f9fb 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/LivePagedListQueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/LivePagedListQueryResultBinder.kt
@@ -31,14 +31,20 @@
     : QueryResultBinder(tiledDataSourceQueryResultBinder.listAdapter) {
     @Suppress("HasPlatformType")
     val typeName = tiledDataSourceQueryResultBinder.itemTypeName
-    override fun convertAndReturn(roomSQLiteQueryVar: String, dbField: FieldSpec,
+    override fun convertAndReturn(roomSQLiteQueryVar: String,
+                                  dbField: FieldSpec,
+                                  inTransaction : Boolean,
                                   scope: CodeGenScope) {
         scope.builder().apply {
             val pagedListProvider = TypeSpec
                     .anonymousClassBuilder("").apply {
                 superclass(ParameterizedTypeName.get(PagingTypeNames.LIVE_PAGED_LIST_PROVIDER,
                         Integer::class.typeName(), typeName))
-                addMethod(createCreateDataSourceMethod(roomSQLiteQueryVar, dbField, scope))
+                addMethod(createCreateDataSourceMethod(
+                        roomSQLiteQueryVar = roomSQLiteQueryVar,
+                        dbField = dbField,
+                        inTransaction = inTransaction,
+                        scope = scope))
             }.build()
             addStatement("return $L", pagedListProvider)
         }
@@ -46,14 +52,18 @@
 
     private fun createCreateDataSourceMethod(roomSQLiteQueryVar: String,
                                              dbField: FieldSpec,
+                                             inTransaction : Boolean,
                                              scope: CodeGenScope): MethodSpec
             = MethodSpec.methodBuilder("createDataSource").apply {
         addAnnotation(Override::class.java)
         addModifiers(Modifier.PROTECTED)
         returns(tiledDataSourceQueryResultBinder.typeName)
         val countedBinderScope = scope.fork()
-        tiledDataSourceQueryResultBinder.convertAndReturn(roomSQLiteQueryVar, dbField,
-                countedBinderScope)
+        tiledDataSourceQueryResultBinder.convertAndReturn(
+                roomSQLiteQueryVar = roomSQLiteQueryVar,
+                dbField = dbField,
+                inTransaction = inTransaction,
+                scope = countedBinderScope)
         addCode(countedBinderScope.builder().build())
     }.build()
 }
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/QueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/QueryResultBinder.kt
index 205bd88..652de46 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/QueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/QueryResultBinder.kt
@@ -32,5 +32,7 @@
      * and returns the result.
      */
     abstract fun convertAndReturn(roomSQLiteQueryVar: String,
-                                  dbField: FieldSpec, scope: CodeGenScope)
+                                  dbField: FieldSpec,
+                                  inTransaction : Boolean,
+                                  scope: CodeGenScope)
 }
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/RxCallableQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/RxCallableQueryResultBinder.kt
index da960f6..1ab91e8 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/RxCallableQueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/RxCallableQueryResultBinder.kt
@@ -38,28 +38,42 @@
  */
 class RxCallableQueryResultBinder(val rxType: RxType,
                                   val typeArg: TypeMirror,
-                                  val instantBinder : InstantQueryResultBinder,
-                                  adapter: QueryResultAdapter?) : QueryResultBinder(adapter) {
-    override fun convertAndReturn(roomSQLiteQueryVar: String, dbField: FieldSpec,
+                                  adapter: QueryResultAdapter?)
+    : QueryResultBinder(adapter) {
+    override fun convertAndReturn(roomSQLiteQueryVar: String,
+                                  dbField: FieldSpec,
+                                  inTransaction : Boolean,
                                   scope: CodeGenScope) {
         val callable = TypeSpec.anonymousClassBuilder("").apply {
             val typeName = typeArg.typeName()
             superclass(ParameterizedTypeName.get(java.util.concurrent.Callable::class.typeName(),
                     typeName))
-            addMethod(createCallMethod(roomSQLiteQueryVar, dbField, scope))
+            addMethod(createCallMethod(
+                    roomSQLiteQueryVar = roomSQLiteQueryVar,
+                    dbField = dbField,
+                    inTransaction = inTransaction,
+                    scope = scope))
         }.build()
         scope.builder().apply {
             addStatement("return $T.fromCallable($L)", rxType.className, callable)
         }
     }
 
-    fun createCallMethod(roomSQLiteQueryVar: String, dbField: FieldSpec,
+    fun createCallMethod(roomSQLiteQueryVar: String,
+                         dbField: FieldSpec,
+                         inTransaction: Boolean,
                          scope: CodeGenScope): MethodSpec {
         val adapterScope = scope.fork()
         return MethodSpec.methodBuilder("call").apply {
             returns(typeArg.typeName())
             addException(Exception::class.typeName())
             addModifiers(Modifier.PUBLIC)
+            val transactionWrapper = if (inTransaction) {
+                transactionWrapper(dbField)
+            } else {
+                null
+            }
+            transactionWrapper?.beginTransactionWithControlFlow()
             val outVar = scope.getTmpVar("_result")
             val cursorVar = scope.getTmpVar("_cursor")
             addStatement("final $T $L = $N.query($L)", AndroidTypeNames.CURSOR, cursorVar,
@@ -76,6 +90,7 @@
                     }
                     endControlFlow()
                 }
+                transactionWrapper?.commitTransaction()
                 addStatement("return $L", outVar)
             }
             nextControlFlow("finally").apply {
@@ -83,10 +98,11 @@
                 addStatement("$L.release()", roomSQLiteQueryVar)
             }
             endControlFlow()
+            transactionWrapper?.endTransactionWithControlFlow()
         }.build()
     }
 
-    enum class RxType(val className : ClassName, val canBeNull : Boolean) {
+    enum class RxType(val className: ClassName, val canBeNull: Boolean) {
         SINGLE(RxJava2TypeNames.SINGLE, canBeNull = false),
         MAYBE(RxJava2TypeNames.MAYBE, canBeNull = true);
     }
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/TiledDataSourceQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/TiledDataSourceQueryResultBinder.kt
index 1b4b3fe..2281cfb 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/TiledDataSourceQueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/TiledDataSourceQueryResultBinder.kt
@@ -37,11 +37,13 @@
     val itemTypeName : TypeName = listAdapter?.rowAdapter?.out?.typeName() ?: TypeName.OBJECT
     val typeName : ParameterizedTypeName = ParameterizedTypeName.get(
             RoomTypeNames.LIMIT_OFFSET_DATA_SOURCE, itemTypeName)
-    override fun convertAndReturn(roomSQLiteQueryVar: String, dbField: FieldSpec,
+    override fun convertAndReturn(roomSQLiteQueryVar: String,
+                                  dbField: FieldSpec,
+                                  inTransaction : Boolean,
                                   scope: CodeGenScope) {
         val tableNamesList = tableNames.joinToString(",") { "\"$it\"" }
-        val spec = TypeSpec.anonymousClassBuilder("$N, $L, $L",
-                dbField, roomSQLiteQueryVar, tableNamesList).apply {
+        val spec = TypeSpec.anonymousClassBuilder("$N, $L, $L, $L",
+                dbField, roomSQLiteQueryVar, inTransaction, tableNamesList).apply {
             superclass(typeName)
             addMethod(createConvertRowsMethod(scope))
         }.build()
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/TransactionWrapper.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/TransactionWrapper.kt
new file mode 100644
index 0000000..30d02ee
--- /dev/null
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/TransactionWrapper.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2017 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 android.arch.persistence.room.solver.query.result
+
+import android.arch.persistence.room.ext.N
+import com.squareup.javapoet.CodeBlock
+import com.squareup.javapoet.FieldSpec
+import com.squareup.javapoet.MethodSpec
+
+/**
+ * helper class to create correct transaction code.
+ */
+interface TransactionWrapper {
+    fun beginTransactionWithControlFlow()
+    fun commitTransaction()
+    fun endTransactionWithControlFlow()
+}
+
+fun MethodSpec.Builder.transactionWrapper(dbField : FieldSpec) = object : TransactionWrapper {
+    override fun beginTransactionWithControlFlow() {
+        addStatement("$N.beginTransaction()", dbField)
+        beginControlFlow("try")
+    }
+
+    override fun commitTransaction() {
+        addStatement("$N.setTransactionSuccessful()", dbField)
+    }
+
+    override fun endTransactionWithControlFlow() {
+        nextControlFlow("finally")
+        addStatement("$N.endTransaction()", dbField)
+        endControlFlow()
+    }
+}
+
+fun CodeBlock.Builder.transactionWrapper(dbField: FieldSpec) = object : TransactionWrapper {
+    override fun beginTransactionWithControlFlow() {
+        addStatement("$N.beginTransaction()", dbField)
+        beginControlFlow("try")
+    }
+
+    override fun commitTransaction() {
+        addStatement("$N.setTransactionSuccessful()", dbField)
+    }
+
+    override fun endTransactionWithControlFlow() {
+        nextControlFlow("finally")
+        addStatement("$N.endTransaction()", dbField)
+        endControlFlow()
+    }
+}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/QueryMethod.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/QueryMethod.kt
index 1aa8430..a6b8cb7 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/QueryMethod.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/QueryMethod.kt
@@ -29,6 +29,7 @@
  */
 data class QueryMethod(val element: ExecutableElement, val query: ParsedQuery, val name: String,
                        val returnType: TypeMirror, val parameters: List<QueryParameter>,
+                       val inTransaction : Boolean,
                        val queryResultBinder : QueryResultBinder) {
     val sectionToParamMapping by lazy {
         query.bindSections.map {
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Warning.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Warning.kt
index 91bcb33..dd154c4 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Warning.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Warning.kt
@@ -32,6 +32,7 @@
     RELATION_TYPE_MISMATCH("ROOM_RELATION_TYPE_MISMATCH"),
     MISSING_SCHEMA_LOCATION("ROOM_MISSING_SCHEMA_LOCATION"),
     MISSING_INDEX_ON_FOREIGN_KEY_CHILD("ROOM_MISSING_FOREIGN_KEY_CHILD_INDEX"),
+    RELATION_QUERY_WITHOUT_TRANSACTION("ROOM_RELATION_QUERY_WITHOUT_TRANSACTION"),
     DEFAULT_CONSTRUCTOR("ROOM_DEFAULT_CONSTRUCTOR");
 
     companion object {
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/DaoWriter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/DaoWriter.kt
index 5c7d8a3..0bb9c19 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/DaoWriter.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/DaoWriter.kt
@@ -445,7 +445,11 @@
         val sqlVar = scope.getTmpVar("_sql")
         val roomSQLiteQueryVar = scope.getTmpVar("_statement")
         queryWriter.prepareReadAndBind(sqlVar, roomSQLiteQueryVar, scope)
-        method.queryResultBinder.convertAndReturn(roomSQLiteQueryVar, dbField, scope)
+        method.queryResultBinder.convertAndReturn(
+                roomSQLiteQueryVar = roomSQLiteQueryVar,
+                dbField = dbField,
+                inTransaction = method.inTransaction,
+                scope = scope)
         return scope.builder().build()
     }
 
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/DaoProcessorTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/DaoProcessorTest.kt
index 3ade277..55f335d 100644
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/DaoProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/DaoProcessorTest.kt
@@ -192,6 +192,81 @@
         }.compilesWithoutError()
     }
 
+    @Test
+    fun query_warnIfTransactionIsMissingForRelation() {
+        if (!enableVerification) {
+            return
+        }
+        singleDao(
+                """
+                @Dao interface MyDao {
+                    static class Merged extends User {
+                       @Relation(parentColumn = "name", entityColumn = "lastName",
+                                 entity = User.class)
+                       java.util.List<User> users;
+                    }
+                    @Query("select * from user")
+                    abstract java.util.List<Merged> loadUsers();
+                }
+                """
+        ) { dao, _ ->
+            assertThat(dao.queryMethods.size, `is`(1))
+            assertThat(dao.queryMethods.first().inTransaction, `is`(false))
+        }.compilesWithoutError()
+                .withWarningContaining(ProcessorErrors.TRANSACTION_MISSING_ON_RELATION)
+    }
+
+    @Test
+    fun query_dontWarnIfTransactionIsMissingForRelation_suppressed() {
+        if (!enableVerification) {
+            return
+        }
+        singleDao(
+                """
+                @Dao interface MyDao {
+                    static class Merged extends User {
+                       @Relation(parentColumn = "name", entityColumn = "lastName",
+                                 entity = User.class)
+                       java.util.List<User> users;
+                    }
+                    @SuppressWarnings(RoomWarnings.RELATION_QUERY_WITHOUT_TRANSACTION)
+                    @Query("select * from user")
+                    abstract java.util.List<Merged> loadUsers();
+                }
+                """
+        ) { dao, _ ->
+            assertThat(dao.queryMethods.size, `is`(1))
+            assertThat(dao.queryMethods.first().inTransaction, `is`(false))
+        }.compilesWithoutError()
+                .withWarningCount(0)
+    }
+
+    @Test
+    fun query_dontWarnIfTransactionNotIsMissingForRelation() {
+        if (!enableVerification) {
+            return
+        }
+        singleDao(
+                """
+                @Dao interface MyDao {
+                    static class Merged extends User {
+                       @Relation(parentColumn = "name", entityColumn = "lastName",
+                                 entity = User.class)
+                       java.util.List<User> users;
+                    }
+                    @Transaction
+                    @Query("select * from user")
+                    abstract java.util.List<Merged> loadUsers();
+                }
+                """
+        ) { dao, _ ->
+            // test sanity
+            assertThat(dao.queryMethods.size, `is`(1))
+            assertThat(dao.queryMethods.first().inTransaction, `is`(true))
+        }.compilesWithoutError()
+                .withWarningCount(0)
+    }
+
     fun singleDao(vararg inputs: String, handler: (Dao, TestInvocation) -> Unit):
             CompileTester {
         return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
@@ -200,7 +275,12 @@
                 ), COMMON.USER))
                 .processedWith(TestProcessor.builder()
                         .forAnnotations(android.arch.persistence.room.Dao::class,
-                                android.arch.persistence.room.Entity::class)
+                                android.arch.persistence.room.Entity::class,
+                                android.arch.persistence.room.Relation::class,
+                                android.arch.persistence.room.Transaction::class,
+                                android.arch.persistence.room.ColumnInfo::class,
+                                android.arch.persistence.room.PrimaryKey::class,
+                                android.arch.persistence.room.Query::class)
                         .nextRunHandler { invocation ->
                             val dao = invocation.roundEnv
                                     .getElementsAnnotatedWith(
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/QueryMethodProcessorTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/QueryMethodProcessorTest.kt
index a2cb768..e07e04d 100644
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/QueryMethodProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/QueryMethodProcessorTest.kt
@@ -445,6 +445,55 @@
     }
 
     @Test
+    fun query_detectTransaction_delete() {
+        singleQueryMethod(
+                """
+                @Query("delete from user where uid = :id")
+                abstract int deleteUser(String id);
+                """
+        ) { method, _ ->
+            assertThat(method.inTransaction, `is`(true))
+        }.compilesWithoutError()
+    }
+
+    @Test
+    fun query_detectTransaction_update() {
+        singleQueryMethod(
+                """
+                @Query("UPDATE user set uid = :id + 1 where uid = :id")
+                abstract int incrementId(String id);
+                """
+        ) { method, _ ->
+            assertThat(method.inTransaction, `is`(true))
+        }.compilesWithoutError()
+    }
+
+    @Test
+    fun query_detectTransaction_select() {
+        singleQueryMethod(
+                """
+                @Query("select * from user")
+                abstract int loadUsers();
+                """
+        ) { method, _ ->
+            assertThat(method.inTransaction, `is`(false))
+        }.compilesWithoutError()
+    }
+
+    @Test
+    fun query_detectTransaction_selectInTransaction() {
+        singleQueryMethod(
+                """
+                @Transaction
+                @Query("select * from user")
+                abstract int loadUsers();
+                """
+        ) { method, _ ->
+            assertThat(method.inTransaction, `is`(true))
+        }.compilesWithoutError()
+    }
+
+    @Test
     fun skipVerification() {
         singleQueryMethod(
                 """
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/QueryTransactionTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/QueryTransactionTest.java
new file mode 100644
index 0000000..dcf98c9
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/QueryTransactionTest.java
@@ -0,0 +1,471 @@
+/*
+ * Copyright (C) 2017 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 android.arch.persistence.room.integration.testapp.test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.arch.core.executor.ArchTaskExecutor;
+import android.arch.core.executor.testing.CountingTaskExecutorRule;
+import android.arch.lifecycle.Lifecycle;
+import android.arch.lifecycle.LiveData;
+import android.arch.lifecycle.Observer;
+import android.arch.paging.LivePagedListProvider;
+import android.arch.paging.PagedList;
+import android.arch.paging.TiledDataSource;
+import android.arch.persistence.room.Dao;
+import android.arch.persistence.room.Database;
+import android.arch.persistence.room.Entity;
+import android.arch.persistence.room.Ignore;
+import android.arch.persistence.room.Insert;
+import android.arch.persistence.room.PrimaryKey;
+import android.arch.persistence.room.Query;
+import android.arch.persistence.room.Relation;
+import android.arch.persistence.room.Room;
+import android.arch.persistence.room.RoomDatabase;
+import android.arch.persistence.room.RoomWarnings;
+import android.arch.persistence.room.Transaction;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import io.reactivex.Flowable;
+import io.reactivex.Maybe;
+import io.reactivex.Single;
+import io.reactivex.observers.TestObserver;
+import io.reactivex.schedulers.Schedulers;
+import io.reactivex.subscribers.TestSubscriber;
+
+@SmallTest
+@RunWith(Parameterized.class)
+public class QueryTransactionTest {
+    @Rule
+    public CountingTaskExecutorRule countingTaskExecutorRule = new CountingTaskExecutorRule();
+    private static final AtomicInteger sStartedTransactionCount = new AtomicInteger(0);
+    private TransactionDb mDb;
+    private final boolean mUseTransactionDao;
+    private Entity1Dao mDao;
+    private final LiveDataQueryTest.TestLifecycleOwner mLifecycleOwner = new LiveDataQueryTest
+            .TestLifecycleOwner();
+
+    @NonNull
+    @Parameterized.Parameters(name = "useTransaction_{0}")
+    public static Boolean[] getParams() {
+        return new Boolean[]{false, true};
+    }
+
+    public QueryTransactionTest(boolean useTransactionDao) {
+        mUseTransactionDao = useTransactionDao;
+    }
+
+    @Before
+    public void initDb() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mLifecycleOwner.handleEvent(Lifecycle.Event.ON_START);
+            }
+        });
+
+        resetTransactionCount();
+        mDb = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getTargetContext(),
+                TransactionDb.class).build();
+        mDao = mUseTransactionDao ? mDb.transactionDao() : mDb.dao();
+        drain();
+    }
+
+    @After
+    public void closeDb() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mLifecycleOwner.handleEvent(Lifecycle.Event.ON_DESTROY);
+            }
+        });
+        drain();
+        mDb.close();
+    }
+
+    @Test
+    public void readList() {
+        mDao.insert(new Entity1(1, "foo"));
+        resetTransactionCount();
+
+        int expectedTransactionCount = mUseTransactionDao ? 1 : 0;
+        List<Entity1> allEntities = mDao.allEntities();
+        assertTransactionCount(allEntities, expectedTransactionCount);
+    }
+
+    @Test
+    public void liveData() {
+        LiveData<List<Entity1>> listLiveData = mDao.liveData();
+        observeForever(listLiveData);
+        drain();
+        assertThat(listLiveData.getValue(), is(Collections.<Entity1>emptyList()));
+
+        resetTransactionCount();
+        mDao.insert(new Entity1(1, "foo"));
+        drain();
+
+        //noinspection ConstantConditions
+        assertThat(listLiveData.getValue().size(), is(1));
+        int expectedTransactionCount = mUseTransactionDao ? 2 : 1;
+        assertTransactionCount(listLiveData.getValue(), expectedTransactionCount);
+    }
+
+    @Test
+    public void flowable() {
+        Flowable<List<Entity1>> flowable = mDao.flowable();
+        TestSubscriber<List<Entity1>> subscriber = observe(flowable);
+        drain();
+        assertThat(subscriber.values().size(), is(1));
+
+        resetTransactionCount();
+        mDao.insert(new Entity1(1, "foo"));
+        drain();
+
+        List<Entity1> allEntities = subscriber.values().get(1);
+        assertThat(allEntities.size(), is(1));
+        int expectedTransactionCount = mUseTransactionDao ? 2 : 1;
+        assertTransactionCount(allEntities, expectedTransactionCount);
+    }
+
+    @Test
+    public void maybe() {
+        mDao.insert(new Entity1(1, "foo"));
+        resetTransactionCount();
+
+        int expectedTransactionCount = mUseTransactionDao ? 1 : 0;
+        Maybe<List<Entity1>> listMaybe = mDao.maybe();
+        TestObserver<List<Entity1>> observer = observe(listMaybe);
+        drain();
+        List<Entity1> allEntities = observer.values().get(0);
+        assertTransactionCount(allEntities, expectedTransactionCount);
+    }
+
+    @Test
+    public void single() {
+        mDao.insert(new Entity1(1, "foo"));
+        resetTransactionCount();
+
+        int expectedTransactionCount = mUseTransactionDao ? 1 : 0;
+        Single<List<Entity1>> listMaybe = mDao.single();
+        TestObserver<List<Entity1>> observer = observe(listMaybe);
+        drain();
+        List<Entity1> allEntities = observer.values().get(0);
+        assertTransactionCount(allEntities, expectedTransactionCount);
+    }
+
+    @Test
+    public void relation() {
+        mDao.insert(new Entity1(1, "foo"));
+        mDao.insert(new Child(1, 1));
+        mDao.insert(new Child(2, 1));
+        resetTransactionCount();
+
+        List<Entity1WithChildren> result = mDao.withRelation();
+        int expectedTransactionCount = mUseTransactionDao ? 1 : 0;
+        assertTransactionCountWithChildren(result, expectedTransactionCount);
+    }
+
+    @Test
+    public void pagedList() {
+        LiveData<PagedList<Entity1>> pagedList = mDao.pagedList().create(null, 10);
+        observeForever(pagedList);
+        drain();
+        assertThat(sStartedTransactionCount.get(), is(mUseTransactionDao ? 1 : 0));
+
+        mDao.insert(new Entity1(1, "foo"));
+        drain();
+        //noinspection ConstantConditions
+        assertThat(pagedList.getValue().size(), is(1));
+        assertTransactionCount(pagedList.getValue(), mUseTransactionDao ? 3 : 1);
+
+        mDao.insert(new Entity1(2, "bar"));
+        drain();
+        assertThat(pagedList.getValue().size(), is(2));
+        assertTransactionCount(pagedList.getValue(), mUseTransactionDao ? 5 : 2);
+    }
+
+    @Test
+    public void dataSource() {
+        mDao.insert(new Entity1(2, "bar"));
+        drain();
+        resetTransactionCount();
+        TiledDataSource<Entity1> dataSource = mDao.dataSource();
+        dataSource.loadRange(0, 10);
+        assertThat(sStartedTransactionCount.get(), is(mUseTransactionDao ? 1 : 0));
+    }
+
+    private void assertTransactionCount(List<Entity1> allEntities, int expectedTransactionCount) {
+        assertThat(sStartedTransactionCount.get(), is(expectedTransactionCount));
+        assertThat(allEntities.isEmpty(), is(false));
+        for (Entity1 entity1 : allEntities) {
+            assertThat(entity1.transactionId, is(expectedTransactionCount));
+        }
+    }
+
+    private void assertTransactionCountWithChildren(List<Entity1WithChildren> allEntities,
+            int expectedTransactionCount) {
+        assertThat(sStartedTransactionCount.get(), is(expectedTransactionCount));
+        assertThat(allEntities.isEmpty(), is(false));
+        for (Entity1WithChildren entity1 : allEntities) {
+            assertThat(entity1.transactionId, is(expectedTransactionCount));
+            assertThat(entity1.children, notNullValue());
+            assertThat(entity1.children.isEmpty(), is(false));
+            for (Child child : entity1.children) {
+                assertThat(child.transactionId, is(expectedTransactionCount));
+            }
+        }
+    }
+
+    private void resetTransactionCount() {
+        sStartedTransactionCount.set(0);
+    }
+
+    private void drain() {
+        try {
+            countingTaskExecutorRule.drainTasks(30, TimeUnit.SECONDS);
+        } catch (InterruptedException e) {
+            throw new AssertionError("interrupted", e);
+        } catch (TimeoutException e) {
+            throw new AssertionError("drain timed out", e);
+        }
+    }
+
+    private <T> TestSubscriber<T> observe(final Flowable<T> flowable) {
+        TestSubscriber<T> subscriber = new TestSubscriber<>();
+        flowable.observeOn(Schedulers.from(ArchTaskExecutor.getMainThreadExecutor()))
+                .subscribeWith(subscriber);
+        return subscriber;
+    }
+
+    private <T> TestObserver<T> observe(final Maybe<T> maybe) {
+        TestObserver<T> observer = new TestObserver<>();
+        maybe.observeOn(Schedulers.from(ArchTaskExecutor.getMainThreadExecutor()))
+                .subscribeWith(observer);
+        return observer;
+    }
+
+    private <T> TestObserver<T> observe(final Single<T> single) {
+        TestObserver<T> observer = new TestObserver<>();
+        single.observeOn(Schedulers.from(ArchTaskExecutor.getMainThreadExecutor()))
+                .subscribeWith(observer);
+        return observer;
+    }
+
+    private <T> void observeForever(final LiveData<T> liveData) {
+        FutureTask<Void> futureTask = new FutureTask<>(new Callable<Void>() {
+            @Override
+            public Void call() throws Exception {
+                liveData.observe(mLifecycleOwner, new Observer<T>() {
+                    @Override
+                    public void onChanged(@Nullable T t) {
+
+                    }
+                });
+                return null;
+            }
+        });
+        ArchTaskExecutor.getMainThreadExecutor().execute(futureTask);
+        try {
+            futureTask.get();
+        } catch (InterruptedException e) {
+            throw new AssertionError("interrupted", e);
+        } catch (ExecutionException e) {
+            throw new AssertionError("execution error", e);
+        }
+    }
+
+    @SuppressWarnings("WeakerAccess")
+    static class Entity1WithChildren extends Entity1 {
+        @Relation(entity = Child.class, parentColumn = "id",
+                entityColumn = "entity1Id")
+        public List<Child> children;
+
+        Entity1WithChildren(int id, String value) {
+            super(id, value);
+        }
+    }
+
+    @SuppressWarnings("WeakerAccess")
+    @Entity
+    static class Child {
+        @PrimaryKey(autoGenerate = true)
+        public int id;
+        public int entity1Id;
+        @Ignore
+        public final int transactionId = sStartedTransactionCount.get();
+
+        Child(int id, int entity1Id) {
+            this.id = id;
+            this.entity1Id = entity1Id;
+        }
+    }
+
+    @SuppressWarnings("WeakerAccess")
+    @Entity
+    static class Entity1 {
+        @PrimaryKey(autoGenerate = true)
+        public int id;
+        public String value;
+        @Ignore
+        public final int transactionId = sStartedTransactionCount.get();
+
+        Entity1(int id, String value) {
+            this.id = id;
+            this.value = value;
+        }
+    }
+
+    // we don't support dao inheritance for queries so for now, go with this
+    interface Entity1Dao {
+        String SELECT_ALL = "select * from Entity1";
+
+        List<Entity1> allEntities();
+
+        Flowable<List<Entity1>> flowable();
+
+        Maybe<List<Entity1>> maybe();
+
+        Single<List<Entity1>> single();
+
+        LiveData<List<Entity1>> liveData();
+
+        List<Entity1WithChildren> withRelation();
+
+        LivePagedListProvider<Integer, Entity1> pagedList();
+
+        TiledDataSource<Entity1> dataSource();
+
+        @Insert
+        void insert(Entity1 entity1);
+
+        @Insert
+        void insert(Child entity1);
+    }
+
+    @Dao
+    interface EntityDao extends Entity1Dao {
+        @Override
+        @Query(SELECT_ALL)
+        List<Entity1> allEntities();
+
+        @Override
+        @Query(SELECT_ALL)
+        Flowable<List<Entity1>> flowable();
+
+        @Override
+        @Query(SELECT_ALL)
+        LiveData<List<Entity1>> liveData();
+
+        @Override
+        @Query(SELECT_ALL)
+        Maybe<List<Entity1>> maybe();
+
+        @Override
+        @Query(SELECT_ALL)
+        Single<List<Entity1>> single();
+
+        @Override
+        @Query(SELECT_ALL)
+        @SuppressWarnings(RoomWarnings.RELATION_QUERY_WITHOUT_TRANSACTION)
+        List<Entity1WithChildren> withRelation();
+
+        @Override
+        @Query(SELECT_ALL)
+        LivePagedListProvider<Integer, Entity1> pagedList();
+
+        @Override
+        @Query(SELECT_ALL)
+        TiledDataSource<Entity1> dataSource();
+    }
+
+    @Dao
+    interface TransactionDao extends Entity1Dao {
+        @Override
+        @Transaction
+        @Query(SELECT_ALL)
+        List<Entity1> allEntities();
+
+        @Override
+        @Transaction
+        @Query(SELECT_ALL)
+        Flowable<List<Entity1>> flowable();
+
+        @Override
+        @Transaction
+        @Query(SELECT_ALL)
+        LiveData<List<Entity1>> liveData();
+
+        @Override
+        @Transaction
+        @Query(SELECT_ALL)
+        Maybe<List<Entity1>> maybe();
+
+        @Override
+        @Transaction
+        @Query(SELECT_ALL)
+        Single<List<Entity1>> single();
+
+        @Override
+        @Transaction
+        @Query(SELECT_ALL)
+        List<Entity1WithChildren> withRelation();
+
+        @Override
+        @Transaction
+        @Query(SELECT_ALL)
+        LivePagedListProvider<Integer, Entity1> pagedList();
+
+        @Override
+        @Transaction
+        @Query(SELECT_ALL)
+        TiledDataSource<Entity1> dataSource();
+    }
+
+    @Database(version = 1, entities = {Entity1.class, Child.class}, exportSchema = false)
+    abstract static class TransactionDb extends RoomDatabase {
+        abstract EntityDao dao();
+
+        abstract TransactionDao transactionDao();
+
+        @Override
+        public void beginTransaction() {
+            super.beginTransaction();
+            sStartedTransactionCount.incrementAndGet();
+        }
+    }
+}
diff --git a/room/runtime/src/main/java/android/arch/persistence/room/paging/LimitOffsetDataSource.java b/room/runtime/src/main/java/android/arch/persistence/room/paging/LimitOffsetDataSource.java
index 800514c..2f9a888 100644
--- a/room/runtime/src/main/java/android/arch/persistence/room/paging/LimitOffsetDataSource.java
+++ b/room/runtime/src/main/java/android/arch/persistence/room/paging/LimitOffsetDataSource.java
@@ -49,10 +49,13 @@
     private final RoomDatabase mDb;
     @SuppressWarnings("FieldCanBeLocal")
     private final InvalidationTracker.Observer mObserver;
+    private final boolean mInTransaction;
 
-    protected LimitOffsetDataSource(RoomDatabase db, RoomSQLiteQuery query, String... tables) {
+    protected LimitOffsetDataSource(RoomDatabase db, RoomSQLiteQuery query,
+            boolean inTransaction, String... tables) {
         mDb = db;
         mSourceQuery = query;
+        mInTransaction = inTransaction;
         mCountQuery = "SELECT COUNT(*) FROM ( " + mSourceQuery.getSql() + " )";
         mLimitOffsetQuery = "SELECT * FROM ( " + mSourceQuery.getSql() + " ) LIMIT ? OFFSET ?";
         mObserver = new InvalidationTracker.Observer(tables) {
@@ -98,13 +101,30 @@
         sqLiteQuery.copyArgumentsFrom(mSourceQuery);
         sqLiteQuery.bindLong(sqLiteQuery.getArgCount() - 1, loadCount);
         sqLiteQuery.bindLong(sqLiteQuery.getArgCount(), startPosition);
-        Cursor cursor = mDb.query(sqLiteQuery);
-
-        try {
-            return convertRows(cursor);
-        } finally {
-            cursor.close();
-            sqLiteQuery.release();
+        if (mInTransaction) {
+            mDb.beginTransaction();
+            Cursor cursor = null;
+            try {
+                cursor = mDb.query(sqLiteQuery);
+                List<T> rows = convertRows(cursor);
+                mDb.setTransactionSuccessful();
+                return rows;
+            } finally {
+                if (cursor != null) {
+                    cursor.close();
+                }
+                mDb.endTransaction();
+                sqLiteQuery.release();
+            }
+        } else {
+            Cursor cursor = mDb.query(sqLiteQuery);
+            //noinspection TryFinallyCanBeTryWithResources
+            try {
+                return convertRows(cursor);
+            } finally {
+                cursor.close();
+                sqLiteQuery.release();
+            }
         }
     }
 }