diff options
Diffstat (limited to 'android/arch/persistence')
43 files changed, 1071 insertions, 202 deletions
diff --git a/android/arch/persistence/db/SupportSQLiteOpenHelper.java b/android/arch/persistence/db/SupportSQLiteOpenHelper.java index 5a96e5ac..02e4e7dc 100644 --- a/android/arch/persistence/db/SupportSQLiteOpenHelper.java +++ b/android/arch/persistence/db/SupportSQLiteOpenHelper.java @@ -17,13 +17,18 @@ package android.arch.persistence.db; import android.content.Context; -import android.database.DatabaseErrorHandler; -import android.database.DefaultDatabaseErrorHandler; +import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; +import android.util.Log; +import android.util.Pair; + +import java.io.File; +import java.io.IOException; +import java.util.List; /** * An interface to map the behavior of {@link android.database.sqlite.SQLiteOpenHelper}. @@ -99,10 +104,29 @@ public interface SupportSQLiteOpenHelper { void close(); /** - * Matching callback methods from {@link android.database.sqlite.SQLiteOpenHelper}. + * Handles various lifecycle events for the SQLite connection, similar to + * {@link android.database.sqlite.SQLiteOpenHelper}. */ @SuppressWarnings({"unused", "WeakerAccess"}) abstract class Callback { + private static final String TAG = "SupportSQLite"; + /** + * Version number of the database (starting at 1); if the database is older, + * {@link SupportSQLiteOpenHelper.Callback#onUpgrade(SupportSQLiteDatabase, int, int)} + * will be used to upgrade the database; if the database is newer, + * {@link SupportSQLiteOpenHelper.Callback#onDowngrade(SupportSQLiteDatabase, int, int)} + * will be used to downgrade the database. + */ + public final int version; + + /** + * Creates a new Callback to get database lifecycle events. + * @param version The version for the database instance. See {@link #version}. + */ + public Callback(int version) { + this.version = version; + } + /** * Called when the database connection is being configured, to enable features such as * write-ahead logging or foreign key support. @@ -193,6 +217,81 @@ public interface SupportSQLiteOpenHelper { public void onOpen(SupportSQLiteDatabase db) { } + + /** + * The method invoked when database corruption is detected. Default implementation will + * delete the database file. + * + * @param db the {@link SupportSQLiteDatabase} object representing the database on which + * corruption is detected. + */ + public void onCorruption(SupportSQLiteDatabase db) { + // the following implementation is taken from {@link DefaultDatabaseErrorHandler}. + + Log.e(TAG, "Corruption reported by sqlite on database: " + db.getPath()); + // is the corruption detected even before database could be 'opened'? + if (!db.isOpen()) { + // database files are not even openable. delete this database file. + // NOTE if the database has attached databases, then any of them could be corrupt. + // and not deleting all of them could cause corrupted database file to remain and + // make the application crash on database open operation. To avoid this problem, + // the application should provide its own {@link DatabaseErrorHandler} impl class + // to delete ALL files of the database (including the attached databases). + deleteDatabaseFile(db.getPath()); + return; + } + + List<Pair<String, String>> attachedDbs = null; + try { + // Close the database, which will cause subsequent operations to fail. + // before that, get the attached database list first. + try { + attachedDbs = db.getAttachedDbs(); + } catch (SQLiteException e) { + /* ignore */ + } + try { + db.close(); + } catch (IOException e) { + /* ignore */ + } + } finally { + // Delete all files of this corrupt database and/or attached databases + if (attachedDbs != null) { + for (Pair<String, String> p : attachedDbs) { + deleteDatabaseFile(p.second); + } + } else { + // attachedDbs = null is possible when the database is so corrupt that even + // "PRAGMA database_list;" also fails. delete the main database file + deleteDatabaseFile(db.getPath()); + } + } + } + + private void deleteDatabaseFile(String fileName) { + if (fileName.equalsIgnoreCase(":memory:") || fileName.trim().length() == 0) { + return; + } + Log.w(TAG, "deleting the database file: " + fileName); + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + SQLiteDatabase.deleteDatabase(new File(fileName)); + } else { + try { + final boolean deleted = new File(fileName).delete(); + if (!deleted) { + Log.e(TAG, "Could not delete the database file " + fileName); + } + } catch (Exception error) { + Log.e(TAG, "error while deleting corrupted database file", error); + } + } + } catch (Exception e) { + /* print warning and ignore exception */ + Log.w(TAG, "delete failed: ", e); + } + } } /** @@ -211,33 +310,15 @@ public interface SupportSQLiteOpenHelper { @Nullable public final String name; /** - * Version number of the database (starting at 1); if the database is older, - * {@link SupportSQLiteOpenHelper.Callback#onUpgrade(SupportSQLiteDatabase, int, int)} - * will be used to upgrade the database; if the database is newer, - * {@link SupportSQLiteOpenHelper.Callback#onDowngrade(SupportSQLiteDatabase, int, int)} - * will be used to downgrade the database. - */ - public final int version; - /** * The callback class to handle creation, upgrade and downgrade. */ @NonNull public final SupportSQLiteOpenHelper.Callback callback; - /** - * The {@link DatabaseErrorHandler} to be used when sqlite reports database - * corruption, or null to use the default error handler. - */ - @Nullable - public final DatabaseErrorHandler errorHandler; - Configuration(@NonNull Context context, @Nullable String name, - int version, @Nullable DatabaseErrorHandler errorHandler, - @NonNull Callback callback) { + Configuration(@NonNull Context context, @Nullable String name, @NonNull Callback callback) { this.context = context; this.name = name; - this.version = version; this.callback = callback; - this.errorHandler = errorHandler; } /** @@ -255,9 +336,7 @@ public interface SupportSQLiteOpenHelper { public static class Builder { Context mContext; String mName; - int mVersion = 1; SupportSQLiteOpenHelper.Callback mCallback; - DatabaseErrorHandler mErrorHandler; public Configuration build() { if (mCallback == null) { @@ -268,11 +347,7 @@ public interface SupportSQLiteOpenHelper { throw new IllegalArgumentException("Must set a non-null context to create" + " the configuration."); } - if (mErrorHandler == null) { - mErrorHandler = new DefaultDatabaseErrorHandler(); - } - return new Configuration(mContext, mName, mVersion, mErrorHandler, - mCallback); + return new Configuration(mContext, mName, mCallback); } Builder(@NonNull Context context) { @@ -280,17 +355,6 @@ public interface SupportSQLiteOpenHelper { } /** - * @param errorHandler The {@link DatabaseErrorHandler} to be used when sqlite - * reports database corruption, or null to use the default error - * handler. - * @return This - */ - public Builder errorHandler(@Nullable DatabaseErrorHandler errorHandler) { - mErrorHandler = errorHandler; - return this; - } - - /** * @param name Name of the database file, or null for an in-memory database. * @return This */ @@ -307,20 +371,6 @@ public interface SupportSQLiteOpenHelper { mCallback = callback; return this; } - - /** - * @param version Version number of the database (starting at 1); if the database is - * older, - * {@link SupportSQLiteOpenHelper.Callback#onUpgrade(SupportSQLiteDatabase, int, int)} - * will be used to upgrade the database; if the database is newer, - * {@link SupportSQLiteOpenHelper.Callback#onDowngrade(SupportSQLiteDatabase, int, int)} - * will be used to downgrade the database. - * @return this - */ - public Builder version(int version) { - mVersion = version; - return this; - } } } diff --git a/android/arch/persistence/db/framework/FrameworkSQLiteDatabase.java b/android/arch/persistence/db/framework/FrameworkSQLiteDatabase.java index 92a58205..e9c2b741 100644 --- a/android/arch/persistence/db/framework/FrameworkSQLiteDatabase.java +++ b/android/arch/persistence/db/framework/FrameworkSQLiteDatabase.java @@ -53,8 +53,7 @@ class FrameworkSQLiteDatabase implements SupportSQLiteDatabase { * * @param delegate The delegate to receive all calls. */ - @SuppressWarnings("WeakerAccess") - public FrameworkSQLiteDatabase(SQLiteDatabase delegate) { + FrameworkSQLiteDatabase(SQLiteDatabase delegate) { mDelegate = delegate; } diff --git a/android/arch/persistence/db/framework/FrameworkSQLiteOpenHelper.java b/android/arch/persistence/db/framework/FrameworkSQLiteOpenHelper.java index aa08fa42..a1690f40 100644 --- a/android/arch/persistence/db/framework/FrameworkSQLiteOpenHelper.java +++ b/android/arch/persistence/db/framework/FrameworkSQLiteOpenHelper.java @@ -28,42 +28,14 @@ import android.support.annotation.RequiresApi; class FrameworkSQLiteOpenHelper implements SupportSQLiteOpenHelper { private final OpenHelper mDelegate; - FrameworkSQLiteOpenHelper(Context context, String name, int version, - DatabaseErrorHandler errorHandler, - SupportSQLiteOpenHelper.Callback callback) { - mDelegate = createDelegate(context, name, version, errorHandler, callback); + FrameworkSQLiteOpenHelper(Context context, String name, + Callback callback) { + mDelegate = createDelegate(context, name, callback); } - private OpenHelper createDelegate(Context context, String name, - int version, DatabaseErrorHandler errorHandler, - final Callback callback) { - return new OpenHelper(context, name, null, version, errorHandler) { - @Override - public void onCreate(SQLiteDatabase sqLiteDatabase) { - mWrappedDb = new FrameworkSQLiteDatabase(sqLiteDatabase); - callback.onCreate(mWrappedDb); - } - - @Override - public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) { - callback.onUpgrade(getWrappedDb(sqLiteDatabase), oldVersion, newVersion); - } - - @Override - public void onConfigure(SQLiteDatabase db) { - callback.onConfigure(getWrappedDb(db)); - } - - @Override - public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { - callback.onDowngrade(getWrappedDb(db), oldVersion, newVersion); - } - - @Override - public void onOpen(SQLiteDatabase db) { - callback.onOpen(getWrappedDb(db)); - } - }; + private OpenHelper createDelegate(Context context, String name, Callback callback) { + final FrameworkSQLiteDatabase[] dbRef = new FrameworkSQLiteDatabase[1]; + return new OpenHelper(context, name, dbRef, callback); } @Override @@ -92,14 +64,29 @@ class FrameworkSQLiteOpenHelper implements SupportSQLiteOpenHelper { mDelegate.close(); } - abstract static class OpenHelper extends SQLiteOpenHelper { - - FrameworkSQLiteDatabase mWrappedDb; - - OpenHelper(Context context, String name, - SQLiteDatabase.CursorFactory factory, int version, - DatabaseErrorHandler errorHandler) { - super(context, name, factory, version, errorHandler); + static class OpenHelper extends SQLiteOpenHelper { + /** + * This is used as an Object reference so that we can access the wrapped database inside + * the constructor. SQLiteOpenHelper requires the error handler to be passed in the + * constructor. + */ + final FrameworkSQLiteDatabase[] mDbRef; + final Callback mCallback; + + OpenHelper(Context context, String name, final FrameworkSQLiteDatabase[] dbRef, + final Callback callback) { + super(context, name, null, callback.version, + new DatabaseErrorHandler() { + @Override + public void onCorruption(SQLiteDatabase dbObj) { + FrameworkSQLiteDatabase db = dbRef[0]; + if (db != null) { + callback.onCorruption(db); + } + } + }); + mCallback = callback; + mDbRef = dbRef; } SupportSQLiteDatabase getWritableSupportDatabase() { @@ -113,16 +100,43 @@ class FrameworkSQLiteOpenHelper implements SupportSQLiteOpenHelper { } FrameworkSQLiteDatabase getWrappedDb(SQLiteDatabase sqLiteDatabase) { - if (mWrappedDb == null) { - mWrappedDb = new FrameworkSQLiteDatabase(sqLiteDatabase); + FrameworkSQLiteDatabase dbRef = mDbRef[0]; + if (dbRef == null) { + dbRef = new FrameworkSQLiteDatabase(sqLiteDatabase); + mDbRef[0] = dbRef; } - return mWrappedDb; + return mDbRef[0]; + } + + @Override + public void onCreate(SQLiteDatabase sqLiteDatabase) { + mCallback.onCreate(getWrappedDb(sqLiteDatabase)); + } + + @Override + public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) { + mCallback.onUpgrade(getWrappedDb(sqLiteDatabase), oldVersion, newVersion); + } + + @Override + public void onConfigure(SQLiteDatabase db) { + mCallback.onConfigure(getWrappedDb(db)); + } + + @Override + public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { + mCallback.onDowngrade(getWrappedDb(db), oldVersion, newVersion); + } + + @Override + public void onOpen(SQLiteDatabase db) { + mCallback.onOpen(getWrappedDb(db)); } @Override public synchronized void close() { super.close(); - mWrappedDb = null; + mDbRef[0] = null; } } } diff --git a/android/arch/persistence/db/framework/FrameworkSQLiteOpenHelperFactory.java b/android/arch/persistence/db/framework/FrameworkSQLiteOpenHelperFactory.java index 2268f45f..ab11d490 100644 --- a/android/arch/persistence/db/framework/FrameworkSQLiteOpenHelperFactory.java +++ b/android/arch/persistence/db/framework/FrameworkSQLiteOpenHelperFactory.java @@ -27,8 +27,6 @@ public final class FrameworkSQLiteOpenHelperFactory implements SupportSQLiteOpen @Override public SupportSQLiteOpenHelper create(SupportSQLiteOpenHelper.Configuration configuration) { return new FrameworkSQLiteOpenHelper( - configuration.context, configuration.name, - configuration.version, configuration.errorHandler, configuration.callback - ); + configuration.context, configuration.name, configuration.callback); } } diff --git a/android/arch/persistence/db/framework/FrameworkSQLiteStatement.java b/android/arch/persistence/db/framework/FrameworkSQLiteStatement.java index a2daf12e..53a04bd6 100644 --- a/android/arch/persistence/db/framework/FrameworkSQLiteStatement.java +++ b/android/arch/persistence/db/framework/FrameworkSQLiteStatement.java @@ -30,8 +30,7 @@ class FrameworkSQLiteStatement implements SupportSQLiteStatement { * * @param delegate The SQLiteStatement to delegate calls to. */ - @SuppressWarnings("WeakerAccess") - public FrameworkSQLiteStatement(SQLiteStatement delegate) { + FrameworkSQLiteStatement(SQLiteStatement delegate) { mDelegate = delegate; } diff --git a/android/arch/persistence/room/Entity.java b/android/arch/persistence/room/Entity.java index f54f0f80..94ca3bfc 100644 --- a/android/arch/persistence/room/Entity.java +++ b/android/arch/persistence/room/Entity.java @@ -36,6 +36,9 @@ import java.lang.annotation.Target; * When a class is marked as an Entity, all of its fields are persisted. If you would like to * exclude some of its fields, you can mark them with {@link Ignore}. * <p> + * If a field is {@code transient}, it is automatically ignored <b>unless</b> it is annotated with + * {@link ColumnInfo}, {@link Embedded} or {@link Relation}. + * <p> * Example: * <pre> * {@literal @}Entity diff --git a/android/arch/persistence/room/ForeignKey.java b/android/arch/persistence/room/ForeignKey.java index 4ba0fb3f..3ba632b5 100644 --- a/android/arch/persistence/room/ForeignKey.java +++ b/android/arch/persistence/room/ForeignKey.java @@ -40,7 +40,7 @@ import android.support.annotation.IntDef; * <a href="https://sqlite.org/pragma.html#pragma_defer_foreign_keys">defer_foreign_keys</a> PRAGMA * to defer them depending on your transaction. * <p> - * Please refer to the SQLite <a href="https://sqlite.org/foreignkeys.html>foreign keys</a> + * Please refer to the SQLite <a href="https://sqlite.org/foreignkeys.html">foreign keys</a> * documentation for details. */ public @interface ForeignKey { diff --git a/android/arch/persistence/room/InvalidationTracker.java b/android/arch/persistence/room/InvalidationTracker.java index 33bc4ed6..45ec0289 100644 --- a/android/arch/persistence/room/InvalidationTracker.java +++ b/android/arch/persistence/room/InvalidationTracker.java @@ -16,7 +16,7 @@ package android.arch.persistence.room; -import android.arch.core.executor.AppToolkitTaskExecutor; +import android.arch.core.executor.ArchTaskExecutor; import android.arch.core.internal.SafeIterableMap; import android.arch.persistence.db.SupportSQLiteDatabase; import android.arch.persistence.db.SupportSQLiteStatement; @@ -166,10 +166,12 @@ public class InvalidationTracker { private static void appendTriggerName(StringBuilder builder, String tableName, String triggerType) { - builder.append("room_table_modification_trigger_") + builder.append("`") + .append("room_table_modification_trigger_") .append(tableName) .append("_") - .append(triggerType); + .append(triggerType) + .append("`"); } private void stopTrackingTable(SupportSQLiteDatabase writableDb, int tableId) { @@ -192,9 +194,9 @@ public class InvalidationTracker { appendTriggerName(stringBuilder, tableName, trigger); stringBuilder.append(" AFTER ") .append(trigger) - .append(" ON ") + .append(" ON `") .append(tableName) - .append(" BEGIN INSERT OR REPLACE INTO ") + .append("` BEGIN INSERT OR REPLACE INTO ") .append(UPDATE_TABLE_NAME) .append(" VALUES(null, ") .append(tableId) @@ -238,7 +240,7 @@ public class InvalidationTracker { currentObserver = mObserverMap.putIfAbsent(observer, wrapper); } if (currentObserver == null && mObservedTableTracker.onAdded(tableIds)) { - AppToolkitTaskExecutor.getInstance().executeOnDiskIO(mSyncTriggers); + ArchTaskExecutor.getInstance().executeOnDiskIO(mSyncTriggers); } } @@ -269,7 +271,7 @@ public class InvalidationTracker { wrapper = mObserverMap.remove(observer); } if (wrapper != null && mObservedTableTracker.onRemoved(wrapper.mTableIds)) { - AppToolkitTaskExecutor.getInstance().executeOnDiskIO(mSyncTriggers); + ArchTaskExecutor.getInstance().executeOnDiskIO(mSyncTriggers); } } @@ -350,11 +352,18 @@ public class InvalidationTracker { return; } - if (mDatabase.inTransaction() - || !mPendingRefresh.compareAndSet(true, false)) { + if (!mPendingRefresh.compareAndSet(true, false)) { // no pending refresh return; } + + if (mDatabase.inTransaction()) { + // current thread is in a transaction. when it ends, it will invoke + // refreshRunnable again. mPendingRefresh is left as false on purpose + // so that the last transaction can flip it on again. + return; + } + mCleanupStatement.executeUpdateDelete(); mQueryArgs[0] = mMaxVersion; Cursor cursor = mDatabase.query(SELECT_UPDATED_TABLES_SQL, mQueryArgs); @@ -400,7 +409,7 @@ public class InvalidationTracker { public void refreshVersionsAsync() { // TODO we should consider doing this sync instead of async. if (mPendingRefresh.compareAndSet(false, true)) { - AppToolkitTaskExecutor.getInstance().executeOnDiskIO(mRefreshRunnable); + ArchTaskExecutor.getInstance().executeOnDiskIO(mRefreshRunnable); } } diff --git a/android/arch/persistence/room/InvalidationTrackerTest.java b/android/arch/persistence/room/InvalidationTrackerTest.java index f0b730ad..d7474fd1 100644 --- a/android/arch/persistence/room/InvalidationTrackerTest.java +++ b/android/arch/persistence/room/InvalidationTrackerTest.java @@ -247,7 +247,7 @@ public class InvalidationTrackerTest { mTracker.mRefreshRunnable.run(); } - @Test + // @Test - disabled due to flakiness b/65257997 public void closedDbAfterOpen() throws InterruptedException { setVersions(3, 1); mTracker.addObserver(new LatchObserver(1, "a", "b")); diff --git a/android/arch/persistence/room/RoomDatabase.java b/android/arch/persistence/room/RoomDatabase.java index e64f2d61..cdad868d 100644 --- a/android/arch/persistence/room/RoomDatabase.java +++ b/android/arch/persistence/room/RoomDatabase.java @@ -16,7 +16,7 @@ package android.arch.persistence.room; -import android.arch.core.executor.AppToolkitTaskExecutor; +import android.arch.core.executor.ArchTaskExecutor; import android.arch.persistence.db.SimpleSQLiteQuery; import android.arch.persistence.db.SupportSQLiteDatabase; import android.arch.persistence.db.SupportSQLiteOpenHelper; @@ -158,7 +158,7 @@ public abstract class RoomDatabase { if (mAllowMainThreadQueries) { return; } - if (AppToolkitTaskExecutor.getInstance().isMainThread()) { + if (ArchTaskExecutor.getInstance().isMainThread()) { throw new IllegalStateException("Cannot access database on the main thread since" + " it may potentially lock the UI for a long period of time."); } @@ -216,7 +216,11 @@ public abstract class RoomDatabase { */ public void endTransaction() { mOpenHelper.getWritableDatabase().endTransaction(); - mInvalidationTracker.refreshVersionsAsync(); + if (!inTransaction()) { + // enqueue refresh only if we are NOT in a transaction. Otherwise, wait for the last + // endTransaction call to do it. + mInvalidationTracker.refreshVersionsAsync(); + } } /** @@ -311,7 +315,6 @@ public abstract class RoomDatabase { private ArrayList<Callback> mCallbacks; private SupportSQLiteOpenHelper.Factory mFactory; - private boolean mInMemory; private boolean mAllowMainThreadQueries; private boolean mRequireMigration; /** @@ -381,6 +384,9 @@ public abstract class RoomDatabase { } /** + * Allows Room to destructively recreate database tables if {@link Migration}s that would + * migrate old database schemas to the latest schema version are not found. + * <p> * When the database version on the device does not match the latest schema version, Room * runs necessary {@link Migration}s on the database. * <p> diff --git a/android/arch/persistence/room/RoomOpenHelper.java b/android/arch/persistence/room/RoomOpenHelper.java index 8767f065..47279d60 100644 --- a/android/arch/persistence/room/RoomOpenHelper.java +++ b/android/arch/persistence/room/RoomOpenHelper.java @@ -44,6 +44,7 @@ public class RoomOpenHelper extends SupportSQLiteOpenHelper.Callback { public RoomOpenHelper(@NonNull DatabaseConfiguration configuration, @NonNull Delegate delegate, @NonNull String identityHash) { + super(delegate.version); mConfiguration = configuration; mDelegate = delegate; mIdentityHash = identityHash; @@ -135,6 +136,12 @@ public class RoomOpenHelper extends SupportSQLiteOpenHelper.Callback { */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public abstract static class Delegate { + public final int version; + + public Delegate(int version) { + this.version = version; + } + protected abstract void dropAllTables(SupportSQLiteDatabase database); protected abstract void createAllTables(SupportSQLiteDatabase database); diff --git a/android/arch/persistence/room/RoomWarnings.java b/android/arch/persistence/room/RoomWarnings.java index 91f32e45..c64be967 100644 --- a/android/arch/persistence/room/RoomWarnings.java +++ b/android/arch/persistence/room/RoomWarnings.java @@ -117,4 +117,12 @@ public class RoomWarnings { */ public static final String MISSING_INDEX_ON_FOREIGN_KEY_CHILD = "ROOM_MISSING_FOREIGN_KEY_CHILD_INDEX"; + + /** + * Reported when a Pojo has multiple constructors, one of which is a no-arg constructor. Room + * will pick that one by default but will print this warning in case the constructor choice is + * important. You can always guide Room to use the right constructor using the @Ignore + * annotation. + */ + public static final String DEFAULT_CONSTRUCTOR = "ROOM_DEFAULT_CONSTRUCTOR"; } diff --git a/android/arch/persistence/room/RxRoom.java b/android/arch/persistence/room/RxRoom.java index adfca27b..285b3f89 100644 --- a/android/arch/persistence/room/RxRoom.java +++ b/android/arch/persistence/room/RxRoom.java @@ -16,7 +16,7 @@ package android.arch.persistence.room; -import android.arch.core.executor.AppToolkitTaskExecutor; +import android.arch.core.executor.ArchTaskExecutor; import android.support.annotation.Nullable; import android.support.annotation.RestrictTo; @@ -133,7 +133,7 @@ public class RxRoom { public Disposable schedule(@NonNull Runnable run, long delay, @NonNull TimeUnit unit) { DisposableRunnable disposable = new DisposableRunnable(run, mDisposed); - AppToolkitTaskExecutor.getInstance().executeOnDiskIO(run); + ArchTaskExecutor.getInstance().executeOnDiskIO(run); return disposable; } diff --git a/android/arch/persistence/room/Transaction.java b/android/arch/persistence/room/Transaction.java new file mode 100644 index 00000000..914e4f41 --- /dev/null +++ b/android/arch/persistence/room/Transaction.java @@ -0,0 +1,51 @@ +/* + * 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.arch.persistence.room; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a method in an abstract {@link Dao} class as a transaction method. + * <p> + * The derived implementation of the method will execute the super method in a database transaction. + * All the parameters and return types are preserved. The transaction will be marked as successful + * unless an exception is thrown in the method body. + * <p> + * Example: + * <pre> + * {@literal @}Dao + * public abstract class ProductDao { + * {@literal @}Insert + * public abstract void insert(Product product); + * {@literal @}Delete + * public abstract void delete(Product product); + * {@literal @}Transaction + * public void insertAndDeleteInTransaction(Product newProduct, Product oldProduct) { + * // Anything inside this method runs in a single transaction. + * insert(newProduct); + * delete(oldProduct); + * } + * } + * </pre> + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.CLASS) +public @interface Transaction { +} diff --git a/android/arch/persistence/room/integration/testapp/CustomerViewModel.java b/android/arch/persistence/room/integration/testapp/CustomerViewModel.java index 1f434ad6..320b2cdd 100644 --- a/android/arch/persistence/room/integration/testapp/CustomerViewModel.java +++ b/android/arch/persistence/room/integration/testapp/CustomerViewModel.java @@ -17,7 +17,7 @@ package android.arch.persistence.room.integration.testapp; import android.app.Application; -import android.arch.core.executor.AppToolkitTaskExecutor; +import android.arch.core.executor.ArchTaskExecutor; import android.arch.lifecycle.AndroidViewModel; import android.arch.lifecycle.LiveData; import android.arch.paging.DataSource; @@ -47,7 +47,7 @@ public class CustomerViewModel extends AndroidViewModel { mDatabase = Room.databaseBuilder(this.getApplication(), SampleDatabase.class, "customerDatabase").build(); - AppToolkitTaskExecutor.getInstance().executeOnDiskIO(new Runnable() { + ArchTaskExecutor.getInstance().executeOnDiskIO(new Runnable() { @Override public void run() { // fill with some simple data @@ -73,7 +73,7 @@ public class CustomerViewModel extends AndroidViewModel { } void insertCustomer() { - AppToolkitTaskExecutor.getInstance().executeOnDiskIO(new Runnable() { + ArchTaskExecutor.getInstance().executeOnDiskIO(new Runnable() { @Override public void run() { mDatabase.getCustomerDao().insert(createCustomer()); diff --git a/android/arch/persistence/room/integration/testapp/PKeyTestDatabase.java b/android/arch/persistence/room/integration/testapp/PKeyTestDatabase.java index e61d808c..63b95072 100644 --- a/android/arch/persistence/room/integration/testapp/PKeyTestDatabase.java +++ b/android/arch/persistence/room/integration/testapp/PKeyTestDatabase.java @@ -23,13 +23,18 @@ import android.arch.persistence.room.Query; import android.arch.persistence.room.RoomDatabase; import android.arch.persistence.room.integration.testapp.vo.IntAutoIncPKeyEntity; import android.arch.persistence.room.integration.testapp.vo.IntegerAutoIncPKeyEntity; +import android.arch.persistence.room.integration.testapp.vo.IntegerPKeyEntity; +import android.arch.persistence.room.integration.testapp.vo.ObjectPKeyEntity; import java.util.List; -@Database(entities = {IntAutoIncPKeyEntity.class, IntegerAutoIncPKeyEntity.class}, version = 1, +@Database(entities = {IntAutoIncPKeyEntity.class, IntegerAutoIncPKeyEntity.class, + ObjectPKeyEntity.class, IntegerPKeyEntity.class}, version = 1, exportSchema = false) public abstract class PKeyTestDatabase extends RoomDatabase { public abstract IntPKeyDao intPKeyDao(); + public abstract IntegerAutoIncPKeyDao integerAutoIncPKeyDao(); + public abstract ObjectPKeyDao objectPKeyDao(); public abstract IntegerPKeyDao integerPKeyDao(); @Dao @@ -50,9 +55,10 @@ public abstract class PKeyTestDatabase extends RoomDatabase { } @Dao - public interface IntegerPKeyDao { + public interface IntegerAutoIncPKeyDao { @Insert - void insertMe(IntegerAutoIncPKeyEntity items); + void insertMe(IntegerAutoIncPKeyEntity item); + @Query("select * from IntegerAutoIncPKeyEntity WHERE pKey = :key") IntegerAutoIncPKeyEntity getMe(int key); @@ -65,4 +71,19 @@ public abstract class PKeyTestDatabase extends RoomDatabase { @Query("select data from IntegerAutoIncPKeyEntity WHERE pKey IN(:ids)") List<String> loadDataById(long... ids); } + + @Dao + public interface ObjectPKeyDao { + @Insert + void insertMe(ObjectPKeyEntity item); + } + + @Dao + public interface IntegerPKeyDao { + @Insert + void insertMe(IntegerPKeyEntity item); + + @Query("select * from IntegerPKeyEntity") + List<IntegerPKeyEntity> loadAll(); + } } diff --git a/android/arch/persistence/room/integration/testapp/TestDatabase.java b/android/arch/persistence/room/integration/testapp/TestDatabase.java index 94172965..2fad7b1f 100644 --- a/android/arch/persistence/room/integration/testapp/TestDatabase.java +++ b/android/arch/persistence/room/integration/testapp/TestDatabase.java @@ -21,6 +21,7 @@ import android.arch.persistence.room.RoomDatabase; import android.arch.persistence.room.TypeConverter; import android.arch.persistence.room.TypeConverters; import android.arch.persistence.room.integration.testapp.dao.BlobEntityDao; +import android.arch.persistence.room.integration.testapp.dao.FunnyNamedDao; import android.arch.persistence.room.integration.testapp.dao.PetCoupleDao; import android.arch.persistence.room.integration.testapp.dao.PetDao; import android.arch.persistence.room.integration.testapp.dao.ProductDao; @@ -31,6 +32,7 @@ import android.arch.persistence.room.integration.testapp.dao.UserDao; import android.arch.persistence.room.integration.testapp.dao.UserPetDao; import android.arch.persistence.room.integration.testapp.dao.WithClauseDao; import android.arch.persistence.room.integration.testapp.vo.BlobEntity; +import android.arch.persistence.room.integration.testapp.vo.FunnyNamedEntity; import android.arch.persistence.room.integration.testapp.vo.Pet; import android.arch.persistence.room.integration.testapp.vo.PetCouple; import android.arch.persistence.room.integration.testapp.vo.Product; @@ -41,7 +43,7 @@ import android.arch.persistence.room.integration.testapp.vo.User; import java.util.Date; @Database(entities = {User.class, Pet.class, School.class, PetCouple.class, Toy.class, - BlobEntity.class, Product.class}, + BlobEntity.class, Product.class, FunnyNamedEntity.class}, version = 1, exportSchema = false) @TypeConverters(TestDatabase.Converters.class) public abstract class TestDatabase extends RoomDatabase { @@ -55,6 +57,7 @@ public abstract class TestDatabase extends RoomDatabase { public abstract ProductDao getProductDao(); public abstract SpecificDogDao getSpecificDogDao(); public abstract WithClauseDao getWithClauseDao(); + public abstract FunnyNamedDao getFunnyNamedDao(); @SuppressWarnings("unused") public static class Converters { diff --git a/android/arch/persistence/room/integration/testapp/dao/FunnyNamedDao.java b/android/arch/persistence/room/integration/testapp/dao/FunnyNamedDao.java new file mode 100644 index 00000000..93b5e72e --- /dev/null +++ b/android/arch/persistence/room/integration/testapp/dao/FunnyNamedDao.java @@ -0,0 +1,50 @@ +/* + * 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.arch.persistence.room.integration.testapp.dao; + +import static android.arch.persistence.room.integration.testapp.vo.FunnyNamedEntity.COLUMN_ID; +import static android.arch.persistence.room.integration.testapp.vo.FunnyNamedEntity.TABLE_NAME; + +import android.arch.lifecycle.LiveData; +import android.arch.persistence.room.Dao; +import android.arch.persistence.room.Delete; +import android.arch.persistence.room.Insert; +import android.arch.persistence.room.Query; +import android.arch.persistence.room.Update; +import android.arch.persistence.room.integration.testapp.vo.FunnyNamedEntity; + +import java.util.List; + +@Dao +public interface FunnyNamedDao { + String SELECT_ONE = "select * from \"" + TABLE_NAME + "\" WHERE \"" + COLUMN_ID + "\" = :id"; + @Insert + void insert(FunnyNamedEntity... entities); + @Delete + void delete(FunnyNamedEntity... entities); + @Update + void update(FunnyNamedEntity... entities); + + @Query("select * from \"" + TABLE_NAME + "\" WHERE \"" + COLUMN_ID + "\" IN (:ids)") + List<FunnyNamedEntity> loadAll(int... ids); + + @Query(SELECT_ONE) + LiveData<FunnyNamedEntity> observableOne(int id); + + @Query(SELECT_ONE) + FunnyNamedEntity load(int id); +} diff --git a/android/arch/persistence/room/integration/testapp/dao/SchoolDao.java b/android/arch/persistence/room/integration/testapp/dao/SchoolDao.java index 7bb137fe..18e8d93e 100644 --- a/android/arch/persistence/room/integration/testapp/dao/SchoolDao.java +++ b/android/arch/persistence/room/integration/testapp/dao/SchoolDao.java @@ -35,16 +35,16 @@ public abstract class SchoolDao { @Query("SELECT * from School WHERE address_street LIKE '%' || :street || '%'") public abstract List<School> findByStreet(String street); - @Query("SELECT mName, manager_mName FROM School") + @Query("SELECT mId, mName, manager_mName FROM School") public abstract List<School> schoolAndManagerNames(); - @Query("SELECT mName, manager_mName FROM School") + @Query("SELECT mId, mName, manager_mName FROM School") @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH) public abstract List<SchoolRef> schoolAndManagerNamesAsPojo(); @Query("SELECT address_lat as lat, address_lng as lng FROM School WHERE mId = :schoolId") public abstract Coordinates loadCoordinates(int schoolId); - @Query("SELECT address_lat, address_lng FROM School WHERE mId = :schoolId") + @Query("SELECT mId, address_lat, address_lng FROM School WHERE mId = :schoolId") public abstract School loadCoordinatesAsSchool(int schoolId); } diff --git a/android/arch/persistence/room/integration/testapp/dao/UserDao.java b/android/arch/persistence/room/integration/testapp/dao/UserDao.java index 337c233f..665a1aeb 100644 --- a/android/arch/persistence/room/integration/testapp/dao/UserDao.java +++ b/android/arch/persistence/room/integration/testapp/dao/UserDao.java @@ -24,6 +24,7 @@ import android.arch.persistence.room.Delete; import android.arch.persistence.room.Insert; import android.arch.persistence.room.OnConflictStrategy; import android.arch.persistence.room.Query; +import android.arch.persistence.room.Transaction; import android.arch.persistence.room.Update; import android.arch.persistence.room.integration.testapp.TestDatabase; import android.arch.persistence.room.integration.testapp.vo.AvgWeightByAge; @@ -259,4 +260,10 @@ public abstract class UserDao { + " WHERE mLastName > :lastName or (mLastName = :lastName and (mName < :name or (mName = :name and mId > :id)))" + " ORDER BY mLastName ASC, mName DESC, mId ASC") public abstract int userComplexCountBefore(String lastName, String name, int id); + + @Transaction + public void insertBothByAnnotation(final User a, final User b) { + insert(a); + insert(b); + } } diff --git a/android/arch/persistence/room/integration/testapp/dao/UserPetDao.java b/android/arch/persistence/room/integration/testapp/dao/UserPetDao.java index 3507aeea..eb159014 100644 --- a/android/arch/persistence/room/integration/testapp/dao/UserPetDao.java +++ b/android/arch/persistence/room/integration/testapp/dao/UserPetDao.java @@ -33,6 +33,8 @@ import android.arch.persistence.room.integration.testapp.vo.UserWithPetsAndToys; import java.util.List; +import io.reactivex.Flowable; + @Dao public interface UserPetDao { @Query("SELECT * FROM User u, Pet p WHERE u.mId = p.mUserId") @@ -62,6 +64,9 @@ public interface UserPetDao { @Query("SELECT * FROM User u where u.mId = :userId") LiveData<UserAndAllPets> liveUserWithPets(int userId); + @Query("SELECT * FROM User u where u.mId = :userId") + Flowable<UserAndAllPets> flowableUserWithPets(int userId); + @Query("SELECT * FROM User u where u.mId = :uid") EmbeddedUserAndAllPets loadUserAndPetsAsEmbedded(int uid); diff --git a/android/arch/persistence/room/integration/testapp/dao/WithClauseDao.java b/android/arch/persistence/room/integration/testapp/dao/WithClauseDao.java index b1c38eda..40098ed4 100644 --- a/android/arch/persistence/room/integration/testapp/dao/WithClauseDao.java +++ b/android/arch/persistence/room/integration/testapp/dao/WithClauseDao.java @@ -16,13 +16,16 @@ package android.arch.persistence.room.integration.testapp.dao; +import android.annotation.TargetApi; import android.arch.lifecycle.LiveData; import android.arch.persistence.room.Dao; import android.arch.persistence.room.Query; +import android.os.Build; import java.util.List; @Dao +@TargetApi(Build.VERSION_CODES.LOLLIPOP) public interface WithClauseDao { @Query("WITH RECURSIVE factorial(n, fact) AS \n" + "(SELECT 0, 1 \n" diff --git a/android/arch/persistence/room/integration/testapp/database/SampleDatabase.java b/android/arch/persistence/room/integration/testapp/database/SampleDatabase.java index eec59f6a..9020eb16 100644 --- a/android/arch/persistence/room/integration/testapp/database/SampleDatabase.java +++ b/android/arch/persistence/room/integration/testapp/database/SampleDatabase.java @@ -18,10 +18,6 @@ package android.arch.persistence.room.integration.testapp.database; import android.arch.persistence.room.Database; import android.arch.persistence.room.RoomDatabase; -import android.arch.persistence.room.TypeConverter; -import android.arch.persistence.room.TypeConverters; - -import java.util.Date; /** * Sample database of customers. diff --git a/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java b/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java index 725d53f8..7fe2bc94 100644 --- a/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java +++ b/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java @@ -318,6 +318,8 @@ public class MigrationTest { + " (`id` INTEGER NOT NULL, `name` TEXT COLLATE NOCASE, PRIMARY KEY(`id`)," + " FOREIGN KEY(`name`) REFERENCES `Entity1`(`name`)" + " ON UPDATE NO ACTION ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED)"); + database.execSQL("CREATE UNIQUE INDEX `index_entity1` ON " + + MigrationDb.Entity1.TABLE_NAME + " (`name`)"); } }; diff --git a/android/arch/persistence/room/integration/testapp/paging/LivePagedListProviderTest.java b/android/arch/persistence/room/integration/testapp/paging/LivePagedListProviderTest.java index 4c9d73e1..df70a170 100644 --- a/android/arch/persistence/room/integration/testapp/paging/LivePagedListProviderTest.java +++ b/android/arch/persistence/room/integration/testapp/paging/LivePagedListProviderTest.java @@ -21,17 +21,17 @@ import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; -import android.arch.core.executor.AppToolkitTaskExecutor; +import android.arch.core.executor.ArchTaskExecutor; import android.arch.core.executor.testing.CountingTaskExecutorRule; import android.arch.lifecycle.Lifecycle; import android.arch.lifecycle.LifecycleOwner; import android.arch.lifecycle.LifecycleRegistry; import android.arch.lifecycle.LiveData; import android.arch.lifecycle.Observer; +import android.arch.paging.PagedList; import android.arch.persistence.room.integration.testapp.test.TestDatabaseTest; import android.arch.persistence.room.integration.testapp.test.TestUtil; import android.arch.persistence.room.integration.testapp.vo.User; -import android.arch.paging.PagedList; import android.support.annotation.Nullable; import android.support.test.filters.LargeTest; import android.support.test.runner.AndroidJUnit4; @@ -131,7 +131,7 @@ public class LivePagedListProviderTest extends TestDatabaseTest { return null; } }); - AppToolkitTaskExecutor.getInstance().executeOnMainThread(futureTask); + ArchTaskExecutor.getInstance().executeOnMainThread(futureTask); futureTask.get(); } @@ -155,7 +155,7 @@ public class LivePagedListProviderTest extends TestDatabaseTest { private static class PagedListObserver<T> implements Observer<PagedList<T>> { private PagedList<T> mList; - public void reset() { + void reset() { mList = null; } diff --git a/android/arch/persistence/room/integration/testapp/test/CustomDatabaseTest.java b/android/arch/persistence/room/integration/testapp/test/CustomDatabaseTest.java index 353c2e39..6f44546b 100644 --- a/android/arch/persistence/room/integration/testapp/test/CustomDatabaseTest.java +++ b/android/arch/persistence/room/integration/testapp/test/CustomDatabaseTest.java @@ -55,7 +55,6 @@ public class CustomDatabaseTest { Customer customer = new Customer(); for (int i = 0; i < 100; i++) { SampleDatabase db = builder.build(); - customer.setId(i); db.getCustomerDao().insert(customer); // Give InvalidationTracker enough time to start #mRefreshRunnable and pass the // initialization check. diff --git a/android/arch/persistence/room/integration/testapp/test/FunnyNamedDaoTest.java b/android/arch/persistence/room/integration/testapp/test/FunnyNamedDaoTest.java new file mode 100644 index 00000000..f4fca7f2 --- /dev/null +++ b/android/arch/persistence/room/integration/testapp/test/FunnyNamedDaoTest.java @@ -0,0 +1,96 @@ +/* + * 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.arch.persistence.room.integration.testapp.test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +import android.arch.core.executor.testing.CountingTaskExecutorRule; +import android.arch.lifecycle.Observer; +import android.arch.persistence.room.integration.testapp.vo.FunnyNamedEntity; +import android.support.annotation.Nullable; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class FunnyNamedDaoTest extends TestDatabaseTest { + @Rule + public CountingTaskExecutorRule mExecutorRule = new CountingTaskExecutorRule(); + + @Test + public void readWrite() { + FunnyNamedEntity entity = new FunnyNamedEntity(1, "a"); + mFunnyNamedDao.insert(entity); + FunnyNamedEntity loaded = mFunnyNamedDao.load(1); + assertThat(loaded, is(entity)); + } + + @Test + public void update() { + FunnyNamedEntity entity = new FunnyNamedEntity(1, "a"); + mFunnyNamedDao.insert(entity); + entity.setValue("b"); + mFunnyNamedDao.update(entity); + FunnyNamedEntity loaded = mFunnyNamedDao.load(1); + assertThat(loaded.getValue(), is("b")); + } + + @Test + public void delete() { + FunnyNamedEntity entity = new FunnyNamedEntity(1, "a"); + mFunnyNamedDao.insert(entity); + assertThat(mFunnyNamedDao.load(1), notNullValue()); + mFunnyNamedDao.delete(entity); + assertThat(mFunnyNamedDao.load(1), nullValue()); + } + + @Test + public void observe() throws TimeoutException, InterruptedException { + final FunnyNamedEntity[] item = new FunnyNamedEntity[1]; + mFunnyNamedDao.observableOne(2).observeForever(new Observer<FunnyNamedEntity>() { + @Override + public void onChanged(@Nullable FunnyNamedEntity funnyNamedEntity) { + item[0] = funnyNamedEntity; + } + }); + + FunnyNamedEntity entity = new FunnyNamedEntity(1, "a"); + mFunnyNamedDao.insert(entity); + mExecutorRule.drainTasks(1, TimeUnit.MINUTES); + assertThat(item[0], nullValue()); + + final FunnyNamedEntity entity2 = new FunnyNamedEntity(2, "b"); + mFunnyNamedDao.insert(entity2); + mExecutorRule.drainTasks(1, TimeUnit.MINUTES); + assertThat(item[0], is(entity2)); + + final FunnyNamedEntity entity3 = new FunnyNamedEntity(2, "c"); + mFunnyNamedDao.update(entity3); + mExecutorRule.drainTasks(1, TimeUnit.MINUTES); + assertThat(item[0], is(entity3)); + } +} diff --git a/android/arch/persistence/room/integration/testapp/test/InvalidationTest.java b/android/arch/persistence/room/integration/testapp/test/InvalidationTest.java index 4787ce52..84f20ec5 100644 --- a/android/arch/persistence/room/integration/testapp/test/InvalidationTest.java +++ b/android/arch/persistence/room/integration/testapp/test/InvalidationTest.java @@ -21,7 +21,7 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.collection.IsCollectionWithSize.hasSize; -import android.arch.core.executor.AppToolkitTaskExecutor; +import android.arch.core.executor.ArchTaskExecutor; import android.arch.core.executor.TaskExecutor; import android.arch.persistence.room.InvalidationTracker; import android.arch.persistence.room.Room; @@ -68,7 +68,7 @@ public class InvalidationTest { @Before public void setSingleThreadedIO() { - AppToolkitTaskExecutor.getInstance().setDelegate(new TaskExecutor() { + ArchTaskExecutor.getInstance().setDelegate(new TaskExecutor() { ExecutorService mIOExecutor = Executors.newSingleThreadExecutor(); Handler mHandler = new Handler(Looper.getMainLooper()); @@ -91,7 +91,7 @@ public class InvalidationTest { @After public void clearExecutor() { - AppToolkitTaskExecutor.getInstance().setDelegate(null); + ArchTaskExecutor.getInstance().setDelegate(null); } private void waitUntilIOThreadIsIdle() { @@ -101,7 +101,7 @@ public class InvalidationTest { return null; } }); - AppToolkitTaskExecutor.getInstance().executeOnDiskIO(future); + ArchTaskExecutor.getInstance().executeOnDiskIO(future); //noinspection TryWithIdenticalCatches try { future.get(); diff --git a/android/arch/persistence/room/integration/testapp/test/LiveDataQueryTest.java b/android/arch/persistence/room/integration/testapp/test/LiveDataQueryTest.java index cae8445b..d78411f8 100644 --- a/android/arch/persistence/room/integration/testapp/test/LiveDataQueryTest.java +++ b/android/arch/persistence/room/integration/testapp/test/LiveDataQueryTest.java @@ -21,7 +21,7 @@ import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; -import android.arch.core.executor.AppToolkitTaskExecutor; +import android.arch.core.executor.ArchTaskExecutor; import android.arch.core.executor.testing.CountingTaskExecutorRule; import android.arch.lifecycle.Lifecycle; import android.arch.lifecycle.LifecycleOwner; @@ -35,9 +35,11 @@ import android.arch.persistence.room.integration.testapp.vo.PetsToys; import android.arch.persistence.room.integration.testapp.vo.Toy; import android.arch.persistence.room.integration.testapp.vo.User; import android.arch.persistence.room.integration.testapp.vo.UserAndAllPets; +import android.os.Build; import android.support.annotation.Nullable; import android.support.test.InstrumentationRegistry; import android.support.test.filters.MediumTest; +import android.support.test.filters.SdkSuppress; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; @@ -235,6 +237,7 @@ public class LiveDataQueryTest extends TestDatabaseTest { } @Test + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP) public void withWithClause() throws ExecutionException, InterruptedException, TimeoutException { LiveData<List<String>> actual = @@ -322,7 +325,7 @@ public class LiveDataQueryTest extends TestDatabaseTest { return null; } }); - AppToolkitTaskExecutor.getInstance().executeOnMainThread(futureTask); + ArchTaskExecutor.getInstance().executeOnMainThread(futureTask); futureTask.get(); } diff --git a/android/arch/persistence/room/integration/testapp/test/PrimaryKeyTest.java b/android/arch/persistence/room/integration/testapp/test/PrimaryKeyTest.java index 97ce10c2..fda43732 100644 --- a/android/arch/persistence/room/integration/testapp/test/PrimaryKeyTest.java +++ b/android/arch/persistence/room/integration/testapp/test/PrimaryKeyTest.java @@ -16,29 +16,35 @@ package android.arch.persistence.room.integration.testapp.test; +import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; - -import android.support.test.InstrumentationRegistry; -import android.support.test.filters.SmallTest; -import android.support.test.runner.AndroidJUnit4; +import static org.junit.Assert.assertNotNull; import android.arch.persistence.room.Room; import android.arch.persistence.room.integration.testapp.PKeyTestDatabase; import android.arch.persistence.room.integration.testapp.vo.IntAutoIncPKeyEntity; import android.arch.persistence.room.integration.testapp.vo.IntegerAutoIncPKeyEntity; +import android.arch.persistence.room.integration.testapp.vo.IntegerPKeyEntity; +import android.arch.persistence.room.integration.testapp.vo.ObjectPKeyEntity; +import android.database.sqlite.SQLiteConstraintException; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import java.util.Arrays; +import java.util.List; @RunWith(AndroidJUnit4.class) @SmallTest public class PrimaryKeyTest { private PKeyTestDatabase mDatabase; + @Before public void setup() { mDatabase = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getTargetContext(), @@ -49,8 +55,8 @@ public class PrimaryKeyTest { public void integerTest() { IntegerAutoIncPKeyEntity entity = new IntegerAutoIncPKeyEntity(); entity.data = "foo"; - mDatabase.integerPKeyDao().insertMe(entity); - IntegerAutoIncPKeyEntity loaded = mDatabase.integerPKeyDao().getMe(1); + mDatabase.integerAutoIncPKeyDao().insertMe(entity); + IntegerAutoIncPKeyEntity loaded = mDatabase.integerAutoIncPKeyDao().getMe(1); assertThat(loaded, notNullValue()); assertThat(loaded.data, is(entity.data)); } @@ -60,8 +66,8 @@ public class PrimaryKeyTest { IntegerAutoIncPKeyEntity entity = new IntegerAutoIncPKeyEntity(); entity.pKey = 0; entity.data = "foo"; - mDatabase.integerPKeyDao().insertMe(entity); - IntegerAutoIncPKeyEntity loaded = mDatabase.integerPKeyDao().getMe(0); + mDatabase.integerAutoIncPKeyDao().insertMe(entity); + IntegerAutoIncPKeyEntity loaded = mDatabase.integerAutoIncPKeyDao().getMe(0); assertThat(loaded, notNullValue()); assertThat(loaded.data, is(entity.data)); } @@ -98,8 +104,8 @@ public class PrimaryKeyTest { public void getInsertedIdFromInteger() { IntegerAutoIncPKeyEntity entity = new IntegerAutoIncPKeyEntity(); entity.data = "foo"; - final long id = mDatabase.integerPKeyDao().insertAndGetId(entity); - assertThat(mDatabase.integerPKeyDao().getMe((int) id).data, is("foo")); + final long id = mDatabase.integerAutoIncPKeyDao().insertAndGetId(entity); + assertThat(mDatabase.integerAutoIncPKeyDao().getMe((int) id).data, is("foo")); } @Test @@ -108,7 +114,34 @@ public class PrimaryKeyTest { entity.data = "foo"; IntegerAutoIncPKeyEntity entity2 = new IntegerAutoIncPKeyEntity(); entity2.data = "foo2"; - final long[] ids = mDatabase.integerPKeyDao().insertAndGetIds(entity, entity2); - assertThat(mDatabase.integerPKeyDao().loadDataById(ids), is(Arrays.asList("foo", "foo2"))); + final long[] ids = mDatabase.integerAutoIncPKeyDao().insertAndGetIds(entity, entity2); + assertThat(mDatabase.integerAutoIncPKeyDao().loadDataById(ids), + is(Arrays.asList("foo", "foo2"))); + } + + @Test + public void insertNullPrimaryKey() throws Exception { + ObjectPKeyEntity o1 = new ObjectPKeyEntity(null, "1"); + + Throwable throwable = null; + try { + mDatabase.objectPKeyDao().insertMe(o1); + } catch (Throwable t) { + throwable = t; + } + assertNotNull("Was expecting an exception", throwable); + assertThat(throwable, instanceOf(SQLiteConstraintException.class)); + } + + @Test + public void insertNullPrimaryKeyForInteger() throws Exception { + IntegerPKeyEntity entity = new IntegerPKeyEntity(); + entity.data = "data"; + mDatabase.integerPKeyDao().insertMe(entity); + + List<IntegerPKeyEntity> list = mDatabase.integerPKeyDao().loadAll(); + assertThat(list.size(), is(1)); + assertThat(list.get(0).data, is("data")); + assertNotNull(list.get(0).pKey); } } diff --git a/android/arch/persistence/room/integration/testapp/test/RxJava2Test.java b/android/arch/persistence/room/integration/testapp/test/RxJava2Test.java index 1bbc1406..01d071e7 100644 --- a/android/arch/persistence/room/integration/testapp/test/RxJava2Test.java +++ b/android/arch/persistence/room/integration/testapp/test/RxJava2Test.java @@ -19,10 +19,12 @@ package android.arch.persistence.room.integration.testapp.test; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; -import android.arch.core.executor.AppToolkitTaskExecutor; +import android.arch.core.executor.ArchTaskExecutor; import android.arch.core.executor.TaskExecutor; import android.arch.persistence.room.EmptyResultSetException; +import android.arch.persistence.room.integration.testapp.vo.Pet; import android.arch.persistence.room.integration.testapp.vo.User; +import android.arch.persistence.room.integration.testapp.vo.UserAndAllPets; import android.support.test.filters.MediumTest; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; @@ -38,6 +40,7 @@ import java.util.Collections; import java.util.List; import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Predicate; import io.reactivex.observers.TestObserver; import io.reactivex.schedulers.TestScheduler; import io.reactivex.subscribers.TestSubscriber; @@ -52,7 +55,7 @@ public class RxJava2Test extends TestDatabaseTest { public void setupSchedulers() { mTestScheduler = new TestScheduler(); mTestScheduler.start(); - AppToolkitTaskExecutor.getInstance().setDelegate(new TaskExecutor() { + ArchTaskExecutor.getInstance().setDelegate(new TaskExecutor() { @Override public void executeOnDiskIO(Runnable runnable) { mTestScheduler.scheduleDirect(runnable); @@ -73,7 +76,7 @@ public class RxJava2Test extends TestDatabaseTest { @After public void clearSchedulers() { mTestScheduler.shutdown(); - AppToolkitTaskExecutor.getInstance().setDelegate(null); + ArchTaskExecutor.getInstance().setDelegate(null); } private void drain() throws InterruptedException { @@ -269,4 +272,60 @@ public class RxJava2Test extends TestDatabaseTest { subscriber.cancel(); subscriber.assertNoErrors(); } + + @Test + public void flowableWithRelation() throws InterruptedException { + final TestSubscriber<UserAndAllPets> subscriber = new TestSubscriber<>(); + + mUserPetDao.flowableUserWithPets(3).subscribe(subscriber); + drain(); + subscriber.assertSubscribed(); + + drain(); + subscriber.assertNoValues(); + + final User user = TestUtil.createUser(3); + mUserDao.insert(user); + drain(); + subscriber.assertValue(new Predicate<UserAndAllPets>() { + @Override + public boolean test(UserAndAllPets userAndAllPets) throws Exception { + return userAndAllPets.user.equals(user); + } + }); + subscriber.assertValueCount(1); + final Pet[] pets = TestUtil.createPetsForUser(3, 1, 2); + mPetDao.insertAll(pets); + drain(); + subscriber.assertValueAt(1, new Predicate<UserAndAllPets>() { + @Override + public boolean test(UserAndAllPets userAndAllPets) throws Exception { + return userAndAllPets.user.equals(user) + && userAndAllPets.pets.equals(Arrays.asList(pets)); + } + }); + } + + @Test + public void flowable_updateInTransaction() throws InterruptedException { + // When subscribing to the emissions of the user + final TestSubscriber<User> userTestSubscriber = mUserDao + .flowableUserById(3) + .observeOn(mTestScheduler) + .test(); + drain(); + userTestSubscriber.assertValueCount(0); + + // When inserting a new user in the data source + mDatabase.beginTransaction(); + try { + mUserDao.insert(TestUtil.createUser(3)); + mDatabase.setTransactionSuccessful(); + + } finally { + mDatabase.endTransaction(); + } + drain(); + userTestSubscriber.assertValueCount(1); + } } diff --git a/android/arch/persistence/room/integration/testapp/test/RxJava2WithInstantTaskExecutorTest.java b/android/arch/persistence/room/integration/testapp/test/RxJava2WithInstantTaskExecutorTest.java new file mode 100644 index 00000000..fcd0b004 --- /dev/null +++ b/android/arch/persistence/room/integration/testapp/test/RxJava2WithInstantTaskExecutorTest.java @@ -0,0 +1,70 @@ +/* + * 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.arch.persistence.room.integration.testapp.test; + +import android.arch.core.executor.testing.InstantTaskExecutorRule; +import android.arch.persistence.room.Room; +import android.arch.persistence.room.integration.testapp.TestDatabase; +import android.arch.persistence.room.integration.testapp.vo.User; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import io.reactivex.subscribers.TestSubscriber; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class RxJava2WithInstantTaskExecutorTest { + @Rule + public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule(); + + private TestDatabase mDatabase; + + @Before + public void initDb() throws Exception { + // using an in-memory database because the information stored here disappears when the + // process is killed + mDatabase = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getContext(), + TestDatabase.class) + // allowing main thread queries, just for testing + .allowMainThreadQueries() + .build(); + } + + @Test + public void testFlowableInTransaction() { + // When subscribing to the emissions of the user + TestSubscriber<User> subscriber = mDatabase.getUserDao().flowableUserById(3).test(); + subscriber.assertValueCount(0); + + // When inserting a new user in the data source + mDatabase.beginTransaction(); + try { + mDatabase.getUserDao().insert(TestUtil.createUser(3)); + mDatabase.setTransactionSuccessful(); + } finally { + mDatabase.endTransaction(); + } + + subscriber.assertValueCount(1); + } +} diff --git a/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java b/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java index 8861adbc..f8049f35 100644 --- a/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java +++ b/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java @@ -502,4 +502,26 @@ public class SimpleEntityReadWriteTest { assertThat(mUserDao.updateByAgeAndIds(3f, 30, Arrays.asList(3, 5)), is(1)); assertThat(mUserDao.loadByIds(3)[0].getWeight(), is(3f)); } + + @Test + public void transactionByAnnotation() { + User a = TestUtil.createUser(3); + User b = TestUtil.createUser(5); + mUserDao.insertBothByAnnotation(a, b); + assertThat(mUserDao.count(), is(2)); + } + + @Test + public void transactionByAnnotation_failure() { + User a = TestUtil.createUser(3); + User b = TestUtil.createUser(3); + boolean caught = false; + try { + mUserDao.insertBothByAnnotation(a, b); + } catch (SQLiteConstraintException e) { + caught = true; + } + assertTrue("SQLiteConstraintException expected", caught); + assertThat(mUserDao.count(), is(0)); + } } diff --git a/android/arch/persistence/room/integration/testapp/test/TestDatabaseTest.java b/android/arch/persistence/room/integration/testapp/test/TestDatabaseTest.java index 51d5bb33..ec775617 100644 --- a/android/arch/persistence/room/integration/testapp/test/TestDatabaseTest.java +++ b/android/arch/persistence/room/integration/testapp/test/TestDatabaseTest.java @@ -18,6 +18,7 @@ package android.arch.persistence.room.integration.testapp.test; import android.arch.persistence.room.Room; import android.arch.persistence.room.integration.testapp.TestDatabase; +import android.arch.persistence.room.integration.testapp.dao.FunnyNamedDao; import android.arch.persistence.room.integration.testapp.dao.PetCoupleDao; import android.arch.persistence.room.integration.testapp.dao.PetDao; import android.arch.persistence.room.integration.testapp.dao.SchoolDao; @@ -42,6 +43,7 @@ public abstract class TestDatabaseTest { protected ToyDao mToyDao; protected SpecificDogDao mSpecificDogDao; protected WithClauseDao mWithClauseDao; + protected FunnyNamedDao mFunnyNamedDao; @Before public void createDb() { @@ -55,5 +57,6 @@ public abstract class TestDatabaseTest { mToyDao = mDatabase.getToyDao(); mSpecificDogDao = mDatabase.getSpecificDogDao(); mWithClauseDao = mDatabase.getWithClauseDao(); + mFunnyNamedDao = mDatabase.getFunnyNamedDao(); } } diff --git a/android/arch/persistence/room/integration/testapp/test/WithClauseTest.java b/android/arch/persistence/room/integration/testapp/test/WithClauseTest.java index 10897da1..92096380 100644 --- a/android/arch/persistence/room/integration/testapp/test/WithClauseTest.java +++ b/android/arch/persistence/room/integration/testapp/test/WithClauseTest.java @@ -20,6 +20,8 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import android.arch.persistence.room.integration.testapp.vo.User; +import android.os.Build; +import android.support.test.filters.SdkSuppress; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; @@ -32,6 +34,7 @@ import java.util.List; @RunWith(AndroidJUnit4.class) @SmallTest +@SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP) public class WithClauseTest extends TestDatabaseTest{ @Test public void noSourceOfData() { diff --git a/android/arch/persistence/room/integration/testapp/vo/FunnyNamedEntity.java b/android/arch/persistence/room/integration/testapp/vo/FunnyNamedEntity.java new file mode 100644 index 00000000..20f3c216 --- /dev/null +++ b/android/arch/persistence/room/integration/testapp/vo/FunnyNamedEntity.java @@ -0,0 +1,75 @@ +/* + * 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.arch.persistence.room.integration.testapp.vo; + +import android.arch.persistence.room.ColumnInfo; +import android.arch.persistence.room.Entity; +import android.arch.persistence.room.PrimaryKey; + +/** + * An entity that was weird names + */ +@Entity(tableName = FunnyNamedEntity.TABLE_NAME) +public class FunnyNamedEntity { + public static final String TABLE_NAME = "funny but not so funny"; + public static final String COLUMN_ID = "_this $is id$"; + public static final String COLUMN_VALUE = "unlikely-Ωşå¨ıünames"; + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = COLUMN_ID) + private int mId; + @ColumnInfo(name = COLUMN_VALUE) + private String mValue; + + public FunnyNamedEntity(int id, String value) { + mId = id; + mValue = value; + } + + public int getId() { + return mId; + } + + public void setId(int id) { + mId = id; + } + + public String getValue() { + return mValue; + } + + public void setValue(String value) { + mValue = value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + FunnyNamedEntity entity = (FunnyNamedEntity) o; + + if (mId != entity.mId) return false; + return mValue != null ? mValue.equals(entity.mValue) : entity.mValue == null; + } + + @Override + public int hashCode() { + int result = mId; + result = 31 * result + (mValue != null ? mValue.hashCode() : 0); + return result; + } +} diff --git a/android/arch/persistence/room/integration/testapp/vo/IntegerPKeyEntity.java b/android/arch/persistence/room/integration/testapp/vo/IntegerPKeyEntity.java new file mode 100644 index 00000000..cae1843e --- /dev/null +++ b/android/arch/persistence/room/integration/testapp/vo/IntegerPKeyEntity.java @@ -0,0 +1,27 @@ +/* + * 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.arch.persistence.room.integration.testapp.vo; + +import android.arch.persistence.room.Entity; +import android.arch.persistence.room.PrimaryKey; + +@Entity +public class IntegerPKeyEntity { + @PrimaryKey + public Integer pKey; + public String data; +} diff --git a/android/arch/persistence/room/integration/testapp/vo/ObjectPKeyEntity.java b/android/arch/persistence/room/integration/testapp/vo/ObjectPKeyEntity.java new file mode 100644 index 00000000..895a35a2 --- /dev/null +++ b/android/arch/persistence/room/integration/testapp/vo/ObjectPKeyEntity.java @@ -0,0 +1,34 @@ +/* + * 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.arch.persistence.room.integration.testapp.vo; + +import android.arch.persistence.room.Entity; +import android.arch.persistence.room.PrimaryKey; +import android.support.annotation.NonNull; + +@Entity +public class ObjectPKeyEntity { + @PrimaryKey + @NonNull + public String pKey; + public String data; + + public ObjectPKeyEntity(String pKey, String data) { + this.pKey = pKey; + this.data = data; + } +} diff --git a/android/arch/persistence/room/integration/testapp/vo/PetCouple.java b/android/arch/persistence/room/integration/testapp/vo/PetCouple.java index f27b1313..e5208ed7 100644 --- a/android/arch/persistence/room/integration/testapp/vo/PetCouple.java +++ b/android/arch/persistence/room/integration/testapp/vo/PetCouple.java @@ -20,11 +20,13 @@ import android.arch.persistence.room.Embedded; import android.arch.persistence.room.Entity; import android.arch.persistence.room.PrimaryKey; import android.arch.persistence.room.RoomWarnings; +import android.support.annotation.NonNull; @Entity @SuppressWarnings(RoomWarnings.PRIMARY_KEY_FROM_EMBEDDED_IS_DROPPED) public class PetCouple { @PrimaryKey + @NonNull public String id; @Embedded(prefix = "male_") public Pet male; diff --git a/android/arch/persistence/room/migration/TableInfoTest.java b/android/arch/persistence/room/migration/TableInfoTest.java index c6eade55..d88c02fd 100644 --- a/android/arch/persistence/room/migration/TableInfoTest.java +++ b/android/arch/persistence/room/migration/TableInfoTest.java @@ -37,6 +37,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import java.io.IOException; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -179,6 +180,35 @@ public class TableInfoTest { Collections.<TableInfo.ForeignKey>emptySet()))); } + @Test + public void readIndices() { + mDb = createDatabase( + "CREATE TABLE foo (n INTEGER, indexed TEXT, unique_indexed TEXT," + + "a INTEGER, b INTEGER);", + "CREATE INDEX foo_indexed ON foo(indexed);", + "CREATE UNIQUE INDEX foo_unique_indexed ON foo(unique_indexed COLLATE NOCASE" + + " DESC);", + "CREATE INDEX " + TableInfo.Index.DEFAULT_PREFIX + "foo_composite_indexed" + + " ON foo(a, b);" + ); + TableInfo info = TableInfo.read(mDb, "foo"); + assertThat(info, is(new TableInfo( + "foo", + toMap(new TableInfo.Column("n", "INTEGER", false, 0), + new TableInfo.Column("indexed", "TEXT", false, 0), + new TableInfo.Column("unique_indexed", "TEXT", false, 0), + new TableInfo.Column("a", "INTEGER", false, 0), + new TableInfo.Column("b", "INTEGER", false, 0)), + Collections.<TableInfo.ForeignKey>emptySet(), + toSet(new TableInfo.Index("index_foo_blahblah", false, + Arrays.asList("a", "b")), + new TableInfo.Index("foo_unique_indexed", true, + Arrays.asList("unique_indexed")), + new TableInfo.Index("foo_indexed", false, + Arrays.asList("indexed")))) + )); + } + private static Map<String, TableInfo.Column> toMap(TableInfo.Column... columns) { Map<String, TableInfo.Column> result = new HashMap<>(); for (TableInfo.Column column : columns) { @@ -187,6 +217,14 @@ public class TableInfoTest { return result; } + private static <T> Set<T> toSet(T... ts) { + final HashSet<T> result = new HashSet<T>(); + for (T t : ts) { + result.add(t); + } + return result; + } + @After public void closeDb() throws IOException { if (mDb != null && mDb.isOpen()) { @@ -199,8 +237,7 @@ public class TableInfoTest { SupportSQLiteOpenHelper.Configuration .builder(InstrumentationRegistry.getTargetContext()) .name(null) - .version(1) - .callback(new SupportSQLiteOpenHelper.Callback() { + .callback(new SupportSQLiteOpenHelper.Callback(1) { @Override public void onCreate(SupportSQLiteDatabase db) { for (String query : queries) { diff --git a/android/arch/persistence/room/package-info.java b/android/arch/persistence/room/package-info.java index faaa952b..1dafc1b2 100644 --- a/android/arch/persistence/room/package-info.java +++ b/android/arch/persistence/room/package-info.java @@ -39,8 +39,8 @@ * database row. For each {@link android.arch.persistence.room.Entity Entity}, a database table * is created to hold the items. The Entity class must be referenced in the * {@link android.arch.persistence.room.Database#entities() Database#entities} array. Each field - * of the Entity is persisted in the database unless it is annotated with - * {@link android.arch.persistence.room.Ignore Ignore}. Entities must have no-arg constructors. + * of the Entity (and its super class) is persisted in the database unless it is denoted + * otherwise (see {@link android.arch.persistence.room.Entity Entity} docs for details). * </li> * <li>{@link android.arch.persistence.room.Dao Dao}: This annotation marks a class or interface * as a Data Access Object. Data access objects are the main component of Room that are diff --git a/android/arch/persistence/room/testing/MigrationTestHelper.java b/android/arch/persistence/room/testing/MigrationTestHelper.java index aea3e96e..18e0a146 100644 --- a/android/arch/persistence/room/testing/MigrationTestHelper.java +++ b/android/arch/persistence/room/testing/MigrationTestHelper.java @@ -29,6 +29,7 @@ import android.arch.persistence.room.migration.bundle.DatabaseBundle; import android.arch.persistence.room.migration.bundle.EntityBundle; import android.arch.persistence.room.migration.bundle.FieldBundle; import android.arch.persistence.room.migration.bundle.ForeignKeyBundle; +import android.arch.persistence.room.migration.bundle.IndexBundle; import android.arch.persistence.room.migration.bundle.SchemaBundle; import android.arch.persistence.room.util.TableInfo; import android.content.Context; @@ -146,7 +147,7 @@ public class MigrationTestHelper extends TestWatcher { RoomOpenHelper roomOpenHelper = new RoomOpenHelper(configuration, new CreatingDelegate(schemaBundle.getDatabase()), schemaBundle.getDatabase().getIdentityHash()); - return openDatabase(name, version, roomOpenHelper); + return openDatabase(name, roomOpenHelper); } /** @@ -189,17 +190,15 @@ public class MigrationTestHelper extends TestWatcher { RoomOpenHelper roomOpenHelper = new RoomOpenHelper(configuration, new MigratingDelegate(schemaBundle.getDatabase(), validateDroppedTables), schemaBundle.getDatabase().getIdentityHash()); - return openDatabase(name, version, roomOpenHelper); + return openDatabase(name, roomOpenHelper); } - private SupportSQLiteDatabase openDatabase(String name, int version, - RoomOpenHelper roomOpenHelper) { + private SupportSQLiteDatabase openDatabase(String name, RoomOpenHelper roomOpenHelper) { SupportSQLiteOpenHelper.Configuration config = SupportSQLiteOpenHelper.Configuration .builder(mInstrumentation.getTargetContext()) .callback(roomOpenHelper) .name(name) - .version(version) .build(); SupportSQLiteDatabase db = mOpenFactory.create(config).getWritableDatabase(); mManagedDatabases.add(new WeakReference<>(db)); @@ -287,7 +286,19 @@ public class MigrationTestHelper extends TestWatcher { private static TableInfo toTableInfo(EntityBundle entityBundle) { return new TableInfo(entityBundle.getTableName(), toColumnMap(entityBundle), - toForeignKeys(entityBundle.getForeignKeys())); + toForeignKeys(entityBundle.getForeignKeys()), toIndices(entityBundle.getIndices())); + } + + private static Set<TableInfo.Index> toIndices(List<IndexBundle> indices) { + if (indices == null) { + return Collections.emptySet(); + } + Set<TableInfo.Index> result = new HashSet<>(); + for (IndexBundle bundle : indices) { + result.add(new TableInfo.Index(bundle.getName(), bundle.isUnique(), + bundle.getColumnNames())); + } + return result; } private static Set<TableInfo.ForeignKey> toForeignKeys( @@ -401,6 +412,7 @@ public class MigrationTestHelper extends TestWatcher { final DatabaseBundle mDatabaseBundle; RoomOpenHelperDelegate(DatabaseBundle databaseBundle) { + super(databaseBundle.getVersion()); mDatabaseBundle = databaseBundle; } diff --git a/android/arch/persistence/room/util/TableInfo.java b/android/arch/persistence/room/util/TableInfo.java index bcd2e9ef..a115147d 100644 --- a/android/arch/persistence/room/util/TableInfo.java +++ b/android/arch/persistence/room/util/TableInfo.java @@ -20,6 +20,7 @@ import android.arch.persistence.db.SupportSQLiteDatabase; import android.database.Cursor; import android.os.Build; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.annotation.RestrictTo; import java.util.ArrayList; @@ -29,6 +30,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeMap; /** * A data class that holds the information about a table. @@ -56,11 +58,70 @@ public class TableInfo { public final Set<ForeignKey> foreignKeys; + /** + * Sometimes, Index information is not available (older versions). If so, we skip their + * verification. + */ + @Nullable + public final Set<Index> indices; + @SuppressWarnings("unused") - public TableInfo(String name, Map<String, Column> columns, Set<ForeignKey> foreignKeys) { + public TableInfo(String name, Map<String, Column> columns, Set<ForeignKey> foreignKeys, + Set<Index> indices) { this.name = name; this.columns = Collections.unmodifiableMap(columns); this.foreignKeys = Collections.unmodifiableSet(foreignKeys); + this.indices = indices == null ? null : Collections.unmodifiableSet(indices); + } + + /** + * For backward compatibility with dbs created with older versions. + */ + @SuppressWarnings("unused") + public TableInfo(String name, Map<String, Column> columns, Set<ForeignKey> foreignKeys) { + this(name, columns, foreignKeys, Collections.<Index>emptySet()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + TableInfo tableInfo = (TableInfo) o; + + if (name != null ? !name.equals(tableInfo.name) : tableInfo.name != null) return false; + if (columns != null ? !columns.equals(tableInfo.columns) : tableInfo.columns != null) { + return false; + } + if (foreignKeys != null ? !foreignKeys.equals(tableInfo.foreignKeys) + : tableInfo.foreignKeys != null) { + return false; + } + if (indices == null || tableInfo.indices == null) { + // if one us is missing index information, seems like we couldn't acquire the + // information so we better skip. + return true; + } + return indices.equals(tableInfo.indices); + } + + @Override + public int hashCode() { + int result = name != null ? name.hashCode() : 0; + result = 31 * result + (columns != null ? columns.hashCode() : 0); + result = 31 * result + (foreignKeys != null ? foreignKeys.hashCode() : 0); + // skip index, it is not reliable for comparison. + return result; + } + + @Override + public String toString() { + return "TableInfo{" + + "name='" + name + '\'' + + ", columns=" + columns + + ", foreignKeys=" + foreignKeys + + ", indices=" + indices + + '}'; } /** @@ -74,7 +135,8 @@ public class TableInfo { public static TableInfo read(SupportSQLiteDatabase database, String tableName) { Map<String, Column> columns = readColumns(database, tableName); Set<ForeignKey> foreignKeys = readForeignKeys(database, tableName); - return new TableInfo(tableName, columns, foreignKeys); + Set<Index> indices = readIndices(database, tableName); + return new TableInfo(tableName, columns, foreignKeys, indices); } private static Set<ForeignKey> readForeignKeys(SupportSQLiteDatabase database, @@ -167,34 +229,74 @@ public class TableInfo { return columns; } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - TableInfo tableInfo = (TableInfo) o; - - if (!name.equals(tableInfo.name)) return false; - //noinspection SimplifiableIfStatement - if (!columns.equals(tableInfo.columns)) return false; - return foreignKeys.equals(tableInfo.foreignKeys); + /** + * @return null if we cannot read the indices due to older sqlite implementations. + */ + @Nullable + private static Set<Index> readIndices(SupportSQLiteDatabase database, String tableName) { + Cursor cursor = database.query("PRAGMA index_list(`" + tableName + "`)"); + try { + final int nameColumnIndex = cursor.getColumnIndex("name"); + final int originColumnIndex = cursor.getColumnIndex("origin"); + final int uniqueIndex = cursor.getColumnIndex("unique"); + if (nameColumnIndex == -1 || originColumnIndex == -1 || uniqueIndex == -1) { + // we cannot read them so better not validate any index. + return null; + } + HashSet<Index> indices = new HashSet<>(); + while (cursor.moveToNext()) { + String origin = cursor.getString(originColumnIndex); + if (!"c".equals(origin)) { + // Ignore auto-created indices + continue; + } + String name = cursor.getString(nameColumnIndex); + boolean unique = cursor.getInt(uniqueIndex) == 1; + Index index = readIndex(database, name, unique); + if (index == null) { + // we cannot read it properly so better not read it + return null; + } + indices.add(index); + } + return indices; + } finally { + cursor.close(); + } } - @Override - public int hashCode() { - int result = name.hashCode(); - result = 31 * result + columns.hashCode(); - result = 31 * result + foreignKeys.hashCode(); - return result; - } + /** + * @return null if we cannot read the index due to older sqlite implementations. + */ + @Nullable + private static Index readIndex(SupportSQLiteDatabase database, String name, boolean unique) { + Cursor cursor = database.query("PRAGMA index_xinfo(`" + name + "`)"); + try { + final int seqnoColumnIndex = cursor.getColumnIndex("seqno"); + final int cidColumnIndex = cursor.getColumnIndex("cid"); + final int nameColumnIndex = cursor.getColumnIndex("name"); + if (seqnoColumnIndex == -1 || cidColumnIndex == -1 || nameColumnIndex == -1) { + // we cannot read them so better not validate any index. + return null; + } + final TreeMap<Integer, String> results = new TreeMap<>(); - @Override - public String toString() { - return "TableInfo{" - + "name='" + name + '\'' - + ", columns=" + columns - + ", foreignKeys=" + foreignKeys - + '}'; + while (cursor.moveToNext()) { + int cid = cursor.getInt(cidColumnIndex); + if (cid < 0) { + // Ignore SQLite row ID + continue; + } + int seq = cursor.getInt(seqnoColumnIndex); + String columnName = cursor.getString(nameColumnIndex); + results.put(seq, columnName); + } + final List<String> columns = new ArrayList<>(results.size()); + columns.addAll(results.values()); + return new Index(name, unique, columns); + } finally { + cursor.close(); + } } /** @@ -379,4 +481,65 @@ public class TableInfo { } } } + + /** + * Holds the information about an SQLite index + * + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + public static class Index { + // should match the value in Index.kt + public static final String DEFAULT_PREFIX = "index_"; + public final String name; + public final boolean unique; + public final List<String> columns; + + public Index(String name, boolean unique, List<String> columns) { + this.name = name; + this.unique = unique; + this.columns = columns; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Index index = (Index) o; + if (unique != index.unique) { + return false; + } + if (!columns.equals(index.columns)) { + return false; + } + if (name.startsWith(Index.DEFAULT_PREFIX)) { + return index.name.startsWith(Index.DEFAULT_PREFIX); + } else { + return name.equals(index.name); + } + } + + @Override + public int hashCode() { + int result; + if (name.startsWith(DEFAULT_PREFIX)) { + result = DEFAULT_PREFIX.hashCode(); + } else { + result = name.hashCode(); + } + result = 31 * result + (unique ? 1 : 0); + result = 31 * result + columns.hashCode(); + return result; + } + + @Override + public String toString() { + return "Index{" + + "name='" + name + '\'' + + ", unique=" + unique + + ", columns=" + columns + + '}'; + } + } } |