Fix issues with reentrant SQLite triggers.

Modified SQLiteConnection and SQLiteSession to support
reentrant execution of SQLite operations, as might occur
when a custom function is invoked by a trigger.

Bug: 5884809
Change-Id: I253d828b2801bd06b1bbda7caa7da3f040a642bb
diff --git a/core/java/android/database/sqlite/SQLiteConnection.java b/core/java/android/database/sqlite/SQLiteConnection.java
index e45d66d..aeca62d 100644
--- a/core/java/android/database/sqlite/SQLiteConnection.java
+++ b/core/java/android/database/sqlite/SQLiteConnection.java
@@ -74,6 +74,12 @@
  * queues.
  * </p>
  *
+ * <h2>Reentrance</h2>
+ * <p>
+ * This class must tolerate reentrant execution of SQLite operations because
+ * triggers may call custom SQLite functions that perform additional queries.
+ * </p>
+ *
  * @hide
  */
 public final class SQLiteConnection {
@@ -205,13 +211,13 @@
         }
 
         if (mConnectionPtr != 0) {
-            mRecentOperations.beginOperation("close", null, null);
+            final int cookie = mRecentOperations.beginOperation("close", null, null);
             try {
                 mPreparedStatementCache.evictAll();
                 nativeClose(mConnectionPtr);
                 mConnectionPtr = 0;
             } finally {
-                mRecentOperations.endOperation();
+                mRecentOperations.endOperation(cookie);
             }
         }
     }
@@ -304,9 +310,9 @@
             throw new IllegalArgumentException("sql must not be null.");
         }
 
-        mRecentOperations.beginOperation("prepare", sql, null);
+        final int cookie = mRecentOperations.beginOperation("prepare", sql, null);
         try {
-            PreparedStatement statement = acquirePreparedStatement(sql);
+            final PreparedStatement statement = acquirePreparedStatement(sql);
             try {
                 if (outStatementInfo != null) {
                     outStatementInfo.numParameters = statement.mNumParameters;
@@ -328,10 +334,10 @@
                 releasePreparedStatement(statement);
             }
         } catch (RuntimeException ex) {
-            mRecentOperations.failOperation(ex);
+            mRecentOperations.failOperation(cookie, ex);
             throw ex;
         } finally {
-            mRecentOperations.endOperation();
+            mRecentOperations.endOperation(cookie);
         }
     }
 
@@ -349,9 +355,9 @@
             throw new IllegalArgumentException("sql must not be null.");
         }
 
-        mRecentOperations.beginOperation("execute", sql, bindArgs);
+        final int cookie = mRecentOperations.beginOperation("execute", sql, bindArgs);
         try {
-            PreparedStatement statement = acquirePreparedStatement(sql);
+            final PreparedStatement statement = acquirePreparedStatement(sql);
             try {
                 throwIfStatementForbidden(statement);
                 bindArguments(statement, bindArgs);
@@ -361,10 +367,10 @@
                 releasePreparedStatement(statement);
             }
         } catch (RuntimeException ex) {
-            mRecentOperations.failOperation(ex);
+            mRecentOperations.failOperation(cookie, ex);
             throw ex;
         } finally {
-            mRecentOperations.endOperation();
+            mRecentOperations.endOperation(cookie);
         }
     }
 
@@ -384,9 +390,9 @@
             throw new IllegalArgumentException("sql must not be null.");
         }
 
-        mRecentOperations.beginOperation("executeForLong", sql, bindArgs);
+        final int cookie = mRecentOperations.beginOperation("executeForLong", sql, bindArgs);
         try {
-            PreparedStatement statement = acquirePreparedStatement(sql);
+            final PreparedStatement statement = acquirePreparedStatement(sql);
             try {
                 throwIfStatementForbidden(statement);
                 bindArguments(statement, bindArgs);
@@ -396,10 +402,10 @@
                 releasePreparedStatement(statement);
             }
         } catch (RuntimeException ex) {
-            mRecentOperations.failOperation(ex);
+            mRecentOperations.failOperation(cookie, ex);
             throw ex;
         } finally {
-            mRecentOperations.endOperation();
+            mRecentOperations.endOperation(cookie);
         }
     }
 
