From d3b0c7e7e23c4f1d8db5a084befe4653dcb5e1d5 Mon Sep 17 00:00:00 2001 From: Fyodor Kupolov Date: Tue, 20 Jun 2017 11:51:55 -0700 Subject: [PATCH] Support for lookaside configuration params Modified SQLiteDatabase to allow passing lookaside configuration to its static initializer. Encapsulated config in OpenParams and added an overloaded version of openDatabase with the new parameter. Configuration is changed in SQLiteConnection::nativeOpen, immediately after opening the database since lookaside memory configuration can only be changed when no connection is using it. Added SQLiteOpenHelper.setLookasideConfig method that is called from the constructor of the subclass. Test: bit FrameworksCoreTests:android.database.DatabaseGeneralTest Test: bit FrameworksCoreTests:android.database.SQLiteOpenHelperTest Bug: 38499845 Change-Id: Ifb761229b43c89c090939030fc25b8c480b9b9e2 --- api/current.txt | 23 ++ api/system-current.txt | 23 ++ api/test-current.txt | 23 ++ .../database/sqlite/SQLiteConnection.java | 15 +- .../database/sqlite/SQLiteDatabase.java | 304 +++++++++++++++++- .../sqlite/SQLiteDatabaseConfiguration.java | 18 ++ .../database/sqlite/SQLiteOpenHelper.java | 78 +++-- .../jni/android_database_SQLiteConnection.cpp | 15 +- .../android/database/DatabaseGeneralTest.java | 54 +++- .../database/SQLiteOpenHelperTest.java | 171 ++++++++++ 10 files changed, 672 insertions(+), 52 deletions(-) create mode 100644 core/tests/coretests/src/android/database/SQLiteOpenHelperTest.java diff --git a/api/current.txt b/api/current.txt index 0eab1fd802ca3..38829b8fe1465 100644 --- a/api/current.txt +++ b/api/current.txt @@ -11868,6 +11868,7 @@ package android.database.sqlite { method public void beginTransactionWithListenerNonExclusive(android.database.sqlite.SQLiteTransactionListener); method public android.database.sqlite.SQLiteStatement compileStatement(java.lang.String) throws android.database.SQLException; method public static android.database.sqlite.SQLiteDatabase create(android.database.sqlite.SQLiteDatabase.CursorFactory); + method public static android.database.sqlite.SQLiteDatabase createInMemory(android.database.sqlite.SQLiteDatabase.OpenParams); method public int delete(java.lang.String, java.lang.String, java.lang.String[]); method public static boolean deleteDatabase(java.io.File); method public void disableWriteAheadLogging(); @@ -11897,6 +11898,7 @@ package android.database.sqlite { method public boolean needUpgrade(int); method protected void onAllReferencesReleased(); method public static android.database.sqlite.SQLiteDatabase openDatabase(java.lang.String, android.database.sqlite.SQLiteDatabase.CursorFactory, int); + method public static android.database.sqlite.SQLiteDatabase openDatabase(java.lang.String, android.database.sqlite.SQLiteDatabase.OpenParams); method public static android.database.sqlite.SQLiteDatabase openDatabase(java.lang.String, android.database.sqlite.SQLiteDatabase.CursorFactory, int, android.database.DatabaseErrorHandler); method public static android.database.sqlite.SQLiteDatabase openOrCreateDatabase(java.io.File, android.database.sqlite.SQLiteDatabase.CursorFactory); method public static android.database.sqlite.SQLiteDatabase openOrCreateDatabase(java.lang.String, android.database.sqlite.SQLiteDatabase.CursorFactory); @@ -11947,6 +11949,26 @@ package android.database.sqlite { method public abstract android.database.Cursor newCursor(android.database.sqlite.SQLiteDatabase, android.database.sqlite.SQLiteCursorDriver, java.lang.String, android.database.sqlite.SQLiteQuery); } + public static final class SQLiteDatabase.OpenParams { + method public android.database.sqlite.SQLiteDatabase.CursorFactory getCursorFactory(); + method public android.database.DatabaseErrorHandler getErrorHandler(); + method public int getLookasideSlotCount(); + method public int getLookasideSlotSize(); + method public int getOpenFlags(); + } + + public static final class SQLiteDatabase.OpenParams.Builder { + ctor public SQLiteDatabase.OpenParams.Builder(); + ctor public SQLiteDatabase.OpenParams.Builder(android.database.sqlite.SQLiteDatabase.OpenParams); + method public android.database.sqlite.SQLiteDatabase.OpenParams.Builder addOpenFlags(int); + method public android.database.sqlite.SQLiteDatabase.OpenParams build(); + method public android.database.sqlite.SQLiteDatabase.OpenParams.Builder removeOpenFlags(int); + method public android.database.sqlite.SQLiteDatabase.OpenParams.Builder setCursorFactory(android.database.sqlite.SQLiteDatabase.CursorFactory); + method public android.database.sqlite.SQLiteDatabase.OpenParams.Builder setErrorHandler(android.database.DatabaseErrorHandler); + method public android.database.sqlite.SQLiteDatabase.OpenParams.Builder setLookasideConfig(int, int); + method public android.database.sqlite.SQLiteDatabase.OpenParams.Builder setOpenFlags(int); + } + public class SQLiteDatabaseCorruptException extends android.database.sqlite.SQLiteException { ctor public SQLiteDatabaseCorruptException(); ctor public SQLiteDatabaseCorruptException(java.lang.String); @@ -12000,6 +12022,7 @@ package android.database.sqlite { method public void onDowngrade(android.database.sqlite.SQLiteDatabase, int, int); method public void onOpen(android.database.sqlite.SQLiteDatabase); method public abstract void onUpgrade(android.database.sqlite.SQLiteDatabase, int, int); + method public void setLookasideConfig(int, int); method public void setWriteAheadLoggingEnabled(boolean); } diff --git a/api/system-current.txt b/api/system-current.txt index 833e1f6fc7c1a..e4d399c2b751d 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -12655,6 +12655,7 @@ package android.database.sqlite { method public void beginTransactionWithListenerNonExclusive(android.database.sqlite.SQLiteTransactionListener); method public android.database.sqlite.SQLiteStatement compileStatement(java.lang.String) throws android.database.SQLException; method public static android.database.sqlite.SQLiteDatabase create(android.database.sqlite.SQLiteDatabase.CursorFactory); + method public static android.database.sqlite.SQLiteDatabase createInMemory(android.database.sqlite.SQLiteDatabase.OpenParams); method public int delete(java.lang.String, java.lang.String, java.lang.String[]); method public static boolean deleteDatabase(java.io.File); method public void disableWriteAheadLogging(); @@ -12684,6 +12685,7 @@ package android.database.sqlite { method public boolean needUpgrade(int); method protected void onAllReferencesReleased(); method public static android.database.sqlite.SQLiteDatabase openDatabase(java.lang.String, android.database.sqlite.SQLiteDatabase.CursorFactory, int); + method public static android.database.sqlite.SQLiteDatabase openDatabase(java.lang.String, android.database.sqlite.SQLiteDatabase.OpenParams); method public static android.database.sqlite.SQLiteDatabase openDatabase(java.lang.String, android.database.sqlite.SQLiteDatabase.CursorFactory, int, android.database.DatabaseErrorHandler); method public static android.database.sqlite.SQLiteDatabase openOrCreateDatabase(java.io.File, android.database.sqlite.SQLiteDatabase.CursorFactory); method public static android.database.sqlite.SQLiteDatabase openOrCreateDatabase(java.lang.String, android.database.sqlite.SQLiteDatabase.CursorFactory); @@ -12734,6 +12736,26 @@ package android.database.sqlite { method public abstract android.database.Cursor newCursor(android.database.sqlite.SQLiteDatabase, android.database.sqlite.SQLiteCursorDriver, java.lang.String, android.database.sqlite.SQLiteQuery); } + public static final class SQLiteDatabase.OpenParams { + method public android.database.sqlite.SQLiteDatabase.CursorFactory getCursorFactory(); + method public android.database.DatabaseErrorHandler getErrorHandler(); + method public int getLookasideSlotCount(); + method public int getLookasideSlotSize(); + method public int getOpenFlags(); + } + + public static final class SQLiteDatabase.OpenParams.Builder { + ctor public SQLiteDatabase.OpenParams.Builder(); + ctor public SQLiteDatabase.OpenParams.Builder(android.database.sqlite.SQLiteDatabase.OpenParams); + method public android.database.sqlite.SQLiteDatabase.OpenParams.Builder addOpenFlags(int); + method public android.database.sqlite.SQLiteDatabase.OpenParams build(); + method public android.database.sqlite.SQLiteDatabase.OpenParams.Builder removeOpenFlags(int); + method public android.database.sqlite.SQLiteDatabase.OpenParams.Builder setCursorFactory(android.database.sqlite.SQLiteDatabase.CursorFactory); + method public android.database.sqlite.SQLiteDatabase.OpenParams.Builder setErrorHandler(android.database.DatabaseErrorHandler); + method public android.database.sqlite.SQLiteDatabase.OpenParams.Builder setLookasideConfig(int, int); + method public android.database.sqlite.SQLiteDatabase.OpenParams.Builder setOpenFlags(int); + } + public class SQLiteDatabaseCorruptException extends android.database.sqlite.SQLiteException { ctor public SQLiteDatabaseCorruptException(); ctor public SQLiteDatabaseCorruptException(java.lang.String); @@ -12787,6 +12809,7 @@ package android.database.sqlite { method public void onDowngrade(android.database.sqlite.SQLiteDatabase, int, int); method public void onOpen(android.database.sqlite.SQLiteDatabase); method public abstract void onUpgrade(android.database.sqlite.SQLiteDatabase, int, int); + method public void setLookasideConfig(int, int); method public void setWriteAheadLoggingEnabled(boolean); } diff --git a/api/test-current.txt b/api/test-current.txt index e6c22ae979f0d..c91d56ec00204 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -11911,6 +11911,7 @@ package android.database.sqlite { method public void beginTransactionWithListenerNonExclusive(android.database.sqlite.SQLiteTransactionListener); method public android.database.sqlite.SQLiteStatement compileStatement(java.lang.String) throws android.database.SQLException; method public static android.database.sqlite.SQLiteDatabase create(android.database.sqlite.SQLiteDatabase.CursorFactory); + method public static android.database.sqlite.SQLiteDatabase createInMemory(android.database.sqlite.SQLiteDatabase.OpenParams); method public int delete(java.lang.String, java.lang.String, java.lang.String[]); method public static boolean deleteDatabase(java.io.File); method public void disableWriteAheadLogging(); @@ -11940,6 +11941,7 @@ package android.database.sqlite { method public boolean needUpgrade(int); method protected void onAllReferencesReleased(); method public static android.database.sqlite.SQLiteDatabase openDatabase(java.lang.String, android.database.sqlite.SQLiteDatabase.CursorFactory, int); + method public static android.database.sqlite.SQLiteDatabase openDatabase(java.lang.String, android.database.sqlite.SQLiteDatabase.OpenParams); method public static android.database.sqlite.SQLiteDatabase openDatabase(java.lang.String, android.database.sqlite.SQLiteDatabase.CursorFactory, int, android.database.DatabaseErrorHandler); method public static android.database.sqlite.SQLiteDatabase openOrCreateDatabase(java.io.File, android.database.sqlite.SQLiteDatabase.CursorFactory); method public static android.database.sqlite.SQLiteDatabase openOrCreateDatabase(java.lang.String, android.database.sqlite.SQLiteDatabase.CursorFactory); @@ -11990,6 +11992,26 @@ package android.database.sqlite { method public abstract android.database.Cursor newCursor(android.database.sqlite.SQLiteDatabase, android.database.sqlite.SQLiteCursorDriver, java.lang.String, android.database.sqlite.SQLiteQuery); } + public static final class SQLiteDatabase.OpenParams { + method public android.database.sqlite.SQLiteDatabase.CursorFactory getCursorFactory(); + method public android.database.DatabaseErrorHandler getErrorHandler(); + method public int getLookasideSlotCount(); + method public int getLookasideSlotSize(); + method public int getOpenFlags(); + } + + public static final class SQLiteDatabase.OpenParams.Builder { + ctor public SQLiteDatabase.OpenParams.Builder(); + ctor public SQLiteDatabase.OpenParams.Builder(android.database.sqlite.SQLiteDatabase.OpenParams); + method public android.database.sqlite.SQLiteDatabase.OpenParams.Builder addOpenFlags(int); + method public android.database.sqlite.SQLiteDatabase.OpenParams build(); + method public android.database.sqlite.SQLiteDatabase.OpenParams.Builder removeOpenFlags(int); + method public android.database.sqlite.SQLiteDatabase.OpenParams.Builder setCursorFactory(android.database.sqlite.SQLiteDatabase.CursorFactory); + method public android.database.sqlite.SQLiteDatabase.OpenParams.Builder setErrorHandler(android.database.DatabaseErrorHandler); + method public android.database.sqlite.SQLiteDatabase.OpenParams.Builder setLookasideConfig(int, int); + method public android.database.sqlite.SQLiteDatabase.OpenParams.Builder setOpenFlags(int); + } + public class SQLiteDatabaseCorruptException extends android.database.sqlite.SQLiteException { ctor public SQLiteDatabaseCorruptException(); ctor public SQLiteDatabaseCorruptException(java.lang.String); @@ -12043,6 +12065,7 @@ package android.database.sqlite { method public void onDowngrade(android.database.sqlite.SQLiteDatabase, int, int); method public void onOpen(android.database.sqlite.SQLiteDatabase); method public abstract void onUpgrade(android.database.sqlite.SQLiteDatabase, int, int); + method public void setLookasideConfig(int, int); method public void setWriteAheadLoggingEnabled(boolean); } diff --git a/core/java/android/database/sqlite/SQLiteConnection.java b/core/java/android/database/sqlite/SQLiteConnection.java index 34a9523fc104c..f894f0536b522 100644 --- a/core/java/android/database/sqlite/SQLiteConnection.java +++ b/core/java/android/database/sqlite/SQLiteConnection.java @@ -16,9 +16,6 @@ package android.database.sqlite; -import dalvik.system.BlockGuard; -import dalvik.system.CloseGuard; - import android.database.Cursor; import android.database.CursorWindow; import android.database.DatabaseUtils; @@ -32,11 +29,14 @@ import android.util.Log; import android.util.LruCache; import android.util.Printer; +import dalvik.system.BlockGuard; +import dalvik.system.CloseGuard; + import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.Map; -import java.util.regex.Pattern; + /** * Represents a SQLite database connection. @@ -118,7 +118,8 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen private int mCancellationSignalAttachCount; private static native long nativeOpen(String path, int openFlags, String label, - boolean enableTrace, boolean enableProfile); + boolean enableTrace, boolean enableProfile, int lookasideSlotSize, + int lookasideSlotCount); private static native void nativeClose(long connectionPtr); private static native void nativeRegisterCustomFunction(long connectionPtr, SQLiteCustomFunction function); @@ -208,8 +209,8 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen private void open() { mConnectionPtr = nativeOpen(mConfiguration.path, mConfiguration.openFlags, mConfiguration.label, - SQLiteDebug.DEBUG_SQL_STATEMENTS, SQLiteDebug.DEBUG_SQL_TIME); - + SQLiteDebug.DEBUG_SQL_STATEMENTS, SQLiteDebug.DEBUG_SQL_TIME, + mConfiguration.lookasideSlotSize, mConfiguration.lookasideSlotCount); setPageSize(); setForeignKeyModeFromConfiguration(); setWalModeFromConfiguration(); diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java index fe849b8a99cf5..7406b3fd1f283 100644 --- a/core/java/android/database/sqlite/SQLiteDatabase.java +++ b/core/java/android/database/sqlite/SQLiteDatabase.java @@ -16,6 +16,8 @@ package android.database.sqlite; +import android.annotation.IntDef; +import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.ContentValues; @@ -34,10 +36,14 @@ import android.util.Log; import android.util.Pair; import android.util.Printer; +import com.android.internal.util.Preconditions; + import dalvik.system.CloseGuard; import java.io.File; import java.io.FileFilter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -253,11 +259,14 @@ public final class SQLiteDatabase extends SQLiteClosable { */ public static final int MAX_SQL_CACHE_SIZE = 100; - private SQLiteDatabase(String path, int openFlags, CursorFactory cursorFactory, - DatabaseErrorHandler errorHandler) { + private SQLiteDatabase(final String path, final int openFlags, + CursorFactory cursorFactory, DatabaseErrorHandler errorHandler, + int lookasideSlotSize, int lookasideSlotCount) { mCursorFactory = cursorFactory; mErrorHandler = errorHandler != null ? errorHandler : new DefaultDatabaseErrorHandler(); mConfigurationLocked = new SQLiteDatabaseConfiguration(path, openFlags); + mConfigurationLocked.lookasideSlotSize = lookasideSlotSize; + mConfigurationLocked.lookasideSlotCount = lookasideSlotCount; } @Override @@ -667,10 +676,29 @@ public final class SQLiteDatabase extends SQLiteClosable { * @return the newly opened database * @throws SQLiteException if the database cannot be opened */ - public static SQLiteDatabase openDatabase(String path, CursorFactory factory, int flags) { + public static SQLiteDatabase openDatabase(@NonNull String path, @Nullable CursorFactory factory, + @DatabaseOpenFlags int flags) { return openDatabase(path, factory, flags, null); } + /** + * Open the database according to the specified {@link OpenParams parameters} + * + * @param path to database file to open and/or create + * @param openParams configuration parameters that are used for opening {@link SQLiteDatabase} + * @return the newly opened database + * @throws SQLiteException if the database cannot be opened + */ + public static SQLiteDatabase openDatabase(@NonNull String path, + @NonNull OpenParams openParams) { + Preconditions.checkArgument(openParams != null, "OpenParams cannot be null"); + SQLiteDatabase db = new SQLiteDatabase(path, openParams.mOpenFlags, + openParams.mCursorFactory, openParams.mErrorHandler, + openParams.mLookasideSlotSize, openParams.mLookasideSlotCount); + db.open(); + return db; + } + /** * Open the database according to the flags {@link #OPEN_READWRITE} * {@link #OPEN_READONLY} {@link #CREATE_IF_NECESSARY} and/or {@link #NO_LOCALIZED_COLLATORS}. @@ -690,9 +718,9 @@ public final class SQLiteDatabase extends SQLiteClosable { * @return the newly opened database * @throws SQLiteException if the database cannot be opened */ - public static SQLiteDatabase openDatabase(String path, CursorFactory factory, int flags, - DatabaseErrorHandler errorHandler) { - SQLiteDatabase db = new SQLiteDatabase(path, flags, factory, errorHandler); + public static SQLiteDatabase openDatabase(@NonNull String path, @Nullable CursorFactory factory, + @DatabaseOpenFlags int flags, @Nullable DatabaseErrorHandler errorHandler) { + SQLiteDatabase db = new SQLiteDatabase(path, flags, factory, errorHandler, -1, -1); db.open(); return db; } @@ -700,22 +728,24 @@ public final class SQLiteDatabase extends SQLiteClosable { /** * Equivalent to openDatabase(file.getPath(), factory, CREATE_IF_NECESSARY). */ - public static SQLiteDatabase openOrCreateDatabase(File file, CursorFactory factory) { + public static SQLiteDatabase openOrCreateDatabase(@NonNull File file, + @Nullable CursorFactory factory) { return openOrCreateDatabase(file.getPath(), factory); } /** * Equivalent to openDatabase(path, factory, CREATE_IF_NECESSARY). */ - public static SQLiteDatabase openOrCreateDatabase(String path, CursorFactory factory) { + public static SQLiteDatabase openOrCreateDatabase(@NonNull String path, + @Nullable CursorFactory factory) { return openDatabase(path, factory, CREATE_IF_NECESSARY, null); } /** * Equivalent to openDatabase(path, factory, CREATE_IF_NECESSARY, errorHandler). */ - public static SQLiteDatabase openOrCreateDatabase(String path, CursorFactory factory, - DatabaseErrorHandler errorHandler) { + public static SQLiteDatabase openOrCreateDatabase(@NonNull String path, + @Nullable CursorFactory factory, @Nullable DatabaseErrorHandler errorHandler) { return openDatabase(path, factory, CREATE_IF_NECESSARY, errorHandler); } @@ -726,7 +756,7 @@ public final class SQLiteDatabase extends SQLiteClosable { * @param file The database file path. * @return True if the database was successfully deleted. */ - public static boolean deleteDatabase(File file) { + public static boolean deleteDatabase(@NonNull File file) { if (file == null) { throw new IllegalArgumentException("file must not be null"); } @@ -825,12 +855,28 @@ public final class SQLiteDatabase extends SQLiteClosable { * cursor when query is called * @return a SQLiteDatabase object, or null if the database can't be created */ - public static SQLiteDatabase create(CursorFactory factory) { + @NonNull + public static SQLiteDatabase create(@Nullable CursorFactory factory) { // This is a magic string with special meaning for SQLite. return openDatabase(SQLiteDatabaseConfiguration.MEMORY_DB_PATH, factory, CREATE_IF_NECESSARY); } + /** + * Create a memory backed SQLite database. Its contents will be destroyed + * when the database is closed. + * + *

Sets the locale of the database to the the system's current locale. + * Call {@link #setLocale} if you would like something else.

+ * @param openParams configuration parameters that are used for opening SQLiteDatabase + * @return a SQLiteDatabase object, or null if the database can't be created + */ + @NonNull + public static SQLiteDatabase createInMemory(@NonNull OpenParams openParams) { + return openDatabase(SQLiteDatabaseConfiguration.MEMORY_DB_PATH, + openParams.toBuilder().addOpenFlags(CREATE_IF_NECESSARY).build()); + } + /** * Registers a CustomFunction callback as a function that can be called from * SQLite database triggers. @@ -2210,4 +2256,238 @@ public final class SQLiteDatabase extends SQLiteClosable { public interface CustomFunction { public void callback(String[] args); } + + /** + * Wrapper for configuration parameters that are used for opening {@link SQLiteDatabase} + */ + public static final class OpenParams { + private final int mOpenFlags; + private final CursorFactory mCursorFactory; + private final DatabaseErrorHandler mErrorHandler; + private final int mLookasideSlotSize; + private final int mLookasideSlotCount; + + private OpenParams(int openFlags, CursorFactory cursorFactory, + DatabaseErrorHandler errorHandler, int lookasideSlotSize, int lookasideSlotCount) { + mOpenFlags = openFlags; + mCursorFactory = cursorFactory; + mErrorHandler = errorHandler; + mLookasideSlotSize = lookasideSlotSize; + mLookasideSlotCount = lookasideSlotCount; + } + + /** + * Returns size in bytes of each lookaside slot or -1 if not set. + * + * @see Builder#setLookasideConfig(int, int) + */ + @IntRange(from = -1) + public int getLookasideSlotSize() { + return mLookasideSlotSize; + } + + /** + * Returns total number of lookaside memory slots per database connection or -1 if not + * set. + * + * @see Builder#setLookasideConfig(int, int) + */ + @IntRange(from = -1) + public int getLookasideSlotCount() { + return mLookasideSlotCount; + } + + /** + * Returns flags to control database access mode + * + * @see Builder#setOpenFlags(int) + */ + @DatabaseOpenFlags + public int getOpenFlags() { + return mOpenFlags; + } + + /** + * Returns an optional factory class that is called to instantiate a cursor when query + * is called + * + * @see Builder#setCursorFactory(CursorFactory) + */ + @Nullable + public CursorFactory getCursorFactory() { + return mCursorFactory; + } + + /** + * Returns handler for database corruption errors + * + * @see Builder#setErrorHandler(DatabaseErrorHandler) + */ + @Nullable + public DatabaseErrorHandler getErrorHandler() { + return mErrorHandler; + } + + /** + * Creates a new instance of builder {@link Builder#Builder(OpenParams) initialized} with + * {@code this} parameters. + * @hide + */ + @NonNull + public Builder toBuilder() { + return new Builder(this); + } + + /** + * Builder for {@link OpenParams}. + */ + public static final class Builder { + private int mLookasideSlotSize = -1; + private int mLookasideSlotCount = -1; + private int mOpenFlags; + private CursorFactory mCursorFactory; + private DatabaseErrorHandler mErrorHandler; + + public Builder() { + } + + public Builder(OpenParams params) { + mLookasideSlotSize = params.mLookasideSlotSize; + mLookasideSlotCount = params.mLookasideSlotCount; + mOpenFlags = params.mOpenFlags; + mCursorFactory = params.mCursorFactory; + mErrorHandler = params.mErrorHandler; + } + + /** + * Configures + * lookaside memory allocator + * + *

SQLite default settings will be used, if this method isn't called. + * Use {@code setLookasideConfig(0,0)} to disable lookaside + * + * @param slotSize The size in bytes of each lookaside slot. + * @param slotCount The total number of lookaside memory slots per database connection. + */ + public Builder setLookasideConfig(@IntRange(from = 0) final int slotSize, + @IntRange(from = 0) final int slotCount) { + Preconditions.checkArgument(slotSize >= 0, + "lookasideSlotCount cannot be negative"); + Preconditions.checkArgument(slotCount >= 0, + "lookasideSlotSize cannot be negative"); + Preconditions.checkArgument( + (slotSize > 0 && slotCount > 0) || (slotCount == 0 && slotSize == 0), + "Invalid configuration: " + slotSize + ", " + slotCount); + + mLookasideSlotSize = slotSize; + mLookasideSlotCount = slotCount; + return this; + } + + /** + * Returns true if {@link #ENABLE_WRITE_AHEAD_LOGGING} flag is set + * @hide + */ + public boolean isWriteAheadLoggingEnabled() { + return (mOpenFlags & ENABLE_WRITE_AHEAD_LOGGING) != 0; + } + + /** + * Sets flags to control database access mode + * @param openFlags The new flags to set + * @see #OPEN_READWRITE + * @see #OPEN_READONLY + * @see #CREATE_IF_NECESSARY + * @see #NO_LOCALIZED_COLLATORS + * @see #ENABLE_WRITE_AHEAD_LOGGING + * @return same builder instance for chaining multiple calls into a single statement + */ + @NonNull + public Builder setOpenFlags(@DatabaseOpenFlags int openFlags) { + mOpenFlags = openFlags; + return this; + } + + /** + * Adds flags to control database access mode + * + * @param openFlags The new flags to add + * @return same builder instance for chaining multiple calls into a single statement + */ + @NonNull + public Builder addOpenFlags(@DatabaseOpenFlags int openFlags) { + mOpenFlags |= openFlags; + return this; + } + + /** + * Removes database access mode flags + * + * @param openFlags Flags to remove + * @return same builder instance for chaining multiple calls into a single statement + */ + @NonNull + public Builder removeOpenFlags(@DatabaseOpenFlags int openFlags) { + mOpenFlags &= ~openFlags; + return this; + } + + /** + * Sets {@link #ENABLE_WRITE_AHEAD_LOGGING} flag if {@code enabled} is {@code true}, + * unsets otherwise + * @hide + */ + public void setWriteAheadLoggingEnabled(boolean enabled) { + if (enabled) { + addOpenFlags(ENABLE_WRITE_AHEAD_LOGGING); + } else { + removeOpenFlags(ENABLE_WRITE_AHEAD_LOGGING); + } + } + + /** + * Set an optional factory class that is called to instantiate a cursor when query + * is called. + * + * @param cursorFactory instance + * @return same builder instance for chaining multiple calls into a single statement + */ + @NonNull + public Builder setCursorFactory(@Nullable CursorFactory cursorFactory) { + mCursorFactory = cursorFactory; + return this; + } + + + /** + * Sets {@link DatabaseErrorHandler} object to handle db corruption errors + */ + @NonNull + public Builder setErrorHandler(@Nullable DatabaseErrorHandler errorHandler) { + mErrorHandler = errorHandler; + return this; + } + + /** + * Creates an instance of {@link OpenParams} with the options that were previously set + * on this builder + */ + @NonNull + public OpenParams build() { + return new OpenParams(mOpenFlags, mCursorFactory, mErrorHandler, mLookasideSlotSize, + mLookasideSlotCount); + } + } + } + + /** @hide */ + @IntDef(flag = true, prefix = {"OPEN_", "CREATE_", "NO_", "ENABLE_"}, value = { + OPEN_READWRITE, + OPEN_READONLY, + CREATE_IF_NECESSARY, + NO_LOCALIZED_COLLATORS, + ENABLE_WRITE_AHEAD_LOGGING + }) + @Retention(RetentionPolicy.SOURCE) + public @interface DatabaseOpenFlags {} } diff --git a/core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java b/core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java index 549ab902fdd1f..1435c83c53dc0 100644 --- a/core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java +++ b/core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java @@ -89,6 +89,20 @@ public final class SQLiteDatabaseConfiguration { public final ArrayList customFunctions = new ArrayList(); + /** + * The size in bytes of each lookaside slot + * + *

If negative, the default lookaside configuration will be used + */ + public int lookasideSlotSize; + + /** + * The total number of lookaside memory slots per database connection + * + *

If negative, the default lookaside configuration will be used + */ + public int lookasideSlotCount; + /** * Creates a database configuration with the required parameters for opening a * database and default values for all other parameters. @@ -108,6 +122,8 @@ public final class SQLiteDatabaseConfiguration { // Set default values for optional parameters. maxSqlCacheSize = 25; locale = Locale.getDefault(); + lookasideSlotSize = -1; + lookasideSlotCount = -1; } /** @@ -146,6 +162,8 @@ public final class SQLiteDatabaseConfiguration { foreignKeyConstraintsEnabled = other.foreignKeyConstraintsEnabled; customFunctions.clear(); customFunctions.addAll(other.customFunctions); + lookasideSlotSize = other.lookasideSlotSize; + lookasideSlotCount = other.lookasideSlotCount; } /** diff --git a/core/java/android/database/sqlite/SQLiteOpenHelper.java b/core/java/android/database/sqlite/SQLiteOpenHelper.java index bb8d9ffa679bf..44a72ddd1f7a8 100644 --- a/core/java/android/database/sqlite/SQLiteOpenHelper.java +++ b/core/java/android/database/sqlite/SQLiteOpenHelper.java @@ -16,10 +16,14 @@ package android.database.sqlite; +import android.annotation.IntRange; import android.content.Context; import android.database.DatabaseErrorHandler; +import android.database.SQLException; import android.database.sqlite.SQLiteDatabase.CursorFactory; +import android.os.FileUtils; import android.util.Log; + import java.io.File; /** @@ -43,24 +47,14 @@ import java.io.File; public abstract class SQLiteOpenHelper { private static final String TAG = SQLiteOpenHelper.class.getSimpleName(); - // When true, getReadableDatabase returns a read-only database if it is just being opened. - // The database handle is reopened in read/write mode when getWritableDatabase is called. - // We leave this behavior disabled in production because it is inefficient and breaks - // many applications. For debugging purposes it can be useful to turn on strict - // read-only semantics to catch applications that call getReadableDatabase when they really - // wanted getWritableDatabase. - private static final boolean DEBUG_STRICT_READONLY = false; - private final Context mContext; private final String mName; - private final CursorFactory mFactory; private final int mNewVersion; private final int mMinimumSupportedVersion; private SQLiteDatabase mDatabase; private boolean mIsInitializing; - private boolean mEnableWriteAheadLogging; - private final DatabaseErrorHandler mErrorHandler; + private final SQLiteDatabase.OpenParams.Builder mOpenParamsBuilder; /** * Create a helper object to create, open, and/or manage a database. @@ -130,10 +124,12 @@ public abstract class SQLiteOpenHelper { mContext = context; mName = name; - mFactory = factory; mNewVersion = version; - mErrorHandler = errorHandler; mMinimumSupportedVersion = Math.max(0, minimumSupportedVersion); + mOpenParamsBuilder = new SQLiteDatabase.OpenParams.Builder(); + mOpenParamsBuilder.setCursorFactory(factory); + mOpenParamsBuilder.setErrorHandler(errorHandler); + mOpenParamsBuilder.addOpenFlags(SQLiteDatabase.CREATE_IF_NECESSARY); } /** @@ -157,7 +153,7 @@ public abstract class SQLiteOpenHelper { */ public void setWriteAheadLoggingEnabled(boolean enabled) { synchronized (this) { - if (mEnableWriteAheadLogging != enabled) { + if (mOpenParamsBuilder.isWriteAheadLoggingEnabled() != enabled) { if (mDatabase != null && mDatabase.isOpen() && !mDatabase.isReadOnly()) { if (enabled) { mDatabase.enableWriteAheadLogging(); @@ -165,11 +161,35 @@ public abstract class SQLiteOpenHelper { mDatabase.disableWriteAheadLogging(); } } - mEnableWriteAheadLogging = enabled; + mOpenParamsBuilder.setWriteAheadLoggingEnabled(enabled); } } } + /** + * Configures lookaside memory allocator + * + *

This method should be called from the constructor of the subclass, + * before opening the database, since lookaside memory configuration can only be changed + * when no connection is using it + * + *

SQLite default settings will be used, if this method isn't called. + * Use {@code setLookasideConfig(0,0)} to disable lookaside + * + * @param slotSize The size in bytes of each lookaside slot. + * @param slotCount The total number of lookaside memory slots per database connection. + */ + public void setLookasideConfig(@IntRange(from = 0) final int slotSize, + @IntRange(from = 0) final int slotCount) { + synchronized (this) { + if (mDatabase != null && mDatabase.isOpen()) { + throw new IllegalStateException( + "Lookaside memory config cannot be changed after opening the database"); + } + mOpenParamsBuilder.setLookasideConfig(slotSize, slotCount); + } + } + /** * Create and/or open a database that will be used for reading and writing. * The first time this is called, the database will be opened and @@ -243,27 +263,22 @@ public abstract class SQLiteOpenHelper { db.reopenReadWrite(); } } else if (mName == null) { - db = SQLiteDatabase.create(null); + db = SQLiteDatabase.createInMemory(mOpenParamsBuilder.build()); } else { + final String path = mContext.getDatabasePath(mName).getPath(); + SQLiteDatabase.OpenParams params = mOpenParamsBuilder.build(); try { - if (DEBUG_STRICT_READONLY && !writable) { - final String path = mContext.getDatabasePath(mName).getPath(); - db = SQLiteDatabase.openDatabase(path, mFactory, - SQLiteDatabase.OPEN_READONLY, mErrorHandler); - } else { - db = mContext.openOrCreateDatabase(mName, mEnableWriteAheadLogging ? - Context.MODE_ENABLE_WRITE_AHEAD_LOGGING : 0, - mFactory, mErrorHandler); - } - } catch (SQLiteException ex) { + db = SQLiteDatabase.openDatabase(path, params); + // Keep pre-O-MR1 behavior by resetting file permissions to 660 + setFilePermissionsForDb(path); + } catch (SQLException ex) { if (writable) { throw ex; } Log.e(TAG, "Couldn't open " + mName + " for writing (will try read-only):", ex); - final String path = mContext.getDatabasePath(mName).getPath(); - db = SQLiteDatabase.openDatabase(path, mFactory, - SQLiteDatabase.OPEN_READONLY, mErrorHandler); + params = params.toBuilder().addOpenFlags(SQLiteDatabase.OPEN_READONLY).build(); + db = SQLiteDatabase.openDatabase(path, params); } } @@ -323,6 +338,11 @@ public abstract class SQLiteOpenHelper { } } + private static void setFilePermissionsForDb(String dbPath) { + int perms = FileUtils.S_IRUSR | FileUtils.S_IWUSR | FileUtils.S_IRGRP | FileUtils.S_IWGRP; + FileUtils.setPermissions(dbPath, perms, -1, -1); + } + /** * Close any open database object. */ diff --git a/core/jni/android_database_SQLiteConnection.cpp b/core/jni/android_database_SQLiteConnection.cpp index bcc3bb09b69dc..62240ea878070 100644 --- a/core/jni/android_database_SQLiteConnection.cpp +++ b/core/jni/android_database_SQLiteConnection.cpp @@ -112,7 +112,8 @@ static int sqliteProgressHandlerCallback(void* data) { static jlong nativeOpen(JNIEnv* env, jclass clazz, jstring pathStr, jint openFlags, - jstring labelStr, jboolean enableTrace, jboolean enableProfile) { + jstring labelStr, jboolean enableTrace, jboolean enableProfile, jint lookasideSz, + jint lookasideCnt) { int sqliteFlags; if (openFlags & SQLiteConnection::CREATE_IF_NECESSARY) { sqliteFlags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE; @@ -137,6 +138,16 @@ static jlong nativeOpen(JNIEnv* env, jclass clazz, jstring pathStr, jint openFla return 0; } + if (lookasideSz >= 0 && lookasideCnt >= 0) { + int err = sqlite3_db_config(db, SQLITE_DBCONFIG_LOOKASIDE, NULL, lookasideSz, lookasideCnt); + if (err != SQLITE_OK) { + ALOGE("sqlite3_db_config(..., %d, %d) failed: %d", lookasideSz, lookasideCnt, err); + throw_sqlite3_exception(env, db, "Cannot set lookaside"); + sqlite3_close(db); + return 0; + } + } + // Check that the database is really read/write when that is what we asked for. if ((sqliteFlags & SQLITE_OPEN_READWRITE) && sqlite3_db_readonly(db, NULL)) { throw_sqlite3_exception(env, db, "Could not open the database in read/write mode."); @@ -789,7 +800,7 @@ static void nativeResetCancel(JNIEnv* env, jobject clazz, jlong connectionPtr, static const JNINativeMethod sMethods[] = { /* name, signature, funcPtr */ - { "nativeOpen", "(Ljava/lang/String;ILjava/lang/String;ZZ)J", + { "nativeOpen", "(Ljava/lang/String;ILjava/lang/String;ZZII)J", (void*)nativeOpen }, { "nativeClose", "(J)V", (void*)nativeClose }, diff --git a/core/tests/coretests/src/android/database/DatabaseGeneralTest.java b/core/tests/coretests/src/android/database/DatabaseGeneralTest.java index c7cb43d3dbc04..7800f4ab0598f 100644 --- a/core/tests/coretests/src/android/database/DatabaseGeneralTest.java +++ b/core/tests/coretests/src/android/database/DatabaseGeneralTest.java @@ -18,11 +18,12 @@ package android.database; import static android.database.DatabaseUtils.InsertHelper.TABLE_INFO_PRAGMA_COLUMNNAME_INDEX; import static android.database.DatabaseUtils.InsertHelper.TABLE_INFO_PRAGMA_DEFAULT_INDEX; + import android.content.ContentValues; import android.content.Context; import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteDebug; import android.database.sqlite.SQLiteException; -import android.os.Handler; import android.os.Parcel; import android.test.AndroidTestCase; import android.test.PerformanceTestCase; @@ -40,6 +41,9 @@ import java.util.Arrays; import java.util.List; import java.util.Locale; +/** + * Usage: bit FrameworksCoreTests:android.database.DatabaseGeneralTest + */ public class DatabaseGeneralTest extends AndroidTestCase implements PerformanceTestCase { private static final String TAG = "DatabaseGeneralTest"; @@ -68,7 +72,7 @@ public class DatabaseGeneralTest extends AndroidTestCase implements PerformanceT @Override protected void tearDown() throws Exception { mDatabase.close(); - mDatabaseFile.delete(); + SQLiteDatabase.deleteDatabase(mDatabaseFile); super.tearDown(); } @@ -1044,6 +1048,52 @@ public class DatabaseGeneralTest extends AndroidTestCase implements PerformanceT } } + @SmallTest + public void testOpenDatabaseLookasideConfig() { + // First check that lookaside is active + verifyLookasideStats(false); + // Reopen test db with lookaside disabled + mDatabase.close(); + SQLiteDatabase.OpenParams params = new SQLiteDatabase.OpenParams.Builder() + .setLookasideConfig(0, 0).build(); + mDatabase = SQLiteDatabase.openDatabase(mDatabaseFile.getPath(), params); + verifyLookasideStats(true); + } + + @SmallTest + public void testOpenParamsSetLookasideConfigValidation() { + try { + SQLiteDatabase.OpenParams params = new SQLiteDatabase.OpenParams.Builder() + .setLookasideConfig(-1, 0).build(); + fail("Negative slot size should be rejected"); + } catch (IllegalArgumentException expected) { + } + try { + SQLiteDatabase.OpenParams params = new SQLiteDatabase.OpenParams.Builder() + .setLookasideConfig(0, -10).build(); + fail("Negative slot count should be rejected"); + } catch (IllegalArgumentException expected) { + } + } + + void verifyLookasideStats(boolean expectDisabled) { + boolean dbStatFound = false; + SQLiteDebug.PagerStats info = SQLiteDebug.getDatabaseInfo(); + for (SQLiteDebug.DbStats dbStat : info.dbStats) { + if (dbStat.dbName.endsWith(mDatabaseFile.getName())) { + dbStatFound = true; + Log.i(TAG, "Lookaside for " + dbStat.dbName + " " + dbStat.lookaside); + if (expectDisabled) { + assertTrue("lookaside slots count should be zero", dbStat.lookaside == 0); + } else { + assertTrue("lookaside slots count should be greater than zero", + dbStat.lookaside > 0); + } + } + } + assertTrue("No dbstat found for " + mDatabaseFile.getName(), dbStatFound); + } + @LargeTest public void testDefaultDatabaseErrorHandler() { DefaultDatabaseErrorHandler errorHandler = new DefaultDatabaseErrorHandler(); diff --git a/core/tests/coretests/src/android/database/SQLiteOpenHelperTest.java b/core/tests/coretests/src/android/database/SQLiteOpenHelperTest.java new file mode 100644 index 0000000000000..75eeb93f5b9a4 --- /dev/null +++ b/core/tests/coretests/src/android/database/SQLiteOpenHelperTest.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2017 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; + +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.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteDatabaseConfiguration; +import android.database.sqlite.SQLiteDebug; +import android.database.sqlite.SQLiteOpenHelper; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; +import android.util.Log; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.List; + +/** + * Tests for {@link SQLiteOpenHelper} + * + *

Run with: bit FrameworksCoreTests:android.database.SQLiteOpenHelperTest + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class SQLiteOpenHelperTest { + private static final String TAG = "SQLiteOpenHelperTest"; + + private TestHelper mTestHelper; + private Context mContext; + private List mHelpersToClose; + + private static class TestHelper extends SQLiteOpenHelper { + TestHelper(Context context) { // In-memory + super(context, null, null, 1); + } + + TestHelper(Context context, String name) { + super(context, name, null, 1); + } + + @Override + public void onCreate(SQLiteDatabase db) { + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + } + } + + @Before + public void setup() { + mContext = InstrumentationRegistry.getContext(); + mTestHelper = new TestHelper(mContext, "openhelper_test"); + mHelpersToClose = new ArrayList<>(); + mHelpersToClose.add(mTestHelper); + } + + @After + public void teardown() { + for (SQLiteOpenHelper helper : mHelpersToClose) { + try { + helper.close(); + if (mTestHelper.getDatabaseName() != null) { + SQLiteDatabase.deleteDatabase( + mContext.getDatabasePath(mTestHelper.getDatabaseName())); + } + } catch (RuntimeException ex) { + Log.w(TAG, "Error occured when closing db helper " + helper, ex); + } + } + } + + @Test + public void testLookasideDefault() throws Exception { + assertNotNull(mTestHelper.getWritableDatabase()); + verifyLookasideStats(false); + } + + @Test + public void testLookasideDisabled() throws Exception { + mTestHelper.setLookasideConfig(0, 0); + assertNotNull(mTestHelper.getWritableDatabase()); + verifyLookasideStats(true); + } + + @Test + public void testInMemoryLookasideDisabled() throws Exception { + TestHelper memHelper = new TestHelper(mContext); + mHelpersToClose.add(memHelper); + memHelper.setLookasideConfig(0, 0); + assertNotNull(memHelper.getWritableDatabase()); + verifyLookasideStats(SQLiteDatabaseConfiguration.MEMORY_DB_PATH, true); + } + + @Test + public void testInMemoryLookasideDefault() throws Exception { + TestHelper memHelper = new TestHelper(mContext); + mHelpersToClose.add(memHelper); + assertNotNull(memHelper.getWritableDatabase()); + verifyLookasideStats(SQLiteDatabaseConfiguration.MEMORY_DB_PATH, false); + } + + @Test + public void testSetLookasideConfigValidation() { + try { + mTestHelper.setLookasideConfig(-1, 0); + fail("Negative slot size should be rejected"); + } catch (IllegalArgumentException expected) { + } + try { + mTestHelper.setLookasideConfig(0, -10); + fail("Negative slot count should be rejected"); + } catch (IllegalArgumentException expected) { + } + try { + mTestHelper.setLookasideConfig(1, 0); + fail("Illegal config should be rejected"); + } catch (IllegalArgumentException expected) { + } + try { + mTestHelper.setLookasideConfig(0, 1); + fail("Illegal config should be rejected"); + } catch (IllegalArgumentException expected) { + } + } + + private void verifyLookasideStats(boolean expectDisabled) { + verifyLookasideStats(mTestHelper.getDatabaseName(), expectDisabled); + } + + private static void verifyLookasideStats(String dbName, boolean expectDisabled) { + boolean dbStatFound = false; + SQLiteDebug.PagerStats info = SQLiteDebug.getDatabaseInfo(); + for (SQLiteDebug.DbStats dbStat : info.dbStats) { + if (dbStat.dbName.endsWith(dbName)) { + dbStatFound = true; + Log.i(TAG, "Lookaside for " + dbStat.dbName + " " + dbStat.lookaside); + if (expectDisabled) { + assertTrue("lookaside slots count should be zero", dbStat.lookaside == 0); + } else { + assertTrue("lookaside slots count should be greater than zero", + dbStat.lookaside > 0); + } + } + } + assertTrue("No dbstat found for " + dbName, dbStatFound); + } +}