From 62dd7c25ac5be2eff96289ce9da9a4d183074009 Mon Sep 17 00:00:00 2001
From: Jeff Sharkey
Date: Fri, 27 Mar 2020 14:43:05 -0600
Subject: Add ability to execute per-connection SQL.
Developers have been able to register custom collators using syntax
like "SELECT icu_load_collation()", but collators are registered per
database connection.
Since we don't expose any details APIs for interacting with connection
pools directly, developers can end up with flaky behavior as their
queries rotate through the pool of connections, as only a subset of
connections will have their collation registered.
This solve this, we add a new execPerConnectionSQL() method to
ensure that a given statement is executed on all current and future
database connections.
Bug: 152005629
Test: atest CtsDatabaseTestCases:android.database.sqlite.cts.SQLiteDatabaseTest
Change-Id: I459fb7b18660d2a04eec92d1e9cc410d769e361d
---
.../android/database/sqlite/SQLiteConnection.java | 26 ++++++++++++
.../android/database/sqlite/SQLiteDatabase.java | 46 ++++++++++++++++++++++
.../sqlite/SQLiteDatabaseConfiguration.java | 10 ++++-
3 files changed, 81 insertions(+), 1 deletion(-)
(limited to 'core/java/android/database/sqlite')
diff --git a/core/java/android/database/sqlite/SQLiteConnection.java b/core/java/android/database/sqlite/SQLiteConnection.java
index 796cfdce2c0d..bcb3934a5b08 100644
--- a/core/java/android/database/sqlite/SQLiteConnection.java
+++ b/core/java/android/database/sqlite/SQLiteConnection.java
@@ -28,6 +28,7 @@ import android.os.SystemClock;
import android.os.Trace;
import android.util.Log;
import android.util.LruCache;
+import android.util.Pair;
import android.util.Printer;
import dalvik.system.BlockGuard;
@@ -230,6 +231,7 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen
setAutoCheckpointInterval();
setLocaleFromConfiguration();
setCustomFunctionsFromConfiguration();
+ executePerConnectionSqlFromConfiguration(0);
}
private void dispose(boolean finalized) {
@@ -468,6 +470,24 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen
}
}
+ private void executePerConnectionSqlFromConfiguration(int startIndex) {
+ for (int i = startIndex; i < mConfiguration.perConnectionSql.size(); i++) {
+ final Pair statement = mConfiguration.perConnectionSql.get(i);
+ final int type = DatabaseUtils.getSqlStatementType(statement.first);
+ switch (type) {
+ case DatabaseUtils.STATEMENT_SELECT:
+ executeForString(statement.first, statement.second, null);
+ break;
+ case DatabaseUtils.STATEMENT_PRAGMA:
+ execute(statement.first, statement.second, null);
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Unsupported configuration statement: " + statement);
+ }
+ }
+ }
+
private void checkDatabaseWiped() {
if (!SQLiteGlobal.checkDbWipe()) {
return;
@@ -513,6 +533,9 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen
.equals(mConfiguration.customScalarFunctions);
boolean customAggregateFunctionsChanged = !configuration.customAggregateFunctions
.equals(mConfiguration.customAggregateFunctions);
+ final int oldSize = mConfiguration.perConnectionSql.size();
+ final int newSize = configuration.perConnectionSql.size();
+ boolean perConnectionSqlChanged = newSize > oldSize;
// Update configuration parameters.
mConfiguration.updateParametersFrom(configuration);
@@ -532,6 +555,9 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen
if (customScalarFunctionsChanged || customAggregateFunctionsChanged) {
setCustomFunctionsFromConfiguration();
}
+ if (perConnectionSqlChanged) {
+ executePerConnectionSqlFromConfiguration(oldSize);
+ }
}
// Called by SQLiteConnectionPool only.
diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java
index 458914efcbbd..24ac1527779e 100644
--- a/core/java/android/database/sqlite/SQLiteDatabase.java
+++ b/core/java/android/database/sqlite/SQLiteDatabase.java
@@ -1046,6 +1046,40 @@ public final class SQLiteDatabase extends SQLiteClosable {
}
}
+ /**
+ * Execute the given SQL statement on all connections to this database.
+ *
+ * This statement will be immediately executed on all existing connections,
+ * and will be automatically executed on all future connections.
+ *
+ * Some example usages are changes like {@code PRAGMA trusted_schema=OFF} or
+ * functions like {@code SELECT icu_load_collation()}. If you execute these
+ * statements using {@link #execSQL} then they will only apply to a single
+ * database connection; using this method will ensure that they are
+ * uniformly applied to all current and future connections.
+ *
+ * @param sql The SQL statement to be executed. Multiple statements
+ * separated by semicolons are not supported.
+ * @param bindArgs The arguments that should be bound to the SQL statement.
+ */
+ public void execPerConnectionSQL(@NonNull String sql, @Nullable Object[] bindArgs)
+ throws SQLException {
+ Objects.requireNonNull(sql);
+
+ synchronized (mLock) {
+ throwIfNotOpenLocked();
+
+ final int index = mConfigurationLocked.perConnectionSql.size();
+ mConfigurationLocked.perConnectionSql.add(Pair.create(sql, bindArgs));
+ try {
+ mConnectionPoolLocked.reconfigure(mConfigurationLocked);
+ } catch (RuntimeException ex) {
+ mConfigurationLocked.perConnectionSql.remove(index);
+ throw ex;
+ }
+ }
+ }
+
/**
* Gets the database version.
*
@@ -1788,6 +1822,12 @@ public final class SQLiteDatabase extends SQLiteClosable {
* using "PRAGMA journal_mode'" statement if your app is using
* {@link #enableWriteAheadLogging()}
*
+ *
+ * Note that {@code PRAGMA} values which apply on a per-connection basis
+ * should not be configured using this method; you should instead
+ * use {@link #execPerConnectionSQL} to ensure that they are uniformly
+ * applied to all current and future connections.
+ *
*
* @param sql the SQL statement to be executed. Multiple statements separated by semicolons are
* not supported.
@@ -1834,6 +1874,12 @@ public final class SQLiteDatabase extends SQLiteClosable {
* using "PRAGMA journal_mode'" statement if your app is using
* {@link #enableWriteAheadLogging()}
*
+ *
+ * Note that {@code PRAGMA} values which apply on a per-connection basis
+ * should not be configured using this method; you should instead
+ * use {@link #execPerConnectionSQL} to ensure that they are uniformly
+ * applied to all current and future connections.
+ *
*
* @param sql the SQL statement to be executed. Multiple statements separated by semicolons are
* not supported.
diff --git a/core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java b/core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java
index b11942abe0c7..21c21c902fed 100644
--- a/core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java
+++ b/core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java
@@ -18,9 +18,10 @@ package android.database.sqlite;
import android.compat.annotation.UnsupportedAppUsage;
import android.util.ArrayMap;
+import android.util.Pair;
+import java.util.ArrayList;
import java.util.Locale;
-import java.util.Map;
import java.util.function.BinaryOperator;
import java.util.function.UnaryOperator;
import java.util.regex.Pattern;
@@ -101,6 +102,11 @@ public final class SQLiteDatabaseConfiguration {
public final ArrayMap> customAggregateFunctions
= new ArrayMap<>();
+ /**
+ * The statements to execute to initialize each connection.
+ */
+ public final ArrayList> perConnectionSql = new ArrayList<>();
+
/**
* The size in bytes of each lookaside slot
*
@@ -194,6 +200,8 @@ public final class SQLiteDatabaseConfiguration {
customScalarFunctions.putAll(other.customScalarFunctions);
customAggregateFunctions.clear();
customAggregateFunctions.putAll(other.customAggregateFunctions);
+ perConnectionSql.clear();
+ perConnectionSql.addAll(other.perConnectionSql);
lookasideSlotSize = other.lookasideSlotSize;
lookasideSlotCount = other.lookasideSlotCount;
idleConnectionTimeoutMs = other.idleConnectionTimeoutMs;
--
cgit v1.2.3