Fix schema identity has to be stable

Room v1 has a bug where it generates the schema identity from the
create table query, which was OK when it was written (because Room
considered column order as part of schema) but not anymore (since
we don't enforce column order).

The problem is fixed by creating both legacy and new identity hash
so that RoomOpenHelper can validate old database.

Unfortunately, this is a large CL because we don't want to update
schema json files so all of the Schema data classes implements a
new schema equality api which checks if the schema description of
two entities are the same, even though their SQL might be different.
(e.g. the column order in an entity or the auto generated index name)

Since we are not overriding json files, the fix will only take effect
after a migration.

Bug: 64290754
Test: android.arch.persistence.room.migration.bundle.*
Change-Id: I44959a353ac919850e2606ca704008ea57da7313
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Database.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Database.kt
index 02e83b3..2cffbf3 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Database.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Database.kt
@@ -56,6 +56,12 @@
      * ensure developer didn't forget to update the version.
      */
     val identityHash: String by lazy {
+        val idKey = SchemaIdentityKey()
+        idKey.appendSorted(entities)
+        idKey.hash()
+    }
+
+    val legacyIdentityHash: String by lazy {
         val entityDescriptions = entities
                 .sortedBy { it.tableName }
                 .map { it.createTableQuery }
@@ -71,6 +77,14 @@
 
     fun exportSchema(file: File) {
         val schemaBundle = SchemaBundle(SchemaBundle.LATEST_FORMAT, bundle)
+        if (file.exists()) {
+            val existing = file.inputStream().use {
+                SchemaBundle.deserialize(it)
+            }
+            if (existing.isSchemaEqual(schemaBundle)) {
+                return
+            }
+        }
         SchemaBundle.serialize(schemaBundle, file)
     }
 }
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Entity.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Entity.kt
index 48592bd..b855f96 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Entity.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Entity.kt
@@ -22,18 +22,30 @@
 import javax.lang.model.type.DeclaredType
 
 // TODO make data class when move to kotlin 1.1
-class Entity(element: TypeElement, val tableName: String, type: DeclaredType,
-             fields: List<Field>, embeddedFields: List<EmbeddedField>,
-             val primaryKey: PrimaryKey, val indices: List<Index>,
-             val foreignKeys: List<ForeignKey>,
-             constructor: Constructor?)
-    : Pojo(element, type, fields, embeddedFields, emptyList(), constructor) {
+class Entity(
+        element: TypeElement, val tableName: String, type: DeclaredType,
+        fields: List<Field>, embeddedFields: List<EmbeddedField>,
+        val primaryKey: PrimaryKey, val indices: List<Index>,
+        val foreignKeys: List<ForeignKey>,
+        constructor: Constructor?)
+    : Pojo(element, type, fields, embeddedFields, emptyList(), constructor), HasSchemaIdentity {
 
     val createTableQuery by lazy {
         createTableQuery(tableName)
     }
 
-    fun createTableQuery(tableName: String): String {
+    // a string defining the identity of this entity, which can be used for equality checks
+    override fun getIdKey(): String {
+        val identityKey = SchemaIdentityKey()
+        identityKey.append(tableName)
+        identityKey.append(primaryKey)
+        identityKey.appendSorted(fields)
+        identityKey.appendSorted(indices)
+        identityKey.appendSorted(foreignKeys)
+        return identityKey.hash()
+    }
+
+    private fun createTableQuery(tableName: String): String {
         val definitions = (fields.map {
             val autoIncrement = primaryKey.autoGenerateId && primaryKey.fields.contains(it)
             it.databaseDefinition(autoIncrement)
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Field.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Field.kt
index 838016e..43e0605 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Field.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Field.kt
@@ -35,7 +35,7 @@
                  * embedded child of the main Pojo*/
                  val parent: EmbeddedField? = null,
                  // index might be removed when being merged into an Entity
-                 var indexed: Boolean = false) {
+                 var indexed: Boolean = false) : HasSchemaIdentity {
     lateinit var getter: FieldGetter
     lateinit var setter: FieldSetter
     // binds the field into a statement
@@ -47,6 +47,11 @@
     /** Whether the table column for this field should be NOT NULL */
     val nonNull = element.isNonNull() && (parent == null || parent.isNonNullRecursively())
 
+    override fun getIdKey(): String {
+        // we don't get the collate information from sqlite so ignoring it here.
+        return "$columnName-${affinity?.name ?: SQLTypeAffinity.TEXT.name}-$nonNull"
+    }
+
     /**
      * Used when reporting errors on duplicate names
      */
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/ForeignKey.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/ForeignKey.kt
index 66cf3a0..833de97 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/ForeignKey.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/ForeignKey.kt
@@ -26,7 +26,16 @@
                       val childFields: List<Field>,
                       val onDelete: ForeignKeyAction,
                       val onUpdate: ForeignKeyAction,
-                      val deferred: Boolean) {
+                      val deferred: Boolean) : HasSchemaIdentity {
+    override fun getIdKey(): String {
+        return parentTable +
+                "-${parentColumns.joinToString(",")}" +
+                "-${childFields.joinToString(",") {it.columnName}}" +
+                "-${onDelete.sqlName}" +
+                "-${onUpdate.sqlName}" +
+                "-$deferred"
+    }
+
     fun databaseDefinition(): String {
         return "FOREIGN KEY(${joinEscaped(childFields.map { it.columnName })})" +
                 " REFERENCES `$parentTable`(${joinEscaped(parentColumns)})" +
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Index.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Index.kt
index 694c627..b4952cf 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Index.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Index.kt
@@ -22,11 +22,17 @@
 /**
  * Represents a processed index.
  */
-data class Index(val name: String, val unique: Boolean, val fields: List<Field>) {
+data class Index(val name: String, val unique: Boolean, val fields: List<Field>) :
+        HasSchemaIdentity {
     companion object {
         // should match the value in TableInfo.Index.DEFAULT_PREFIX
         const val DEFAULT_PREFIX = "index_"
     }
+
+    override fun getIdKey(): String {
+        return "$unique-$name-${fields.joinToString(",") { it.columnName }}"
+    }
+
     fun createQuery(tableName: String): String {
         val uniqueSQL = if (unique) {
             "UNIQUE"
@@ -35,7 +41,7 @@
         }
         return """
             CREATE $uniqueSQL INDEX `$name`
-            ON `$tableName` (${fields.map { it.columnName }.joinToString(", ") { "`$it`"}})
+            ON `$tableName` (${fields.map { it.columnName }.joinToString(", ") { "`$it`" }})
             """.trimIndent().replace("\n", " ")
     }
 
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/PrimaryKey.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/PrimaryKey.kt
index d1a2c21..1d76d40 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/PrimaryKey.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/PrimaryKey.kt
@@ -22,7 +22,7 @@
  * Represents a PrimaryKey for an Entity.
  */
 data class PrimaryKey(val declaredIn: Element?, val fields: List<Field>,
-                      val autoGenerateId: Boolean) {
+                      val autoGenerateId: Boolean) : HasSchemaIdentity {
     companion object {
         val MISSING = PrimaryKey(null, emptyList(), false)
     }
@@ -36,4 +36,8 @@
 
     fun toBundle(): PrimaryKeyBundle = PrimaryKeyBundle(
             autoGenerateId, fields.map { it.columnName })
+
+    override fun getIdKey(): String {
+        return "$autoGenerateId-${fields.map { it.columnName }}"
+    }
 }
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/SchemaIdentityKey.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/SchemaIdentityKey.kt
new file mode 100644
index 0000000..06c9ff5
--- /dev/null
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/SchemaIdentityKey.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 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.vo
+
+import org.apache.commons.codec.digest.DigestUtils
+import java.util.Locale
+
+interface HasSchemaIdentity {
+    fun getIdKey(): String
+}
+
+/**
+ * A class that can be converted into a unique identifier for an object
+ */
+class SchemaIdentityKey {
+    companion object {
+        private val SEPARATOR = "?:?"
+        private val ENGLISH_SORT = Comparator<String> { o1, o2 ->
+            o1.toLowerCase(Locale.ENGLISH).compareTo(o2.toLowerCase(Locale.ENGLISH))
+        }
+    }
+
+    private val sb = StringBuilder()
+    fun append(identity: HasSchemaIdentity) {
+        append(identity.getIdKey())
+    }
+
+    fun appendSorted(identities: List<HasSchemaIdentity>) {
+        identities.map { it.getIdKey() }.sortedWith(ENGLISH_SORT).forEach {
+            append(it)
+        }
+    }
+
+    fun hash() = DigestUtils.md5Hex(sb.toString())
+    fun append(identity: String) {
+        sb.append(identity).append(SEPARATOR)
+    }
+}
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/SQLiteOpenHelperWriter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/SQLiteOpenHelperWriter.kt
index 16fcd9c..d62cc1c 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/SQLiteOpenHelperWriter.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/SQLiteOpenHelperWriter.kt
@@ -40,10 +40,10 @@
         scope.builder().apply {
             val sqliteConfigVar = scope.getTmpVar("_sqliteConfig")
             val callbackVar = scope.getTmpVar("_openCallback")
-            addStatement("final $T $L = new $T($N, $L, $S)",
+            addStatement("final $T $L = new $T($N, $L, $S, $S)",
                     SupportDbTypeNames.SQLITE_OPEN_HELPER_CALLBACK,
                     callbackVar, RoomTypeNames.OPEN_HELPER, configuration,
-                    createOpenCallback(scope), database.identityHash)
+                    createOpenCallback(scope), database.identityHash, database.legacyIdentityHash)
             // build configuration
             addStatement(
                     """
diff --git a/room/compiler/src/test/data/databasewriter/output/ComplexDatabase.java b/room/compiler/src/test/data/databasewriter/output/ComplexDatabase.java
index cfdc110..db2b450 100644
--- a/room/compiler/src/test/data/databasewriter/output/ComplexDatabase.java
+++ b/room/compiler/src/test/data/databasewriter/output/ComplexDatabase.java
@@ -28,7 +28,7 @@
             public void createAllTables(SupportSQLiteDatabase _db) {
                 _db.execSQL("CREATE TABLE IF NOT EXISTS `User` (`uid` INTEGER NOT NULL, `name` TEXT, `lastName` TEXT, `ageColumn` INTEGER NOT NULL, PRIMARY KEY(`uid`))");
                 _db.execSQL("CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)");
-                _db.execSQL("INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"6773601c5bcf94c71ee4eb0de04f21a4\")");
+                _db.execSQL("INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"cd8098a1e968898879c194cef2dff8f7\")");
             }
 
             public void dropAllTables(SupportSQLiteDatabase _db) {
@@ -69,7 +69,7 @@
                             + " Found:\n" + _existingUser);
                 }
             }
-        }, "6773601c5bcf94c71ee4eb0de04f21a4");
+        }, "cd8098a1e968898879c194cef2dff8f7", "6773601c5bcf94c71ee4eb0de04f21a4");
         final SupportSQLiteOpenHelper.Configuration _sqliteConfig = SupportSQLiteOpenHelper.Configuration.builder(configuration.context)
                 .name(configuration.name)
                 .callback(_openCallback)
@@ -96,4 +96,4 @@
             }
         }
     }
-}
+}
\ No newline at end of file
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java
index 7fe2bc9..e6db52b 100644
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java
@@ -19,6 +19,7 @@
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.CoreMatchers.instanceOf;
 import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
 import static org.hamcrest.CoreMatchers.nullValue;
 import static org.hamcrest.MatcherAssert.assertThat;
 
@@ -33,6 +34,7 @@
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
 
+import org.hamcrest.MatcherAssert;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -252,6 +254,30 @@
         db.close();
     }
 
+    @Test
+    public void failWithIdentityCheck() throws IOException {
+        for (int i = 1; i < MigrationDb.LATEST_VERSION; i++) {
+            String name = "test_" + i;
+            helper.createDatabase(name, i).close();
+            IllegalStateException exception = null;
+            try {
+                MigrationDb db = Room.databaseBuilder(
+                        InstrumentationRegistry.getInstrumentation().getTargetContext(),
+                        MigrationDb.class, name).build();
+                db.runInTransaction(new Runnable() {
+                    @Override
+                    public void run() {
+                        // do nothing
+                    }
+                });
+            } catch (IllegalStateException ex) {
+                exception = ex;
+            }
+            MatcherAssert.assertThat("identity detection should've failed",
+                    exception, notNullValue());
+        }
+    }
+
     private void testFailure(int startVersion, int endVersion) throws IOException {
         final SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, startVersion);
         db.close();
diff --git a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/DatabaseBundle.java b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/DatabaseBundle.java
index 4ac9029..f131838 100644
--- a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/DatabaseBundle.java
+++ b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/DatabaseBundle.java
@@ -32,7 +32,7 @@
  * @hide
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class DatabaseBundle {
+public class DatabaseBundle implements SchemaEquality<DatabaseBundle> {
     @SerializedName("version")
     private int mVersion;
     @SerializedName("identityHash")
@@ -104,4 +104,10 @@
         result.addAll(mSetupQueries);
         return result;
     }
+
+    @Override
+    public boolean isSchemaEqual(DatabaseBundle other) {
+        return SchemaEqualityUtil.checkSchemaEquality(getEntitiesByTableName(),
+                other.getEntitiesByTableName());
+    }
 }
diff --git a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/EntityBundle.java b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/EntityBundle.java
index 8980a3b..d78ac35 100644
--- a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/EntityBundle.java
+++ b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/EntityBundle.java
@@ -16,6 +16,8 @@
 
 package android.arch.persistence.room.migration.bundle;
 
+import static android.arch.persistence.room.migration.bundle.SchemaEqualityUtil.checkSchemaEquality;
+
 import android.support.annotation.RestrictTo;
 
 import com.google.gson.annotations.SerializedName;
@@ -35,7 +37,7 @@
  * @hide
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class EntityBundle {
+public class EntityBundle implements SchemaEquality<EntityBundle> {
 
     static final String NEW_TABLE_PREFIX = "_new_";
 
@@ -176,4 +178,15 @@
         }
         return result;
     }
+
+    @Override
+    public boolean isSchemaEqual(EntityBundle other) {
+        if (!mTableName.equals(other.mTableName)) {
+            return false;
+        }
+        return checkSchemaEquality(getFieldsByColumnName(), other.getFieldsByColumnName())
+                && checkSchemaEquality(mPrimaryKey, other.mPrimaryKey)
+                && checkSchemaEquality(mIndices, other.mIndices)
+                && checkSchemaEquality(mForeignKeys, other.mForeignKeys);
+    }
 }
diff --git a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/FieldBundle.java b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/FieldBundle.java
index eb73d81..5f74087 100644
--- a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/FieldBundle.java
+++ b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/FieldBundle.java
@@ -27,7 +27,7 @@
  * @hide
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class FieldBundle {
+public class FieldBundle implements SchemaEquality<FieldBundle> {
     @SerializedName("fieldPath")
     private String mFieldPath;
     @SerializedName("columnName")
@@ -59,4 +59,14 @@
     public boolean isNonNull() {
         return mNonNull;
     }
+
+    @Override
+    public boolean isSchemaEqual(FieldBundle other) {
+        if (mNonNull != other.mNonNull) return false;
+        if (mColumnName != null ? !mColumnName.equals(other.mColumnName)
+                : other.mColumnName != null) {
+            return false;
+        }
+        return mAffinity != null ? mAffinity.equals(other.mAffinity) : other.mAffinity == null;
+    }
 }
diff --git a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/ForeignKeyBundle.java b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/ForeignKeyBundle.java
index d72cf8c..367dd74 100644
--- a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/ForeignKeyBundle.java
+++ b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/ForeignKeyBundle.java
@@ -28,7 +28,7 @@
  * @hide
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class ForeignKeyBundle {
+public class ForeignKeyBundle implements SchemaEquality<ForeignKeyBundle> {
     @SerializedName("table")
     private String mTable;
     @SerializedName("onDelete")
@@ -43,10 +43,10 @@
     /**
      * Creates a foreign key bundle with the given parameters.
      *
-     * @param table The target table
-     * @param onDelete OnDelete action
-     * @param onUpdate OnUpdate action
-     * @param columns The list of columns in the current table
+     * @param table             The target table
+     * @param onDelete          OnDelete action
+     * @param onUpdate          OnUpdate action
+     * @param columns           The list of columns in the current table
      * @param referencedColumns The list of columns in the referenced table
      */
     public ForeignKeyBundle(String table, String onDelete, String onUpdate,
@@ -102,4 +102,18 @@
     public List<String> getReferencedColumns() {
         return mReferencedColumns;
     }
+
+    @Override
+    public boolean isSchemaEqual(ForeignKeyBundle other) {
+        if (mTable != null ? !mTable.equals(other.mTable) : other.mTable != null) return false;
+        if (mOnDelete != null ? !mOnDelete.equals(other.mOnDelete) : other.mOnDelete != null) {
+            return false;
+        }
+        if (mOnUpdate != null ? !mOnUpdate.equals(other.mOnUpdate) : other.mOnUpdate != null) {
+            return false;
+        }
+        // order matters
+        return mColumns.equals(other.mColumns) && mReferencedColumns.equals(
+                other.mReferencedColumns);
+    }
 }
diff --git a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/IndexBundle.java b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/IndexBundle.java
index ba40618..e991316 100644
--- a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/IndexBundle.java
+++ b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/IndexBundle.java
@@ -28,7 +28,9 @@
  * @hide
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class IndexBundle {
+public class IndexBundle implements SchemaEquality<IndexBundle> {
+    // should match Index.kt
+    public static final String DEFAULT_PREFIX = "index_";
     @SerializedName("name")
     private String mName;
     @SerializedName("unique")
@@ -65,4 +67,25 @@
     public String create(String tableName) {
         return BundleUtil.replaceTableName(mCreateSql, tableName);
     }
+
+    @Override
+    public boolean isSchemaEqual(IndexBundle other) {
+        if (mUnique != other.mUnique) return false;
+        if (mName.startsWith(DEFAULT_PREFIX)) {
+            if (!other.mName.startsWith(DEFAULT_PREFIX)) {
+                return false;
+            }
+        } else if (other.mName.startsWith(DEFAULT_PREFIX)) {
+            return false;
+        } else if (!mName.equals(other.mName)) {
+            return false;
+        }
+
+        // order matters
+        if (mColumnNames != null ? !mColumnNames.equals(other.mColumnNames)
+                : other.mColumnNames != null) {
+            return false;
+        }
+        return true;
+    }
 }
diff --git a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/PrimaryKeyBundle.java b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/PrimaryKeyBundle.java
index c16f967..820aa7e 100644
--- a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/PrimaryKeyBundle.java
+++ b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/PrimaryKeyBundle.java
@@ -28,7 +28,7 @@
  * @hide
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class PrimaryKeyBundle {
+public class PrimaryKeyBundle implements SchemaEquality<PrimaryKeyBundle> {
     @SerializedName("columnNames")
     private List<String> mColumnNames;
     @SerializedName("autoGenerate")
@@ -46,4 +46,9 @@
     public boolean isAutoGenerate() {
         return mAutoGenerate;
     }
+
+    @Override
+    public boolean isSchemaEqual(PrimaryKeyBundle other) {
+        return mColumnNames.equals(other.mColumnNames) && mAutoGenerate == other.mAutoGenerate;
+    }
 }
diff --git a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/SchemaBundle.java b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/SchemaBundle.java
index d6171aa..af35e6f 100644
--- a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/SchemaBundle.java
+++ b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/SchemaBundle.java
@@ -37,7 +37,7 @@
  * @hide
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class SchemaBundle {
+public class SchemaBundle implements SchemaEquality<SchemaBundle> {
 
     @SerializedName("formatVersion")
     private int mFormatVersion;
@@ -47,6 +47,7 @@
     private static final Gson GSON;
     private static final String CHARSET = "UTF-8";
     public static final int LATEST_FORMAT = 1;
+
     static {
         GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
     }
@@ -104,4 +105,9 @@
         }
     }
 
+    @Override
+    public boolean isSchemaEqual(SchemaBundle other) {
+        return SchemaEqualityUtil.checkSchemaEquality(mDatabase, other.mDatabase)
+                && mFormatVersion == other.mFormatVersion;
+    }
 }
diff --git a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/SchemaEquality.java b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/SchemaEquality.java
new file mode 100644
index 0000000..59ea4b0
--- /dev/null
+++ b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/SchemaEquality.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 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.migration.bundle;
+
+import android.support.annotation.RestrictTo;
+
+/**
+ * A loose equals check which checks schema equality instead of 100% equality (e.g. order of
+ * columns in an entity does not have to match)
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+interface SchemaEquality<T> {
+    boolean isSchemaEqual(T other);
+}
diff --git a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/SchemaEqualityUtil.java b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/SchemaEqualityUtil.java
new file mode 100644
index 0000000..65a7572
--- /dev/null
+++ b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/SchemaEqualityUtil.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 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.migration.bundle;
+
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * utility class to run schema equality on collections.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+class SchemaEqualityUtil {
+    static <T, K extends SchemaEquality<K>> boolean checkSchemaEquality(
+            @Nullable Map<T, K> map1, @Nullable Map<T, K> map2) {
+        if (map1 == null) {
+            return map2 == null;
+        }
+        if (map2 == null) {
+            return false;
+        }
+        if (map1.size() != map2.size()) {
+            return false;
+        }
+        for (Map.Entry<T, K> pair : map1.entrySet()) {
+            if (!checkSchemaEquality(pair.getValue(), map2.get(pair.getKey()))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    static <K extends SchemaEquality<K>> boolean checkSchemaEquality(
+            @Nullable List<K> list1, @Nullable List<K> list2) {
+        if (list1 == null) {
+            return list2 == null;
+        }
+        if (list2 == null) {
+            return false;
+        }
+        if (list1.size() != list2.size()) {
+            return false;
+        }
+        // we don't care this is n^2, small list + only used for testing.
+        for (K item1 : list1) {
+            // find matching item
+            boolean matched = false;
+            for (K item2 : list2) {
+                if (checkSchemaEquality(item1, item2)) {
+                    matched = true;
+                    break;
+                }
+            }
+            if (!matched) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @SuppressWarnings("SimplifiableIfStatement")
+    static <K extends SchemaEquality<K>> boolean checkSchemaEquality(
+            @Nullable K item1, @Nullable K item2) {
+        if (item1 == null) {
+            return item2 == null;
+        }
+        if (item2 == null) {
+            return false;
+        }
+        return item1.isSchemaEqual(item2);
+    }
+}
diff --git a/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/EntityBundleTest.java b/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/EntityBundleTest.java
new file mode 100644
index 0000000..4b4df8b
--- /dev/null
+++ b/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/EntityBundleTest.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 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.migration.bundle;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import static java.util.Arrays.asList;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Collections;
+
+@SuppressWarnings("ArraysAsListWithZeroOrOneArgument")
+@RunWith(JUnit4.class)
+public class EntityBundleTest {
+    @Test
+    public void schemaEquality_same_equal() {
+        EntityBundle bundle = new EntityBundle("foo", "sq",
+                asList(createFieldBundle("foo"), createFieldBundle("bar")),
+                new PrimaryKeyBundle(false, asList("foo")),
+                asList(createIndexBundle("foo")),
+                asList(createForeignKeyBundle("bar", "foo")));
+
+        EntityBundle other = new EntityBundle("foo", "sq",
+                asList(createFieldBundle("foo"), createFieldBundle("bar")),
+                new PrimaryKeyBundle(false, asList("foo")),
+                asList(createIndexBundle("foo")),
+                asList(createForeignKeyBundle("bar", "foo")));
+
+        assertThat(bundle.isSchemaEqual(other), is(true));
+    }
+
+    @Test
+    public void schemaEquality_reorderedFields_equal() {
+        EntityBundle bundle = new EntityBundle("foo", "sq",
+                asList(createFieldBundle("foo"), createFieldBundle("bar")),
+                new PrimaryKeyBundle(false, asList("foo")),
+                Collections.<IndexBundle>emptyList(),
+                Collections.<ForeignKeyBundle>emptyList());
+
+        EntityBundle other = new EntityBundle("foo", "sq",
+                asList(createFieldBundle("bar"), createFieldBundle("foo")),
+                new PrimaryKeyBundle(false, asList("foo")),
+                Collections.<IndexBundle>emptyList(),
+                Collections.<ForeignKeyBundle>emptyList());
+
+        assertThat(bundle.isSchemaEqual(other), is(true));
+    }
+
+    @Test
+    public void schemaEquality_diffFields_notEqual() {
+        EntityBundle bundle = new EntityBundle("foo", "sq",
+                asList(createFieldBundle("foo"), createFieldBundle("bar")),
+                new PrimaryKeyBundle(false, asList("foo")),
+                Collections.<IndexBundle>emptyList(),
+                Collections.<ForeignKeyBundle>emptyList());
+
+        EntityBundle other = new EntityBundle("foo", "sq",
+                asList(createFieldBundle("foo2"), createFieldBundle("bar")),
+                new PrimaryKeyBundle(false, asList("foo")),
+                Collections.<IndexBundle>emptyList(),
+                Collections.<ForeignKeyBundle>emptyList());
+
+        assertThat(bundle.isSchemaEqual(other), is(false));
+    }
+
+    @Test
+    public void schemaEquality_reorderedForeignKeys_equal() {
+        EntityBundle bundle = new EntityBundle("foo", "sq",
+                Collections.<FieldBundle>emptyList(),
+                new PrimaryKeyBundle(false, asList("foo")),
+                Collections.<IndexBundle>emptyList(),
+                asList(createForeignKeyBundle("x", "y"),
+                        createForeignKeyBundle("bar", "foo")));
+
+        EntityBundle other = new EntityBundle("foo", "sq",
+                Collections.<FieldBundle>emptyList(),
+                new PrimaryKeyBundle(false, asList("foo")),
+                Collections.<IndexBundle>emptyList(),
+                asList(createForeignKeyBundle("bar", "foo"),
+                        createForeignKeyBundle("x", "y")));
+
+
+        assertThat(bundle.isSchemaEqual(other), is(true));
+    }
+
+    @Test
+    public void schemaEquality_diffForeignKeys_notEqual() {
+        EntityBundle bundle = new EntityBundle("foo", "sq",
+                Collections.<FieldBundle>emptyList(),
+                new PrimaryKeyBundle(false, asList("foo")),
+                Collections.<IndexBundle>emptyList(),
+                asList(createForeignKeyBundle("bar", "foo")));
+
+        EntityBundle other = new EntityBundle("foo", "sq",
+                Collections.<FieldBundle>emptyList(),
+                new PrimaryKeyBundle(false, asList("foo")),
+                Collections.<IndexBundle>emptyList(),
+                asList(createForeignKeyBundle("bar2", "foo")));
+
+        assertThat(bundle.isSchemaEqual(other), is(false));
+    }
+
+    @Test
+    public void schemaEquality_reorderedIndices_equal() {
+        EntityBundle bundle = new EntityBundle("foo", "sq",
+                Collections.<FieldBundle>emptyList(),
+                new PrimaryKeyBundle(false, asList("foo")),
+                asList(createIndexBundle("foo"), createIndexBundle("baz")),
+                Collections.<ForeignKeyBundle>emptyList());
+
+        EntityBundle other = new EntityBundle("foo", "sq",
+                Collections.<FieldBundle>emptyList(),
+                new PrimaryKeyBundle(false, asList("foo")),
+                asList(createIndexBundle("baz"), createIndexBundle("foo")),
+                Collections.<ForeignKeyBundle>emptyList());
+
+        assertThat(bundle.isSchemaEqual(other), is(true));
+    }
+
+    @Test
+    public void schemaEquality_diffIndices_notEqual() {
+        EntityBundle bundle = new EntityBundle("foo", "sq",
+                Collections.<FieldBundle>emptyList(),
+                new PrimaryKeyBundle(false, asList("foo")),
+                asList(createIndexBundle("foo")),
+                Collections.<ForeignKeyBundle>emptyList());
+
+        EntityBundle other = new EntityBundle("foo", "sq",
+                Collections.<FieldBundle>emptyList(),
+                new PrimaryKeyBundle(false, asList("foo")),
+                asList(createIndexBundle("foo2")),
+                Collections.<ForeignKeyBundle>emptyList());
+
+        assertThat(bundle.isSchemaEqual(other), is(false));
+    }
+
+    private FieldBundle createFieldBundle(String name) {
+        return new FieldBundle("foo", name, "text", false);
+    }
+
+    private IndexBundle createIndexBundle(String colName) {
+        return new IndexBundle("ind_" + colName, false,
+                asList(colName), "create");
+    }
+
+    private ForeignKeyBundle createForeignKeyBundle(String targetTable, String column) {
+        return new ForeignKeyBundle(targetTable, "CASCADE", "CASCADE",
+                asList(column), asList(column));
+    }
+}
diff --git a/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/FieldBundleTest.java b/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/FieldBundleTest.java
new file mode 100644
index 0000000..eac4477
--- /dev/null
+++ b/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/FieldBundleTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 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.migration.bundle;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class FieldBundleTest {
+    @Test
+    public void schemaEquality_same_equal() {
+        FieldBundle bundle = new FieldBundle("foo", "foo", "text", false);
+        FieldBundle copy = new FieldBundle("foo", "foo", "text", false);
+        assertThat(bundle.isSchemaEqual(copy), is(true));
+    }
+
+    @Test
+    public void schemaEquality_diffNonNull_notEqual() {
+        FieldBundle bundle = new FieldBundle("foo", "foo", "text", false);
+        FieldBundle copy = new FieldBundle("foo", "foo", "text", true);
+        assertThat(bundle.isSchemaEqual(copy), is(false));
+    }
+
+    @Test
+    public void schemaEquality_diffColumnName_notEqual() {
+        FieldBundle bundle = new FieldBundle("foo", "foo", "text", false);
+        FieldBundle copy = new FieldBundle("foo", "foo2", "text", true);
+        assertThat(bundle.isSchemaEqual(copy), is(false));
+    }
+
+    @Test
+    public void schemaEquality_diffAffinity_notEqual() {
+        FieldBundle bundle = new FieldBundle("foo", "foo", "text", false);
+        FieldBundle copy = new FieldBundle("foo", "foo2", "int", false);
+        assertThat(bundle.isSchemaEqual(copy), is(false));
+    }
+
+    @Test
+    public void schemaEquality_diffPath_equal() {
+        FieldBundle bundle = new FieldBundle("foo", "foo", "text", false);
+        FieldBundle copy = new FieldBundle("foo>bar", "foo", "text", false);
+        assertThat(bundle.isSchemaEqual(copy), is(true));
+    }
+}
diff --git a/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/ForeignKeyBundleTest.java b/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/ForeignKeyBundleTest.java
new file mode 100644
index 0000000..be1b81e
--- /dev/null
+++ b/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/ForeignKeyBundleTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 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.migration.bundle;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+
+@RunWith(JUnit4.class)
+public class ForeignKeyBundleTest {
+    @Test
+    public void schemaEquality_same_equal() {
+        ForeignKeyBundle bundle = new ForeignKeyBundle("table", "onDelete",
+                "onUpdate", Arrays.asList("col1", "col2"),
+                Arrays.asList("target1", "target2"));
+        ForeignKeyBundle other = new ForeignKeyBundle("table", "onDelete",
+                "onUpdate", Arrays.asList("col1", "col2"),
+                Arrays.asList("target1", "target2"));
+        assertThat(bundle.isSchemaEqual(other), is(true));
+    }
+
+    @Test
+    public void schemaEquality_diffTable_notEqual() {
+        ForeignKeyBundle bundle = new ForeignKeyBundle("table", "onDelete",
+                "onUpdate", Arrays.asList("col1", "col2"),
+                Arrays.asList("target1", "target2"));
+        ForeignKeyBundle other = new ForeignKeyBundle("table2", "onDelete",
+                "onUpdate", Arrays.asList("col1", "col2"),
+                Arrays.asList("target1", "target2"));
+        assertThat(bundle.isSchemaEqual(other), is(false));
+    }
+
+    @Test
+    public void schemaEquality_diffOnDelete_notEqual() {
+        ForeignKeyBundle bundle = new ForeignKeyBundle("table", "onDelete2",
+                "onUpdate", Arrays.asList("col1", "col2"),
+                Arrays.asList("target1", "target2"));
+        ForeignKeyBundle other = new ForeignKeyBundle("table", "onDelete",
+                "onUpdate", Arrays.asList("col1", "col2"),
+                Arrays.asList("target1", "target2"));
+        assertThat(bundle.isSchemaEqual(other), is(false));
+    }
+
+    @Test
+    public void schemaEquality_diffOnUpdate_notEqual() {
+        ForeignKeyBundle bundle = new ForeignKeyBundle("table", "onDelete",
+                "onUpdate", Arrays.asList("col1", "col2"),
+                Arrays.asList("target1", "target2"));
+        ForeignKeyBundle other = new ForeignKeyBundle("table", "onDelete",
+                "onUpdate2", Arrays.asList("col1", "col2"),
+                Arrays.asList("target1", "target2"));
+        assertThat(bundle.isSchemaEqual(other), is(false));
+    }
+
+    @Test
+    public void schemaEquality_diffSrcOrder_notEqual() {
+        ForeignKeyBundle bundle = new ForeignKeyBundle("table", "onDelete",
+                "onUpdate", Arrays.asList("col2", "col1"),
+                Arrays.asList("target1", "target2"));
+        ForeignKeyBundle other = new ForeignKeyBundle("table", "onDelete",
+                "onUpdate", Arrays.asList("col1", "col2"),
+                Arrays.asList("target1", "target2"));
+        assertThat(bundle.isSchemaEqual(other), is(false));
+    }
+
+    @Test
+    public void schemaEquality_diffTargetOrder_notEqual() {
+        ForeignKeyBundle bundle = new ForeignKeyBundle("table", "onDelete",
+                "onUpdate", Arrays.asList("col1", "col2"),
+                Arrays.asList("target1", "target2"));
+        ForeignKeyBundle other = new ForeignKeyBundle("table", "onDelete",
+                "onUpdate", Arrays.asList("col1", "col2"),
+                Arrays.asList("target2", "target1"));
+        assertThat(bundle.isSchemaEqual(other), is(false));
+    }
+}
diff --git a/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/IndexBundleTest.java b/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/IndexBundleTest.java
new file mode 100644
index 0000000..aa7230f
--- /dev/null
+++ b/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/IndexBundleTest.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 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.migration.bundle;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+
+@RunWith(JUnit4.class)
+public class IndexBundleTest {
+    @Test
+    public void schemaEquality_same_equal() {
+        IndexBundle bundle = new IndexBundle("index1", false,
+                Arrays.asList("col1", "col2"), "sql");
+        IndexBundle other = new IndexBundle("index1", false,
+                Arrays.asList("col1", "col2"), "sql");
+        assertThat(bundle.isSchemaEqual(other), is(true));
+    }
+
+    @Test
+    public void schemaEquality_diffName_notEqual() {
+        IndexBundle bundle = new IndexBundle("index1", false,
+                Arrays.asList("col1", "col2"), "sql");
+        IndexBundle other = new IndexBundle("index3", false,
+                Arrays.asList("col1", "col2"), "sql");
+        assertThat(bundle.isSchemaEqual(other), is(false));
+    }
+
+    @Test
+    public void schemaEquality_diffGenericName_equal() {
+        IndexBundle bundle = new IndexBundle(IndexBundle.DEFAULT_PREFIX + "x", false,
+                Arrays.asList("col1", "col2"), "sql");
+        IndexBundle other = new IndexBundle(IndexBundle.DEFAULT_PREFIX + "y", false,
+                Arrays.asList("col1", "col2"), "sql");
+        assertThat(bundle.isSchemaEqual(other), is(true));
+    }
+
+    @Test
+    public void schemaEquality_diffUnique_notEqual() {
+        IndexBundle bundle = new IndexBundle("index1", false,
+                Arrays.asList("col1", "col2"), "sql");
+        IndexBundle other = new IndexBundle("index1", true,
+                Arrays.asList("col1", "col2"), "sql");
+        assertThat(bundle.isSchemaEqual(other), is(false));
+    }
+
+    @Test
+    public void schemaEquality_diffColumns_notEqual() {
+        IndexBundle bundle = new IndexBundle("index1", false,
+                Arrays.asList("col1", "col2"), "sql");
+        IndexBundle other = new IndexBundle("index1", false,
+                Arrays.asList("col2", "col1"), "sql");
+        assertThat(bundle.isSchemaEqual(other), is(false));
+    }
+
+    @Test
+    public void schemaEquality_diffSql_equal() {
+        IndexBundle bundle = new IndexBundle("index1", false,
+                Arrays.asList("col1", "col2"), "sql");
+        IndexBundle other = new IndexBundle("index1", false,
+                Arrays.asList("col1", "col2"), "sql22");
+        assertThat(bundle.isSchemaEqual(other), is(true));
+    }
+}
diff --git a/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/PrimaryKeyBundleTest.java b/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/PrimaryKeyBundleTest.java
new file mode 100644
index 0000000..3b9e464
--- /dev/null
+++ b/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/PrimaryKeyBundleTest.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 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.migration.bundle;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+
+@RunWith(JUnit4.class)
+public class PrimaryKeyBundleTest {
+    @Test
+    public void schemaEquality_same_equal() {
+        PrimaryKeyBundle bundle = new PrimaryKeyBundle(true,
+                Arrays.asList("foo", "bar"));
+        PrimaryKeyBundle other = new PrimaryKeyBundle(true,
+                Arrays.asList("foo", "bar"));
+        assertThat(bundle.isSchemaEqual(other), is(true));
+    }
+
+    @Test
+    public void schemaEquality_diffAutoGen_notEqual() {
+        PrimaryKeyBundle bundle = new PrimaryKeyBundle(true,
+                Arrays.asList("foo", "bar"));
+        PrimaryKeyBundle other = new PrimaryKeyBundle(false,
+                Arrays.asList("foo", "bar"));
+        assertThat(bundle.isSchemaEqual(other), is(false));
+    }
+
+    @Test
+    public void schemaEquality_diffColumns_notEqual() {
+        PrimaryKeyBundle bundle = new PrimaryKeyBundle(true,
+                Arrays.asList("foo", "baz"));
+        PrimaryKeyBundle other = new PrimaryKeyBundle(true,
+                Arrays.asList("foo", "bar"));
+        assertThat(bundle.isSchemaEqual(other), is(false));
+    }
+
+    @Test
+    public void schemaEquality_diffColumnOrder_notEqual() {
+        PrimaryKeyBundle bundle = new PrimaryKeyBundle(true,
+                Arrays.asList("foo", "bar"));
+        PrimaryKeyBundle other = new PrimaryKeyBundle(true,
+                Arrays.asList("bar", "foo"));
+        assertThat(bundle.isSchemaEqual(other), is(false));
+    }
+}
diff --git a/room/runtime/src/main/java/android/arch/persistence/room/RoomOpenHelper.java b/room/runtime/src/main/java/android/arch/persistence/room/RoomOpenHelper.java
index 47279d6..005164e 100644
--- a/room/runtime/src/main/java/android/arch/persistence/room/RoomOpenHelper.java
+++ b/room/runtime/src/main/java/android/arch/persistence/room/RoomOpenHelper.java
@@ -41,13 +41,20 @@
     private final Delegate mDelegate;
     @NonNull
     private final String mIdentityHash;
+    /**
+     * Room v1 had a bug where the hash was not consistent if fields are reordered.
+     * The new has fixes it but we still need to accept the legacy hash.
+     */
+    @NonNull // b/64290754
+    private final String mLegacyHash;
 
     public RoomOpenHelper(@NonNull DatabaseConfiguration configuration, @NonNull Delegate delegate,
-            @NonNull String identityHash) {
+            @NonNull String identityHash, @NonNull String legacyHash) {
         super(delegate.version);
         mConfiguration = configuration;
         mDelegate = delegate;
         mIdentityHash = identityHash;
+        mLegacyHash = legacyHash;
     }
 
     @Override
@@ -115,7 +122,7 @@
         } finally {
             cursor.close();
         }
-        if (!mIdentityHash.equals(identityHash)) {
+        if (!mIdentityHash.equals(identityHash) && !mLegacyHash.equals(identityHash)) {
             throw new IllegalStateException("Room cannot verify the data integrity. Looks like"
                     + " you've changed schema but forgot to update the version number. You can"
                     + " simply fix this by increasing the version number.");
diff --git a/room/testing/src/main/java/android/arch/persistence/room/testing/MigrationTestHelper.java b/room/testing/src/main/java/android/arch/persistence/room/testing/MigrationTestHelper.java
index 2e93bbe..55f8191 100644
--- a/room/testing/src/main/java/android/arch/persistence/room/testing/MigrationTestHelper.java
+++ b/room/testing/src/main/java/android/arch/persistence/room/testing/MigrationTestHelper.java
@@ -146,6 +146,9 @@
                 true);
         RoomOpenHelper roomOpenHelper = new RoomOpenHelper(configuration,
                 new CreatingDelegate(schemaBundle.getDatabase()),
+                schemaBundle.getDatabase().getIdentityHash(),
+                // we pass the same hash twice since an old schema does not necessarily have
+                // a legacy hash and we would not even persist it.
                 schemaBundle.getDatabase().getIdentityHash());
         return openDatabase(name, roomOpenHelper);
     }
@@ -189,6 +192,9 @@
                 true);
         RoomOpenHelper roomOpenHelper = new RoomOpenHelper(configuration,
                 new MigratingDelegate(schemaBundle.getDatabase(), validateDroppedTables),
+                // we pass the same hash twice since an old schema does not necessarily have
+                // a legacy hash and we would not even persist it.
+                schemaBundle.getDatabase().getIdentityHash(),
                 schemaBundle.getDatabase().getIdentityHash());
         return openDatabase(name, roomOpenHelper);
     }