Set minimum SQL statement cache during migrations.
This is a workaround of b/183028015 where the framework's connection is caching prepared statements using the SQL query string as key and will cause issues if the schemas changes since the cache is not cleared and the statement might contain outdated information. By setting the cache size to the minimum allowed (1) we can reduce (but not completely eliminate) the chance of a SELECT query being used from cached whose backing schemas has changed. Note that the reduction in cache size is only temporary and during migrations where the schema is most likely to change.
Bug: 271083856
Test: FrameworkSQLiteDatabaseTest
Change-Id: I35ed1cecf7837d4d218a6af842edd1deac53e1e2
(cherry picked from commit on android-review.googlesource.com host: d9b627a16f1990ffb05da1424473fdccb52ad897)
Merged-In: I35ed1cecf7837d4d218a6af842edd1deac53e1e2
diff --git a/sqlite/sqlite-framework/src/androidTest/java/androidx/sqlite/db/framework/FrameworkSQLiteDatabaseTest.kt b/sqlite/sqlite-framework/src/androidTest/java/androidx/sqlite/db/framework/FrameworkSQLiteDatabaseTest.kt
index f419abd..66d82ae 100644
--- a/sqlite/sqlite-framework/src/androidTest/java/androidx/sqlite/db/framework/FrameworkSQLiteDatabaseTest.kt
+++ b/sqlite/sqlite-framework/src/androidTest/java/androidx/sqlite/db/framework/FrameworkSQLiteDatabaseTest.kt
@@ -17,6 +17,8 @@
package androidx.sqlite.db.framework
import android.content.Context
+import androidx.sqlite.db.SupportSQLiteDatabase
+import androidx.sqlite.db.SupportSQLiteOpenHelper
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
@@ -108,4 +110,53 @@
val actual = db1.attachedDbs?.map { it.first to it.second }
assertThat(expected).isEqualTo(actual)
}
+
+ // b/271083856 and b/183028015
+ @Test
+ fun testFrameWorkSQLiteDatabase_onUpgrade_maxSqlCache() {
+ // Open and close DB at initial version.
+ openHelper.writableDatabase.use { db ->
+ db.execSQL("CREATE TABLE Foo (id INTEGER NOT NULL PRIMARY KEY, data TEXT)")
+ db.execSQL("INSERT INTO Foo (id, data) VALUES (1, 'bar')")
+ }
+
+ FrameworkSQLiteOpenHelper(
+ context,
+ dbName,
+ object : SupportSQLiteOpenHelper.Callback(10) {
+ override fun onCreate(db: SupportSQLiteDatabase) {}
+
+ override fun onUpgrade(
+ db: SupportSQLiteDatabase,
+ oldVersion: Int,
+ newVersion: Int
+ ) {
+ // Do a query, this query will get cached, but we expect it to get evicted if
+ // androidx.sqlite workarounds this issue by reducing the cache size.
+ db.query("SELECT * FROM Foo").let { c ->
+ assertThat(c.moveToNext()).isTrue()
+ assertThat(c.getString(1)).isEqualTo("bar")
+ c.close()
+ }
+ // Alter table, specifically make it so that using a cached query will be
+ // troublesome.
+ db.execSQL("ALTER TABLE Foo RENAME TO Foo_old")
+ db.execSQL("CREATE TABLE Foo (id INTEGER NOT NULL PRIMARY KEY)")
+ // Do an irrelevant query to evict the last SELECT statement, sadly this is
+ // required because we can only reduce the cache size to 1, and only SELECT or
+ // UPDATE statement are cache.
+ // See frameworks/base/core/java/android/database/sqlite/SQLiteConnection.java;l=1209
+ db.query("SELECT * FROM Foo_old").close()
+ // Do earlier query, checking it is not cached
+ db.query("SELECT * FROM Foo").let { c ->
+ assertThat(c.columnNames.toList()).containsExactly("id")
+ assertThat(c.count).isEqualTo(0)
+ c.close()
+ }
+ }
+ },
+ useNoBackupDirectory = false,
+ allowDataLossOnRecovery = false
+ ).writableDatabase.close()
+ }
}
\ No newline at end of file
diff --git a/sqlite/sqlite-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteOpenHelper.kt b/sqlite/sqlite-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteOpenHelper.kt
index be7b101..65adc7e 100644
--- a/sqlite/sqlite-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteOpenHelper.kt
+++ b/sqlite/sqlite-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteOpenHelper.kt
@@ -257,6 +257,13 @@
}
override fun onConfigure(db: SQLiteDatabase) {
+ if (!migrated && callback.version != db.version) {
+ // Reduce the prepared statement cache to the minimum allowed (1) to avoid
+ // issues with queries executed during migrations. Note that when a migration is
+ // done the connection is closed and re-opened to avoid stale connections, which
+ // in turns resets the cache max size. See b/271083856
+ db.setMaxSqlCacheSize(1)
+ }
try {
callback.onConfigure(getWrappedDb(db))
} catch (t: Throwable) {