summaryrefslogtreecommitdiff
path: root/android/arch/persistence
diff options
context:
space:
mode:
Diffstat (limited to 'android/arch/persistence')
-rw-r--r--android/arch/persistence/db/SupportSQLiteOpenHelper.java158
-rw-r--r--android/arch/persistence/db/framework/FrameworkSQLiteDatabase.java3
-rw-r--r--android/arch/persistence/db/framework/FrameworkSQLiteOpenHelper.java106
-rw-r--r--android/arch/persistence/db/framework/FrameworkSQLiteOpenHelperFactory.java4
-rw-r--r--android/arch/persistence/db/framework/FrameworkSQLiteStatement.java3
-rw-r--r--android/arch/persistence/room/Entity.java3
-rw-r--r--android/arch/persistence/room/ForeignKey.java2
-rw-r--r--android/arch/persistence/room/InvalidationTracker.java29
-rw-r--r--android/arch/persistence/room/InvalidationTrackerTest.java2
-rw-r--r--android/arch/persistence/room/RoomDatabase.java14
-rw-r--r--android/arch/persistence/room/RoomOpenHelper.java7
-rw-r--r--android/arch/persistence/room/RoomWarnings.java8
-rw-r--r--android/arch/persistence/room/RxRoom.java4
-rw-r--r--android/arch/persistence/room/Transaction.java51
-rw-r--r--android/arch/persistence/room/integration/testapp/CustomerViewModel.java6
-rw-r--r--android/arch/persistence/room/integration/testapp/PKeyTestDatabase.java27
-rw-r--r--android/arch/persistence/room/integration/testapp/TestDatabase.java5
-rw-r--r--android/arch/persistence/room/integration/testapp/dao/FunnyNamedDao.java50
-rw-r--r--android/arch/persistence/room/integration/testapp/dao/SchoolDao.java6
-rw-r--r--android/arch/persistence/room/integration/testapp/dao/UserDao.java7
-rw-r--r--android/arch/persistence/room/integration/testapp/dao/UserPetDao.java5
-rw-r--r--android/arch/persistence/room/integration/testapp/dao/WithClauseDao.java3
-rw-r--r--android/arch/persistence/room/integration/testapp/database/SampleDatabase.java4
-rw-r--r--android/arch/persistence/room/integration/testapp/migration/MigrationTest.java2
-rw-r--r--android/arch/persistence/room/integration/testapp/paging/LivePagedListProviderTest.java8
-rw-r--r--android/arch/persistence/room/integration/testapp/test/CustomDatabaseTest.java1
-rw-r--r--android/arch/persistence/room/integration/testapp/test/FunnyNamedDaoTest.java96
-rw-r--r--android/arch/persistence/room/integration/testapp/test/InvalidationTest.java8
-rw-r--r--android/arch/persistence/room/integration/testapp/test/LiveDataQueryTest.java7
-rw-r--r--android/arch/persistence/room/integration/testapp/test/PrimaryKeyTest.java57
-rw-r--r--android/arch/persistence/room/integration/testapp/test/RxJava2Test.java65
-rw-r--r--android/arch/persistence/room/integration/testapp/test/RxJava2WithInstantTaskExecutorTest.java70
-rw-r--r--android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java22
-rw-r--r--android/arch/persistence/room/integration/testapp/test/TestDatabaseTest.java3
-rw-r--r--android/arch/persistence/room/integration/testapp/test/WithClauseTest.java3
-rw-r--r--android/arch/persistence/room/integration/testapp/vo/FunnyNamedEntity.java75
-rw-r--r--android/arch/persistence/room/integration/testapp/vo/IntegerPKeyEntity.java27
-rw-r--r--android/arch/persistence/room/integration/testapp/vo/ObjectPKeyEntity.java34
-rw-r--r--android/arch/persistence/room/integration/testapp/vo/PetCouple.java2
-rw-r--r--android/arch/persistence/room/migration/TableInfoTest.java41
-rw-r--r--android/arch/persistence/room/package-info.java4
-rw-r--r--android/arch/persistence/room/testing/MigrationTestHelper.java24
-rw-r--r--android/arch/persistence/room/util/TableInfo.java217
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
+ + '}';
+ }
+ }
}