Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b2054891 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Clear statement cache if schema changes"

parents 1e800b82 999b4b19
Loading
Loading
Loading
Loading
+45 −8
Original line number Diff line number Diff line
@@ -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;
@@ -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;

@@ -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();
@@ -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.
@@ -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
@@ -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 {
@@ -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);
@@ -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
@@ -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.
     *
@@ -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);
        }
+10 −0
Original line number Diff line number Diff line
@@ -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.
     *
+4 −2
Original line number Diff line number Diff line
@@ -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 {
+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.
@@ -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();
        }
    }
}
+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");
        }
    }
}