Loading core/java/android/database/sqlite/SQLiteConnection.java +45 −8 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ package android.database.sqlite; import android.annotation.NonNull; import com.android.internal.annotations.GuardedBy; import android.database.Cursor; import android.database.CursorWindow; Loading Loading @@ -110,9 +111,13 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen private final int mConnectionId; private final boolean mIsPrimaryConnection; private final boolean mIsReadOnlyConnection; private final PreparedStatementCache mPreparedStatementCache; private PreparedStatement mPreparedStatementPool; // A lock access to the statement cache. private final Object mCacheLock = new Object(); @GuardedBy("mCacheLock") private final PreparedStatementCache mPreparedStatementCache; // The recent operations log. private final OperationLog mRecentOperations; Loading Loading @@ -589,7 +594,9 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen mConfiguration.updateParametersFrom(configuration); // Update prepared statement cache size. synchronized (mCacheLock) { mPreparedStatementCache.resize(configuration.maxSqlCacheSize); } if (foreignKeyModeChanged) { setForeignKeyModeFromConfiguration(); Loading Loading @@ -624,8 +631,10 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen // Called by SQLiteConnectionPool only. // Returns true if the prepared statement cache contains the specified SQL. boolean isPreparedStatementInCache(String sql) { synchronized (mCacheLock) { return mPreparedStatementCache.get(sql) != null; } } /** * Gets the unique id of this connection. Loading Loading @@ -1059,12 +1068,14 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen /** * Return a {@link #PreparedStatement}, possibly from the cache. */ PreparedStatement acquirePreparedStatement(String sql) { @GuardedBy("mCacheLock") private PreparedStatement acquirePreparedStatementLI(String sql) { ++mPool.mTotalPrepareStatements; PreparedStatement statement = mPreparedStatementCache.get(sql); boolean skipCache = false; if (statement != null) { if (!statement.mInUse) { statement.mInUse = true; return statement; } // The statement is already in the cache but is in use (this statement appears Loading Loading @@ -1095,10 +1106,20 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen return statement; } /** * Return a {@link #PreparedStatement}, possibly from the cache. */ PreparedStatement acquirePreparedStatement(String sql) { synchronized (mCacheLock) { return acquirePreparedStatementLI(sql); } } /** * Release a {@link #PreparedStatement} that was originally supplied by this connection. */ void releasePreparedStatement(PreparedStatement statement) { @GuardedBy("mCacheLock") private void releasePreparedStatementLI(PreparedStatement statement) { statement.mInUse = false; if (statement.mInCache) { try { Loading @@ -1121,6 +1142,15 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen } } /** * Release a {@link #PreparedStatement} that was originally supplied by this connection. */ void releasePreparedStatement(PreparedStatement statement) { synchronized (mCacheLock) { releasePreparedStatementLI(statement); } } private void finalizePreparedStatement(PreparedStatement statement) { nativeFinalizeStatement(mConnectionPtr, statement.mStatementPtr); recyclePreparedStatement(statement); Loading Loading @@ -1295,9 +1325,11 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen mRecentOperations.dump(printer); if (verbose) { synchronized (mCacheLock) { mPreparedStatementCache.dump(printer); } } } /** * Describes the currently executing operation, in the case where the Loading Loading @@ -1427,6 +1459,12 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen return sql.replaceAll("[\\s]*\\n+[\\s]*", " "); } void clearPreparedStatementCache() { synchronized (mCacheLock) { mPreparedStatementCache.evictAll(); } } /** * Holder type for a prepared statement. * Loading Loading @@ -1469,8 +1507,7 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen public boolean mInUse; } private final class PreparedStatementCache extends LruCache<String, PreparedStatement> { private final class PreparedStatementCache extends LruCache<String, PreparedStatement> { public PreparedStatementCache(int size) { super(size); } Loading core/java/android/database/sqlite/SQLiteConnectionPool.java +10 −0 Original line number Diff line number Diff line Loading @@ -1126,6 +1126,16 @@ public final class SQLiteConnectionPool implements Closeable { mConnectionWaiterPool = waiter; } void clearAcquiredConnectionsPreparedStatementCache() { synchronized (mLock) { if (!mAcquiredConnections.isEmpty()) { for (SQLiteConnection connection : mAcquiredConnections.keySet()) { connection.clearPreparedStatementCache(); } } } } /** * Dumps debugging information about this connection pool. * Loading core/java/android/database/sqlite/SQLiteDatabase.java +4 −2 Original line number Diff line number Diff line Loading @@ -2154,10 +2154,12 @@ public final class SQLiteDatabase extends SQLiteClosable { try (SQLiteStatement statement = new SQLiteStatement(this, sql, bindArgs)) { return statement.executeUpdateDelete(); } finally { // If schema was updated, close non-primary connections, otherwise they might // have outdated schema information // If schema was updated, close non-primary connections and clear prepared // statement caches of active connections, otherwise they might have outdated // schema information. if (statementType == DatabaseUtils.STATEMENT_DDL) { mConnectionPoolLocked.closeAvailableNonPrimaryConnectionsAndLogExceptions(); mConnectionPoolLocked.clearAcquiredConnectionsPreparedStatementCache(); } } } finally { Loading core/tests/coretests/src/android/database/sqlite/SQLiteCursorTest.java +37 −2 Original line number Diff line number Diff line /* * Copyright (C) 2010 The Android Open Source Project * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. Loading Loading @@ -187,4 +187,39 @@ public class SQLiteCursorTest extends AndroidTestCase { } assertEquals("All rows should be visited", 10, n); } // Return the number of columns associated with a new cursor. private int columnCount() { final String query = "SELECT * FROM t1"; try (Cursor c = mDatabase.rawQuery(query, null)) { return c.getColumnCount(); } } /** * Verify that a cursor that is created after the database schema is updated, sees the updated * schema. */ public void testSchemaChangeCursor() { // Create the t1 table and put some data in it. mDatabase.beginTransaction(); try { mDatabase.execSQL("CREATE TABLE t1 (i int);"); mDatabase.execSQL("INSERT INTO t1 (i) VALUES (2)"); mDatabase.execSQL("INSERT INTO t1 (i) VALUES (3)"); mDatabase.execSQL("INSERT INTO t1 (i) VALUES (5)"); mDatabase.setTransactionSuccessful(); } finally { mDatabase.endTransaction(); } mDatabase.beginTransaction(); try { assertEquals(1, columnCount()); mDatabase.execSQL("ALTER TABLE t1 ADD COLUMN j int"); assertEquals(2, columnCount()); } finally { mDatabase.endTransaction(); } } } core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java 0 → 100644 +140 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.database.sqlite; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import android.content.Context; import android.database.Cursor; import android.database.DatabaseUtils; import android.os.SystemClock; import android.test.AndroidTestCase; import android.util.Log; import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import java.io.File; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; @RunWith(AndroidJUnit4.class) @SmallTest public class SQLiteDatabaseTest { private static final String TAG = "SQLiteDatabaseTest"; private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext(); private SQLiteDatabase mDatabase; private File mDatabaseFile; private static final String DATABASE_FILE_NAME = "database_test.db"; @Before public void setUp() throws Exception { assertNotNull(mContext); mContext.deleteDatabase(DATABASE_FILE_NAME); mDatabaseFile = mContext.getDatabasePath(DATABASE_FILE_NAME); mDatabaseFile.getParentFile().mkdirs(); // directory may not exist mDatabase = SQLiteDatabase.openOrCreateDatabase(mDatabaseFile, null); assertNotNull(mDatabase); } @After public void tearDown() throws Exception { closeAndDeleteDatabase(); } private void closeAndDeleteDatabase() { mDatabase.close(); SQLiteDatabase.deleteDatabase(mDatabaseFile); } @Test public void testStatementDDLEvictsCache() { // The following will be cached (key is SQL string) String selectQuery = "SELECT * FROM t1"; mDatabase.beginTransaction(); mDatabase.execSQL("CREATE TABLE `t1` (`c1` INTEGER NOT NULL PRIMARY KEY, data TEXT)"); try (Cursor c = mDatabase.rawQuery(selectQuery, null)) { assertEquals(2, c.getColumnCount()); } // Alter the schema in such a way that if the cached query is used it would produce wrong // results due to the change in column amounts. mDatabase.execSQL("ALTER TABLE `t1` RENAME TO `t1_old`"); mDatabase.execSQL("CREATE TABLE `t1` (`c1` INTEGER NOT NULL PRIMARY KEY)"); // Execute cached query (that should have been evicted), validating it sees the new schema. try (Cursor c = mDatabase.rawQuery(selectQuery, null)) { assertEquals(1, c.getColumnCount()); } mDatabase.setTransactionSuccessful(); mDatabase.endTransaction(); } @Test public void testStressDDLEvicts() { mDatabase.enableWriteAheadLogging(); mDatabase.execSQL("CREATE TABLE `t1` (`c1` INTEGER NOT NULL PRIMARY KEY, data TEXT)"); final int iterations = 1000; ExecutorService exec = Executors.newFixedThreadPool(2); exec.execute(() -> { boolean pingPong = true; for (int i = 0; i < iterations; i++) { mDatabase.beginTransaction(); if (pingPong) { mDatabase.execSQL("ALTER TABLE `t1` RENAME TO `t1_old`"); mDatabase.execSQL("CREATE TABLE `t1` (`c1` INTEGER NOT NULL " + "PRIMARY KEY)"); pingPong = false; } else { mDatabase.execSQL("DROP TABLE `t1`"); mDatabase.execSQL("ALTER TABLE `t1_old` RENAME TO `t1`"); pingPong = true; } mDatabase.setTransactionSuccessful(); mDatabase.endTransaction(); } }); exec.execute(() -> { for (int i = 0; i < iterations; i++) { try (Cursor c = mDatabase.rawQuery("SELECT * FROM t1", null)) { c.getCount(); } } }); try { exec.shutdown(); assertTrue(exec.awaitTermination(1, TimeUnit.MINUTES)); } catch (InterruptedException e) { fail("Timed out"); } } } Loading
core/java/android/database/sqlite/SQLiteConnection.java +45 −8 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ package android.database.sqlite; import android.annotation.NonNull; import com.android.internal.annotations.GuardedBy; import android.database.Cursor; import android.database.CursorWindow; Loading Loading @@ -110,9 +111,13 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen private final int mConnectionId; private final boolean mIsPrimaryConnection; private final boolean mIsReadOnlyConnection; private final PreparedStatementCache mPreparedStatementCache; private PreparedStatement mPreparedStatementPool; // A lock access to the statement cache. private final Object mCacheLock = new Object(); @GuardedBy("mCacheLock") private final PreparedStatementCache mPreparedStatementCache; // The recent operations log. private final OperationLog mRecentOperations; Loading Loading @@ -589,7 +594,9 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen mConfiguration.updateParametersFrom(configuration); // Update prepared statement cache size. synchronized (mCacheLock) { mPreparedStatementCache.resize(configuration.maxSqlCacheSize); } if (foreignKeyModeChanged) { setForeignKeyModeFromConfiguration(); Loading Loading @@ -624,8 +631,10 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen // Called by SQLiteConnectionPool only. // Returns true if the prepared statement cache contains the specified SQL. boolean isPreparedStatementInCache(String sql) { synchronized (mCacheLock) { return mPreparedStatementCache.get(sql) != null; } } /** * Gets the unique id of this connection. Loading Loading @@ -1059,12 +1068,14 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen /** * Return a {@link #PreparedStatement}, possibly from the cache. */ PreparedStatement acquirePreparedStatement(String sql) { @GuardedBy("mCacheLock") private PreparedStatement acquirePreparedStatementLI(String sql) { ++mPool.mTotalPrepareStatements; PreparedStatement statement = mPreparedStatementCache.get(sql); boolean skipCache = false; if (statement != null) { if (!statement.mInUse) { statement.mInUse = true; return statement; } // The statement is already in the cache but is in use (this statement appears Loading Loading @@ -1095,10 +1106,20 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen return statement; } /** * Return a {@link #PreparedStatement}, possibly from the cache. */ PreparedStatement acquirePreparedStatement(String sql) { synchronized (mCacheLock) { return acquirePreparedStatementLI(sql); } } /** * Release a {@link #PreparedStatement} that was originally supplied by this connection. */ void releasePreparedStatement(PreparedStatement statement) { @GuardedBy("mCacheLock") private void releasePreparedStatementLI(PreparedStatement statement) { statement.mInUse = false; if (statement.mInCache) { try { Loading @@ -1121,6 +1142,15 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen } } /** * Release a {@link #PreparedStatement} that was originally supplied by this connection. */ void releasePreparedStatement(PreparedStatement statement) { synchronized (mCacheLock) { releasePreparedStatementLI(statement); } } private void finalizePreparedStatement(PreparedStatement statement) { nativeFinalizeStatement(mConnectionPtr, statement.mStatementPtr); recyclePreparedStatement(statement); Loading Loading @@ -1295,9 +1325,11 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen mRecentOperations.dump(printer); if (verbose) { synchronized (mCacheLock) { mPreparedStatementCache.dump(printer); } } } /** * Describes the currently executing operation, in the case where the Loading Loading @@ -1427,6 +1459,12 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen return sql.replaceAll("[\\s]*\\n+[\\s]*", " "); } void clearPreparedStatementCache() { synchronized (mCacheLock) { mPreparedStatementCache.evictAll(); } } /** * Holder type for a prepared statement. * Loading Loading @@ -1469,8 +1507,7 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen public boolean mInUse; } private final class PreparedStatementCache extends LruCache<String, PreparedStatement> { private final class PreparedStatementCache extends LruCache<String, PreparedStatement> { public PreparedStatementCache(int size) { super(size); } Loading
core/java/android/database/sqlite/SQLiteConnectionPool.java +10 −0 Original line number Diff line number Diff line Loading @@ -1126,6 +1126,16 @@ public final class SQLiteConnectionPool implements Closeable { mConnectionWaiterPool = waiter; } void clearAcquiredConnectionsPreparedStatementCache() { synchronized (mLock) { if (!mAcquiredConnections.isEmpty()) { for (SQLiteConnection connection : mAcquiredConnections.keySet()) { connection.clearPreparedStatementCache(); } } } } /** * Dumps debugging information about this connection pool. * Loading
core/java/android/database/sqlite/SQLiteDatabase.java +4 −2 Original line number Diff line number Diff line Loading @@ -2154,10 +2154,12 @@ public final class SQLiteDatabase extends SQLiteClosable { try (SQLiteStatement statement = new SQLiteStatement(this, sql, bindArgs)) { return statement.executeUpdateDelete(); } finally { // If schema was updated, close non-primary connections, otherwise they might // have outdated schema information // If schema was updated, close non-primary connections and clear prepared // statement caches of active connections, otherwise they might have outdated // schema information. if (statementType == DatabaseUtils.STATEMENT_DDL) { mConnectionPoolLocked.closeAvailableNonPrimaryConnectionsAndLogExceptions(); mConnectionPoolLocked.clearAcquiredConnectionsPreparedStatementCache(); } } } finally { Loading
core/tests/coretests/src/android/database/sqlite/SQLiteCursorTest.java +37 −2 Original line number Diff line number Diff line /* * Copyright (C) 2010 The Android Open Source Project * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. Loading Loading @@ -187,4 +187,39 @@ public class SQLiteCursorTest extends AndroidTestCase { } assertEquals("All rows should be visited", 10, n); } // Return the number of columns associated with a new cursor. private int columnCount() { final String query = "SELECT * FROM t1"; try (Cursor c = mDatabase.rawQuery(query, null)) { return c.getColumnCount(); } } /** * Verify that a cursor that is created after the database schema is updated, sees the updated * schema. */ public void testSchemaChangeCursor() { // Create the t1 table and put some data in it. mDatabase.beginTransaction(); try { mDatabase.execSQL("CREATE TABLE t1 (i int);"); mDatabase.execSQL("INSERT INTO t1 (i) VALUES (2)"); mDatabase.execSQL("INSERT INTO t1 (i) VALUES (3)"); mDatabase.execSQL("INSERT INTO t1 (i) VALUES (5)"); mDatabase.setTransactionSuccessful(); } finally { mDatabase.endTransaction(); } mDatabase.beginTransaction(); try { assertEquals(1, columnCount()); mDatabase.execSQL("ALTER TABLE t1 ADD COLUMN j int"); assertEquals(2, columnCount()); } finally { mDatabase.endTransaction(); } } }
core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java 0 → 100644 +140 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.database.sqlite; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import android.content.Context; import android.database.Cursor; import android.database.DatabaseUtils; import android.os.SystemClock; import android.test.AndroidTestCase; import android.util.Log; import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import java.io.File; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; @RunWith(AndroidJUnit4.class) @SmallTest public class SQLiteDatabaseTest { private static final String TAG = "SQLiteDatabaseTest"; private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext(); private SQLiteDatabase mDatabase; private File mDatabaseFile; private static final String DATABASE_FILE_NAME = "database_test.db"; @Before public void setUp() throws Exception { assertNotNull(mContext); mContext.deleteDatabase(DATABASE_FILE_NAME); mDatabaseFile = mContext.getDatabasePath(DATABASE_FILE_NAME); mDatabaseFile.getParentFile().mkdirs(); // directory may not exist mDatabase = SQLiteDatabase.openOrCreateDatabase(mDatabaseFile, null); assertNotNull(mDatabase); } @After public void tearDown() throws Exception { closeAndDeleteDatabase(); } private void closeAndDeleteDatabase() { mDatabase.close(); SQLiteDatabase.deleteDatabase(mDatabaseFile); } @Test public void testStatementDDLEvictsCache() { // The following will be cached (key is SQL string) String selectQuery = "SELECT * FROM t1"; mDatabase.beginTransaction(); mDatabase.execSQL("CREATE TABLE `t1` (`c1` INTEGER NOT NULL PRIMARY KEY, data TEXT)"); try (Cursor c = mDatabase.rawQuery(selectQuery, null)) { assertEquals(2, c.getColumnCount()); } // Alter the schema in such a way that if the cached query is used it would produce wrong // results due to the change in column amounts. mDatabase.execSQL("ALTER TABLE `t1` RENAME TO `t1_old`"); mDatabase.execSQL("CREATE TABLE `t1` (`c1` INTEGER NOT NULL PRIMARY KEY)"); // Execute cached query (that should have been evicted), validating it sees the new schema. try (Cursor c = mDatabase.rawQuery(selectQuery, null)) { assertEquals(1, c.getColumnCount()); } mDatabase.setTransactionSuccessful(); mDatabase.endTransaction(); } @Test public void testStressDDLEvicts() { mDatabase.enableWriteAheadLogging(); mDatabase.execSQL("CREATE TABLE `t1` (`c1` INTEGER NOT NULL PRIMARY KEY, data TEXT)"); final int iterations = 1000; ExecutorService exec = Executors.newFixedThreadPool(2); exec.execute(() -> { boolean pingPong = true; for (int i = 0; i < iterations; i++) { mDatabase.beginTransaction(); if (pingPong) { mDatabase.execSQL("ALTER TABLE `t1` RENAME TO `t1_old`"); mDatabase.execSQL("CREATE TABLE `t1` (`c1` INTEGER NOT NULL " + "PRIMARY KEY)"); pingPong = false; } else { mDatabase.execSQL("DROP TABLE `t1`"); mDatabase.execSQL("ALTER TABLE `t1_old` RENAME TO `t1`"); pingPong = true; } mDatabase.setTransactionSuccessful(); mDatabase.endTransaction(); } }); exec.execute(() -> { for (int i = 0; i < iterations; i++) { try (Cursor c = mDatabase.rawQuery("SELECT * FROM t1", null)) { c.getCount(); } } }); try { exec.shutdown(); assertTrue(exec.awaitTermination(1, TimeUnit.MINUTES)); } catch (InterruptedException e) { fail("Timed out"); } } }