Merge "Do not start a nested transaction on blocking DAO writes" into androidx-main
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/InTransactionTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/InTransactionTest.kt
index 112ff24..6bf6a0e 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/InTransactionTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/InTransactionTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.room.integration.kotlintestapp.test
 
+import android.database.sqlite.SQLiteTransactionListener
 import androidx.kruth.assertThat
 import androidx.room.Room
 import androidx.room.RoomDatabase
@@ -23,6 +24,7 @@
 import androidx.sqlite.db.SupportSQLiteDatabase
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.filters.SmallTest
+import kotlin.test.fail
 import org.junit.Test
 
 @SmallTest
@@ -46,4 +48,64 @@
         assertThat(database.inTransaction()).isFalse()
         assertThat(onOpenCalled).isEqualTo(0)
     }
+
+    @Test
+    fun beginTransactionWithListener_commit() {
+        val roomDb =
+            Room.inMemoryDatabaseBuilder<TestDatabase>(ApplicationProvider.getApplicationContext())
+                .build()
+        val supportDb = roomDb.openHelper.writableDatabase
+        supportDb.beginTransactionWithListenerNonExclusive(
+            object : SQLiteTransactionListener {
+                override fun onBegin() {
+                    // TODO(b/408279360): Assert this case once fixed in framework.
+                    // We do not assert that `inTransaction()` is true here because at the time this
+                    // callback is invoked the transaction stack has not been set and the API
+                    // will return false. Meanwhile starting a transaction here will fail due to
+                    // the recursive invocation of beginTransaction(), i.e. the database is
+                    // neither in a transaction nor once can be started at this time.
+                }
+
+                override fun onCommit() {
+                    assertThat(supportDb.inTransaction()).isTrue()
+                    roomDb.booksDao().insertPublisher("p1", "pub1")
+                }
+
+                override fun onRollback() {
+                    fail("onRollback should not be called")
+                }
+            }
+        )
+        supportDb.setTransactionSuccessful()
+        supportDb.endTransaction()
+
+        assertThat(roomDb.booksDao().getPublishers()).isNotEmpty()
+        roomDb.close()
+    }
+
+    @Test
+    fun beginTransactionWithListener_rollback() {
+        val roomDb =
+            Room.inMemoryDatabaseBuilder<TestDatabase>(ApplicationProvider.getApplicationContext())
+                .build()
+        val supportDb = roomDb.openHelper.writableDatabase
+        supportDb.beginTransactionWithListenerNonExclusive(
+            object : SQLiteTransactionListener {
+                override fun onBegin() {}
+
+                override fun onCommit() {
+                    fail("onCommit should not be called")
+                }
+
+                override fun onRollback() {
+                    assertThat(supportDb.inTransaction()).isTrue()
+                    roomDb.booksDao().insertPublisher("p1", "pub1")
+                }
+            }
+        )
+        supportDb.endTransaction()
+
+        assertThat(roomDb.booksDao().getPublishers()).isEmpty()
+        roomDb.close()
+    }
 }
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/util/DBUtil.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/util/DBUtil.android.kt
index a3f17aa..3b7d0df 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/util/DBUtil.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/util/DBUtil.android.kt
@@ -67,6 +67,10 @@
     db.assertNotMainThread()
     db.assertNotSuspendingTransaction()
     return runBlockingUninterruptible {
+        // If in compatibility mode and the database is already in a transaction, then do not
+        // start a nested transaction to avoid the overhead and because the SupportSQLite APIs
+        // do not support real SAVEPOINT-based nested transactions.
+        val inTransaction = !(db.inCompatibilityMode() && db.inTransaction()) && inTransaction
         db.internalPerform(isReadOnly, inTransaction) { connection ->
             val rawConnection = (connection as RawConnectionAccessor).rawConnection
             block.invoke(rawConnection)