@@ -419,9 +425,9 @@
             throw new IllegalArgumentException("sql must not be null.");
         }
 
-        mRecentOperations.beginOperation("executeForString", sql, bindArgs);
+        final int cookie = mRecentOperations.beginOperation("executeForString", sql, bindArgs);
         try {
-            PreparedStatement statement = acquirePreparedStatement(sql);
+            final PreparedStatement statement = acquirePreparedStatement(sql);
             try {
                 throwIfStatementForbidden(statement);
                 bindArguments(statement, bindArgs);
@@ -431,10 +437,10 @@
                 releasePreparedStatement(statement);
             }
         } catch (RuntimeException ex) {
-            mRecentOperations.failOperation(ex);
+            mRecentOperations.failOperation(cookie, ex);
             throw ex;
         } finally {
-            mRecentOperations.endOperation();
+            mRecentOperations.endOperation(cookie);
         }
     }
 
@@ -456,9 +462,10 @@
             throw new IllegalArgumentException("sql must not be null.");
         }
 
-        mRecentOperations.beginOperation("executeForBlobFileDescriptor", sql, bindArgs);
+        final int cookie = mRecentOperations.beginOperation("executeForBlobFileDescriptor",
+                sql, bindArgs);
         try {
-            PreparedStatement statement = acquirePreparedStatement(sql);
+            final PreparedStatement statement = acquirePreparedStatement(sql);
             try {
                 throwIfStatementForbidden(statement);
                 bindArguments(statement, bindArgs);
@@ -470,10 +477,10 @@
                 releasePreparedStatement(statement);
             }
         } catch (RuntimeException ex) {
-            mRecentOperations.failOperation(ex);
+            mRecentOperations.failOperation(cookie, ex);
             throw ex;
         } finally {
-            mRecentOperations.endOperation();
+            mRecentOperations.endOperation(cookie);
         }
     }
 
@@ -493,9 +500,10 @@
             throw new IllegalArgumentException("sql must not be null.");
         }
 
-        mRecentOperations.beginOperation("executeForChangedRowCount", sql, bindArgs);
+        final int cookie = mRecentOperations.beginOperation("executeForChangedRowCount",
+                sql, bindArgs);
         try {
-            PreparedStatement statement = acquirePreparedStatement(sql);
+            final PreparedStatement statement = acquirePreparedStatement(sql);
             try {
                 throwIfStatementForbidden(statement);
                 bindArguments(statement, bindArgs);
@@ -506,10 +514,10 @@
                 releasePreparedStatement(statement);
             }
         } catch (RuntimeException ex) {
-            mRecentOperations.failOperation(ex);
+            mRecentOperations.failOperation(cookie, ex);
             throw ex;
         } finally {
-            mRecentOperations.endOperation();
+            mRecentOperations.endOperation(cookie);
         }
     }
 
@@ -529,9 +537,10 @@
             throw new IllegalArgumentException("sql must not be null.");
         }
 
-        mRecentOperations.beginOperation("executeForLastInsertedRowId", sql, bindArgs);
+        final int cookie = mRecentOperations.beginOperation("executeForLastInsertedRowId",
+                sql, bindArgs);
         try {
-            PreparedStatement statement = acquirePreparedStatement(sql);
+            final PreparedStatement statement = acquirePreparedStatement(sql);
             try {
                 throwIfStatementForbidden(statement);
                 bindArguments(statement, bindArgs);
@@ -542,10 +551,10 @@
                 releasePreparedStatement(statement);
             }
         } catch (RuntimeException ex) {
-            mRecentOperations.failOperation(ex);
+            mRecentOperations.failOperation(cookie, ex);
             throw ex;
         } finally {
-            mRecentOperations.endOperation();
+            mRecentOperations.endOperation(cookie);
         }
     }
 
@@ -581,9 +590,10 @@
         int actualPos = -1;
         int countedRows = -1;
         int filledRows = -1;
-        mRecentOperations.beginOperation("executeForCursorWindow", sql, bindArgs);
+        final int cookie = mRecentOperations.beginOperation("executeForCursorWindow",
+                sql, bindArgs);
         try {
-            PreparedStatement statement = acquirePreparedStatement(sql);
+            final PreparedStatement statement = acquirePreparedStatement(sql);
             try {
                 throwIfStatementForbidden(statement);
                 bindArguments(statement, bindArgs);
@@ -600,11 +610,11 @@
                 releasePreparedStatement(statement);
             }
         } catch (RuntimeException ex) {
-            mRecentOperations.failOperation(ex);
+            mRecentOperations.failOperation(cookie, ex);
             throw ex;
         } finally {
-            if (mRecentOperations.endOperationDeferLog()) {
-                mRecentOperations.logOperation("window='" + window
+            if (mRecentOperations.endOperationDeferLog(cookie)) {
+                mRecentOperations.logOperation(cookie, "window='" + window
                         + "', startPos=" + startPos
                         + ", actualPos=" + actualPos
                         + ", filledRows=" + filledRows
@@ -615,8 +625,15 @@
 
     private PreparedStatement acquirePreparedStatement(String sql) {
         PreparedStatement statement = mPreparedStatementCache.get(sql);
+        boolean skipCache = false;
         if (statement != null) {
-            return statement;
+            if (!statement.mInUse) {
+                return statement;
+            }
+            // The statement is already in the cache but is in use (this statement appears
+            // to be not only re-entrant but recursive!).  So prepare a new copy of the
+            // statement but do not cache it.
+            skipCache = true;
         }
 
         final int statementPtr = nativePrepareStatement(mConnectionPtr, sql);
@@ -625,7 +642,7 @@
             final int type = DatabaseUtils.getSqlStatementType(sql);
             final boolean readOnly = nativeIsReadOnly(mConnectionPtr, statementPtr);
             statement = obtainPreparedStatement(sql, statementPtr, numParameters, type, readOnly);
-            if (isCacheable(type)) {
+            if (!skipCache && isCacheable(type)) {
                 mPreparedStatementCache.put(sql, statement);
                 statement.mInCache = true;
             }
@@ -637,18 +654,20 @@
             }
             throw ex;
         }
+        statement.mInUse = true;
         return statement;
     }
 
     private void releasePreparedStatement(PreparedStatement statement) {
+        statement.mInUse = false;
         if (statement.mInCache) {
             try {
                 nativeResetStatementAndClearBindings(mConnectionPtr, statement.mStatementPtr);
             } catch (SQLiteException ex) {
-                // The statement could not be reset due to an error.
-                // The entryRemoved() callback for the cache will recursively call
-                // releasePreparedStatement() again, but this time mInCache will be false
-                // so the statement will be finalized and recycled.
+                // The statement could not be reset due to an error.  Remove it from the cache.
+                // When remove() is called, the cache will invoke its entryRemoved() callback,
+                // which will in turn call finalizePreparedStatement() to finalize and
+                // recycle the statement.
                 if (SQLiteDebug.DEBUG_SQL_CACHE) {
                     Log.v(TAG, "Could not reset prepared statement due to an exception.  "
                             + "Removing it from the cache.  SQL: "
@@ -657,11 +676,15 @@
                 mPreparedStatementCache.remove(statement.mSql);
             }
         } else {
-            nativeFinalizeStatement(mConnectionPtr, statement.mStatementPtr);
-            recyclePreparedStatement(statement);
+            finalizePreparedStatement(statement);
         }
     }
 
+    private void finalizePreparedStatement(PreparedStatement statement) {
+        nativeFinalizeStatement(mConnectionPtr, statement.mStatementPtr);
+        recyclePreparedStatement(statement);
+    }
+
     private void bindArguments(PreparedStatement statement, Object[] bindArgs) {
         final int count = bindArgs != null ? bindArgs.length : 0;
         if (count != statement.mNumParameters) {
@@ -735,9 +758,10 @@
      * Dumps debugging information about this connection.
      *
      * @param printer The printer to receive the dump, not null.
+     * @param verbose True to dump more verbose information.
      */
-    public void dump(Printer printer) {
-        dumpUnsafe(printer);
+    public void dump(Printer printer, boolean verbose) {
+        dumpUnsafe(printer, verbose);
     }
 
     /**
@@ -752,15 +776,21 @@
      * it should not crash.  This is ok as it is only used for diagnostic purposes.
      *
      * @param printer The printer to receive the dump, not null.
+     * @param verbose True to dump more verbose information.
      */
-    void dumpUnsafe(Printer printer) {
+    void dumpUnsafe(Printer printer, boolean verbose) {
         printer.println("Connection #" + mConnectionId + ":");
+        if (verbose) {
+            printer.println("  connectionPtr: 0x" + Integer.toHexString(mConnectionPtr));
+        }
         printer.println("  isPrimaryConnection: " + mIsPrimaryConnection);
-        printer.println("  connectionPtr: 0x" + Integer.toHexString(mConnectionPtr));
         printer.println("  onlyAllowReadOnlyOperations: " + mOnlyAllowReadOnlyOperations);
 
         mRecentOperations.dump(printer);
-        mPreparedStatementCache.dump(printer);
+
+        if (verbose) {
+            mPreparedStatementCache.dump(printer);
+        }
     }
 
     /**
@@ -917,6 +947,12 @@
 
         // True if the statement is in the cache.
         public boolean mInCache;
+
+        // True if the statement is in use (currently executing).
+        // We need this flag because due to the use of custom functions in triggers, it's
+        // possible for SQLite calls to be re-entrant.  Consequently we need to prevent
+        // in use statements from being finalized until they are no longer in use.
+        public boolean mInUse;
     }
 
     private final class PreparedStatementCache
@@ -929,7 +965,9 @@
         protected void entryRemoved(boolean evicted, String key,
                 PreparedStatement oldValue, PreparedStatement newValue) {
             oldValue.mInCache = false;
-            releasePreparedStatement(oldValue);
+            if (!oldValue.mInUse) {
+                finalizePreparedStatement(oldValue);
+            }
         }
 
         public void dump(Printer printer) {
@@ -958,11 +996,14 @@
 
     private static final class OperationLog {
         private static final int MAX_RECENT_OPERATIONS = 10;
+        private static final int COOKIE_GENERATION_SHIFT = 8;
+        private static final int COOKIE_INDEX_MASK = 0xff;
 
         private final Operation[] mOperations = new Operation[MAX_RECENT_OPERATIONS];
         private int mIndex;
+        private int mGeneration;
 
-        public void beginOperation(String kind, String sql, Object[] bindArgs) {
+        public int beginOperation(String kind, String sql, Object[] bindArgs) {
             synchronized (mOperations) {
                 final int index = (mIndex + 1) % MAX_RECENT_OPERATIONS;
                 Operation operation = mOperations[index];
@@ -995,47 +1036,54 @@
                         }
                     }
                 }
+                operation.mCookie = newOperationCookieLocked(index);
                 mIndex = index;
+                return operation.mCookie;
             }
         }
 
-        public void failOperation(Exception ex) {
+        public void failOperation(int cookie, Exception ex) {
             synchronized (mOperations) {
-                final Operation operation =  mOperations[mIndex];
-                operation.mException = ex;
-            }
-        }
-
-        public boolean endOperationDeferLog() {
-            synchronized (mOperations) {
-                return endOperationDeferLogLocked();
-            }
-        }
-
-        private boolean endOperationDeferLogLocked() {
-            final Operation operation =  mOperations[mIndex];
-            operation.mEndTime = System.currentTimeMillis();
-            operation.mFinished = true;
-            return SQLiteDebug.DEBUG_LOG_SLOW_QUERIES && SQLiteDebug.shouldLogSlowQuery(
-                            operation.mEndTime - operation.mStartTime);
-        }
-
-        public void endOperation() {
-            synchronized (mOperations) {
-                if (endOperationDeferLogLocked()) {
-                    logOperationLocked(null);
+                final Operation operation = getOperationLocked(cookie);
+                if (operation != null) {
+                    operation.mException = ex;
                 }
             }
         }
 
-        public void logOperation(String detail) {
+        public void endOperation(int cookie) {
             synchronized (mOperations) {
-                logOperationLocked(detail);
+                if (endOperationDeferLogLocked(cookie)) {
+                    logOperationLocked(cookie, null);
+                }
             }
         }
 
-        private void logOperationLocked(String detail) {
-            final Operation operation =  mOperations[mIndex];
+        public boolean endOperationDeferLog(int cookie) {
+            synchronized (mOperations) {
+                return endOperationDeferLogLocked(cookie);
+            }
+        }
+
+        public void logOperation(int cookie, String detail) {
+            synchronized (mOperations) {
+                logOperationLocked(cookie, detail);
+            }
+        }
+
+        private boolean endOperationDeferLogLocked(int cookie) {
+            final Operation operation = getOperationLocked(cookie);
+            if (operation != null) {
+                operation.mEndTime = System.currentTimeMillis();
+                operation.mFinished = true;
+                return SQLiteDebug.DEBUG_LOG_SLOW_QUERIES && SQLiteDebug.shouldLogSlowQuery(
+                                operation.mEndTime - operation.mStartTime);
+            }
+            return false;
+        }
+
+        private void logOperationLocked(int cookie, String detail) {
+            final Operation operation = getOperationLocked(cookie);
             StringBuilder msg = new StringBuilder();
             operation.describe(msg);
             if (detail != null) {
@@ -1044,6 +1092,17 @@
             Log.d(TAG, msg.toString());
         }
 
+        private int newOperationCookieLocked(int index) {
+            final int generation = mGeneration++;
+            return generation << COOKIE_GENERATION_SHIFT | index;
+        }
+
+        private Operation getOperationLocked(int cookie) {
+            final int index = cookie & COOKIE_INDEX_MASK;
+            final Operation operation = mOperations[index];
+            return operation.mCookie == cookie ? operation : null;
+        }
+
         public String describeCurrentOperation() {
             synchronized (mOperations) {
                 final Operation operation = mOperations[mIndex];
@@ -1097,6 +1156,7 @@
         public ArrayList<Object> mBindArgs;
         public boolean mFinished;
         public Exception mException;
+        public int mCookie;
 
         public void describe(StringBuilder msg) {
             msg.append(mKind);
diff --git a/core/java/android/database/sqlite/SQLiteConnectionPool.java b/core/java/android/database/sqlite/SQLiteConnectionPool.java
index b88bfee..5469213 100644
--- a/core/java/android/database/sqlite/SQLiteConnectionPool.java
+++ b/core/java/android/database/sqlite/SQLiteConnectionPool.java
@@ -833,8 +833,9 @@
      * Dumps debugging information about this connection pool.
      *
      * @param printer The printer to receive the dump, not null.
+     * @param verbose True to dump more verbose information.
      */
-    public void dump(Printer printer) {
+    public void dump(Printer printer, boolean verbose) {
         Printer indentedPrinter = PrefixPrinter.create(printer, "    ");
         synchronized (mLock) {
             printer.println("Connection pool for " + mConfiguration.path + ":");
@@ -843,7 +844,7 @@
 
             printer.println("  Available primary connection:");
             if (mAvailablePrimaryConnection != null) {
-                mAvailablePrimaryConnection.dump(indentedPrinter);
+                mAvailablePrimaryConnection.dump(indentedPrinter, verbose);
             } else {
                 indentedPrinter.println("<none>");
             }
@@ -852,7 +853,7 @@
             if (!mAvailableNonPrimaryConnections.isEmpty()) {
                 final int count = mAvailableNonPrimaryConnections.size();
                 for (int i = 0; i < count; i++) {
-                    mAvailableNonPrimaryConnections.get(i).dump(indentedPrinter);
+                    mAvailableNonPrimaryConnections.get(i).dump(indentedPrinter, verbose);
                 }
             } else {
                 indentedPrinter.println("<none>");
@@ -863,7 +864,7 @@
                 for (Map.Entry<SQLiteConnection, Boolean> entry :
                         mAcquiredConnections.entrySet()) {
                     final SQLiteConnection connection = entry.getKey();
-                    connection.dumpUnsafe(indentedPrinter);
+                    connection.dumpUnsafe(indentedPrinter, verbose);
                     indentedPrinter.println("  Pending reconfiguration: " + entry.getValue());
                 }
             } else {
diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java
index 377a680..9cb6480 100644
--- a/core/java/android/database/sqlite/SQLiteDatabase.java
+++ b/core/java/android/database/sqlite/SQLiteDatabase.java
@@ -1665,17 +1665,17 @@
      * Dump detailed information about all open databases in the current process.
      * Used by bug report.
      */
-    static void dumpAll(Printer printer) {
+    static void dumpAll(Printer printer, boolean verbose) {
         for (SQLiteDatabase db : getActiveDatabases()) {
-            db.dump(printer);
+            db.dump(printer, verbose);
         }
     }
 
-    private void dump(Printer printer) {
+    private void dump(Printer printer, boolean verbose) {
         synchronized (mLock) {
             if (mConnectionPoolLocked != null) {
                 printer.println("");
-                mConnectionPoolLocked.dump(printer);
+                mConnectionPoolLocked.dump(printer, verbose);
             }
         }
     }
diff --git a/core/java/android/database/sqlite/SQLiteDebug.java b/core/java/android/database/sqlite/SQLiteDebug.java
index d87c3e4..a64251b 100644
--- a/core/java/android/database/sqlite/SQLiteDebug.java
+++ b/core/java/android/database/sqlite/SQLiteDebug.java
@@ -190,9 +190,17 @@
     /**
      * Dumps detailed information about all databases used by the process.
      * @param printer The printer for dumping database state.
+     * @param args Command-line arguments supplied to dumpsys dbinfo
      */
     public static void dump(Printer printer, String[] args) {
-        SQLiteDatabase.dumpAll(printer);
+        boolean verbose = false;
+        for (String arg : args) {
+            if (arg.equals("-v")) {
+                verbose = true;
+            }
+        }
+
+        SQLiteDatabase.dumpAll(printer, verbose);
     }
 
     /**
diff --git a/core/java/android/database/sqlite/SQLiteSession.java b/core/java/android/database/sqlite/SQLiteSession.java
index 61fe45a..a933051 100644
--- a/core/java/android/database/sqlite/SQLiteSession.java
+++ b/core/java/android/database/sqlite/SQLiteSession.java
@@ -150,6 +150,12 @@
  * A query that works well on 100 rows may struggle with 10,000.</li>
  * </ul>
  *
+ * <h2>Reentrance</h2>
+ * <p>
+ * This class must tolerate reentrant execution of SQLite operations because
+ * triggers may call custom SQLite functions that perform additional queries.
+ * </p>
+ *
  * TODO: Support timeouts on all possibly blocking operations.
  *
  * @hide
@@ -159,6 +165,7 @@
 
     private SQLiteConnection mConnection;
     private int mConnectionFlags;
+    private int mConnectionUseCount;
     private Transaction mTransactionPool;
     private Transaction mTransactionStack;
 
@@ -289,7 +296,9 @@
 
     private void beginTransactionUnchecked(int transactionMode,
             SQLiteTransactionListener transactionListener, int connectionFlags) {
-        acquireConnectionIfNoTransaction(null, connectionFlags); // might throw
+        if (mTransactionStack == null) {
+            acquireConnection(null, connectionFlags); // might throw
+        }
         try {
             // Set up the transaction such that we can back out safely
             // in case we fail part way.
@@ -325,7 +334,9 @@
             transaction.mParent = mTransactionStack;
             mTransactionStack = transaction;
         } finally {
-            releaseConnectionIfNoTransaction(); // might throw
+            if (mTransactionStack == null) {
+                releaseConnection(); // might throw
+            }
         }
     }
 
@@ -408,7 +419,7 @@
                     mConnection.execute("ROLLBACK;", null); // might throw
                 }
             } finally {
-                releaseConnectionIfNoTransaction(); // might throw
+                releaseConnection(); // might throw
             }
         }
 
@@ -534,11 +545,11 @@
             throw new IllegalArgumentException("sql must not be null.");
         }
 
-        acquireConnectionIfNoTransaction(sql, connectionFlags); // might throw
+        acquireConnection(sql, connectionFlags); // might throw
         try {
             mConnection.prepare(sql, outStatementInfo); // might throw
         } finally {
-            releaseConnectionIfNoTransaction(); // might throw
+            releaseConnection(); // might throw
         }
     }
 
@@ -562,11 +573,11 @@
             return;
         }
 
-        acquireConnectionIfNoTransaction(sql, connectionFlags); // might throw
+        acquireConnection(sql, connectionFlags); // might throw
         try {
             mConnection.execute(sql, bindArgs); // might throw
         } finally {
-            releaseConnectionIfNoTransaction(); // might throw
+            releaseConnection(); // might throw
         }
     }
 
@@ -592,11 +603,11 @@
             return 0;
         }
 
-        acquireConnectionIfNoTransaction(sql, connectionFlags); // might throw
+        acquireConnection(sql, connectionFlags); // might throw
         try {
             return mConnection.executeForLong(sql, bindArgs); // might throw
         } finally {
-            releaseConnectionIfNoTransaction(); // might throw
+            releaseConnection(); // might throw
         }
     }
 
@@ -622,11 +633,11 @@
             return null;
         }
 
-        acquireConnectionIfNoTransaction(sql, connectionFlags); // might throw
+        acquireConnection(sql, connectionFlags); // might throw
         try {
             return mConnection.executeForString(sql, bindArgs); // might throw
         } finally {
-            releaseConnectionIfNoTransaction(); // might throw
+            releaseConnection(); // might throw
         }
     }
 
@@ -655,11 +666,11 @@
             return null;
         }
 
-        acquireConnectionIfNoTransaction(sql, connectionFlags); // might throw
+        acquireConnection(sql, connectionFlags); // might throw
         try {
             return mConnection.executeForBlobFileDescriptor(sql, bindArgs); // might throw
         } finally {
-            releaseConnectionIfNoTransaction(); // might throw
+            releaseConnection(); // might throw
         }
     }
 
@@ -685,11 +696,11 @@
             return 0;
         }
 
-        acquireConnectionIfNoTransaction(sql, connectionFlags); // might throw
+        acquireConnection(sql, connectionFlags); // might throw
         try {
             return mConnection.executeForChangedRowCount(sql, bindArgs); // might throw
         } finally {
-            releaseConnectionIfNoTransaction(); // might throw
+            releaseConnection(); // might throw
         }
     }
 
@@ -715,11 +726,11 @@
             return 0;
         }
 
-        acquireConnectionIfNoTransaction(sql, connectionFlags); // might throw
+        acquireConnection(sql, connectionFlags); // might throw
         try {
             return mConnection.executeForLastInsertedRowId(sql, bindArgs); // might throw
         } finally {
-            releaseConnectionIfNoTransaction(); // might throw
+            releaseConnection(); // might throw
         }
     }
 
@@ -760,12 +771,12 @@
             return 0;
         }
 
-        acquireConnectionIfNoTransaction(sql, connectionFlags); // might throw
+        acquireConnection(sql, connectionFlags); // might throw
         try {
             return mConnection.executeForCursorWindow(sql, bindArgs,
                     window, startPos, requiredPos, countAllRows); // might throw
         } finally {
-            releaseConnectionIfNoTransaction(); // might throw
+            releaseConnection(); // might throw
         }
     }
 
@@ -807,16 +818,19 @@
         return false;
     }
 
-    private void acquireConnectionIfNoTransaction(String sql, int connectionFlags) {
-        if (mTransactionStack == null) {
-            assert mConnection == null;
+    private void acquireConnection(String sql, int connectionFlags) {
+        if (mConnection == null) {
+            assert mConnectionUseCount == 0;
             mConnection = mConnectionPool.acquireConnection(sql, connectionFlags); // might throw
             mConnectionFlags = connectionFlags;
         }
+        mConnectionUseCount += 1;
     }
 
-    private void releaseConnectionIfNoTransaction() {
-        if (mTransactionStack == null && mConnection != null) {
+    private void releaseConnection() {
+        assert mConnection != null;
+        assert mConnectionUseCount > 0;
+        if (--mConnectionUseCount == 0) {
             try {
                 mConnectionPool.releaseConnection(mConnection); // might throw
             } finally {