Validate migration

This CL adds necessary code to validate database integrity after a
migration. This way, if developer does not implement migration
properly, Room will throw an exception.

Test: TableInfoTest, MigrationTest
Bug: 36602348
Change-Id: Id335875387a740344deb76778c5df6978b0212a5
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/ext/javapoet_ext.kt b/room/compiler/src/main/kotlin/com/android/support/room/ext/javapoet_ext.kt
index 31ac485..52061e3 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/ext/javapoet_ext.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/ext/javapoet_ext.kt
@@ -69,6 +69,10 @@
             ClassName.get("com.android.support.room", "RoomOpenHelper")
     val OPEN_HELPER_DELEGATE: ClassName =
             ClassName.get("com.android.support.room", "RoomOpenHelper.Delegate")
+    val TABLE_INFO : ClassName =
+            ClassName.get("com.android.support.room.util", "TableInfo")
+    val TABLE_INFO_COLUMN : ClassName =
+            ClassName.get("com.android.support.room.util", "TableInfo.Column")
 }
 
 object LifecyclesTypeNames {
@@ -85,4 +89,5 @@
 object CommonTypeNames {
     val LIST = ClassName.get("java.util", "List")
     val SET = ClassName.get("java.util", "Set")
+    val STRING = ClassName.get("java.lang", "String")
 }
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/writer/SQLiteOpenHelperWriter.kt b/room/compiler/src/main/kotlin/com/android/support/room/writer/SQLiteOpenHelperWriter.kt
index a42ba76..eaf75ef 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/writer/SQLiteOpenHelperWriter.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/writer/SQLiteOpenHelperWriter.kt
@@ -28,7 +28,6 @@
 import com.android.support.room.vo.Entity
 import com.squareup.javapoet.MethodSpec
 import com.squareup.javapoet.ParameterSpec
-import com.squareup.javapoet.TypeName
 import com.squareup.javapoet.TypeSpec
 import javax.lang.model.element.Modifier.PROTECTED
 import javax.lang.model.element.Modifier.PUBLIC
@@ -43,8 +42,8 @@
             val callbackVar = scope.getTmpVar("_openCallback")
             addStatement("final $T $L = new $T($N, $L, $S)",
                     SupportDbTypeNames.SQLITE_OPEN_HELPER_CALLBACK,
-                    callbackVar, RoomTypeNames.OPEN_HELPER, configuration, createOpenCallback(),
-                    database.identityHash)
+                    callbackVar, RoomTypeNames.OPEN_HELPER, configuration,
+                    createOpenCallback(scope), database.identityHash)
             // build configuration
             addStatement(
                     """
@@ -63,22 +62,26 @@
         }
     }
 
-    private fun createOpenCallback() : TypeSpec {
+    private fun createOpenCallback(scope: CodeGenScope) : TypeSpec {
         return TypeSpec.anonymousClassBuilder("").apply {
             superclass(RoomTypeNames.OPEN_HELPER_DELEGATE)
             addMethod(createCreateAllTables())
             addMethod(createDropAllTables())
             addMethod(createOnOpen())
-            addMethod(createValidateMigration())
+            addMethod(createValidateMigration(scope.fork()))
         }.build()
     }
 
-    private fun createValidateMigration(): MethodSpec {
+    private fun createValidateMigration(scope: CodeGenScope): MethodSpec {
         return MethodSpec.methodBuilder("validateMigration").apply {
             addModifiers(PROTECTED)
-            returns(TypeName.BOOLEAN)
-            addParameter(SupportDbTypeNames.DB, "_db")
-            addStatement("return true")
+            val dbParam = ParameterSpec.builder(SupportDbTypeNames.DB, "_db").build()
+            addParameter(dbParam)
+            database.entities.forEach { entity ->
+                val methodScope = scope.fork()
+                TableInfoValidationWriter(entity).write(dbParam, methodScope)
+                addCode(methodScope.builder().build())
+            }
         }.build()
     }
 
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/writer/TableInfoValidationWriter.kt b/room/compiler/src/main/kotlin/com/android/support/room/writer/TableInfoValidationWriter.kt
new file mode 100644
index 0000000..b5e9cfb
--- /dev/null
+++ b/room/compiler/src/main/kotlin/com/android/support/room/writer/TableInfoValidationWriter.kt
@@ -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 com.android.support.room.writer
+
+import com.android.support.room.ext.CommonTypeNames
+import com.android.support.room.ext.L
+import com.android.support.room.ext.N
+import com.android.support.room.ext.RoomTypeNames
+import com.android.support.room.ext.S
+import com.android.support.room.ext.T
+import com.android.support.room.ext.typeName
+import com.android.support.room.parser.SQLTypeAffinity
+import com.android.support.room.solver.CodeGenScope
+import com.android.support.room.vo.Entity
+import com.squareup.javapoet.ParameterSpec
+import com.squareup.javapoet.ParameterizedTypeName
+import stripNonJava
+import java.util.HashMap
+
+class TableInfoValidationWriter(val entity : Entity) {
+    fun write(dbParam : ParameterSpec, scope : CodeGenScope) {
+        val suffix = entity.tableName.stripNonJava().capitalize()
+        val expectedInfoVar = scope.getTmpVar("_info$suffix")
+        scope.builder().apply {
+            val columnListVar = scope.getTmpVar("_columns$suffix")
+            val columnListType = ParameterizedTypeName.get(HashMap::class.typeName(),
+                    CommonTypeNames.STRING, RoomTypeNames.TABLE_INFO_COLUMN)
+            addStatement("final $T $L = new $T($L)", columnListType, columnListVar,
+                    columnListType, entity.fields.size)
+            entity.fields.forEachIndexed { index, field ->
+                addStatement("$L.put($S, new $T($S, $S, $L))",
+                        columnListVar, field.columnName, RoomTypeNames.TABLE_INFO_COLUMN,
+                        /*name*/ field.columnName,
+                        /*type*/ field.affinity?.name ?: SQLTypeAffinity.TEXT.name,
+                        /*pkeyPos*/ entity.primaryKey.fields.indexOf(field) + 1)
+            }
+            addStatement("final $T $L = new $T($S, $L)",
+                    RoomTypeNames.TABLE_INFO, expectedInfoVar, RoomTypeNames.TABLE_INFO,
+                    entity.tableName, columnListVar)
+
+            val existingVar = scope.getTmpVar("_existing$suffix")
+            addStatement("final $T $L = $T.read($N, $S)",
+                    RoomTypeNames.TABLE_INFO, existingVar, RoomTypeNames.TABLE_INFO,
+                    dbParam, entity.tableName)
+
+            beginControlFlow("if (! $L.equals($L))", expectedInfoVar, existingVar).apply {
+                addStatement("throw new $T($S + $L + $S + $L)",
+                        IllegalStateException::class.typeName(),
+                        "Migration didn't properly handle ${entity.tableName}" +
+                                "(${entity.element.qualifiedName}).\n Expected:\n",
+                        expectedInfoVar, "\n Found:\n", existingVar)
+            }
+            endControlFlow()
+        }
+    }
+}
diff --git a/room/compiler/src/test/data/databasewriter/output/ComplexDatabase.java b/room/compiler/src/test/data/databasewriter/output/ComplexDatabase.java
index eb2cc8f..04ab0b6 100644
--- a/room/compiler/src/test/data/databasewriter/output/ComplexDatabase.java
+++ b/room/compiler/src/test/data/databasewriter/output/ComplexDatabase.java
@@ -1,4 +1,3 @@
-
 package foo.bar;
 
 import com.android.support.db.SupportSQLiteDatabase;
@@ -9,7 +8,12 @@
 import com.android.support.room.InvalidationTracker;
 import com.android.support.room.RoomOpenHelper;
 import com.android.support.room.RoomOpenHelper.Delegate;
+import com.android.support.room.util.TableInfo;
+import com.android.support.room.util.TableInfo.Column;
+import java.lang.IllegalStateException;
 import java.lang.Override;
+import java.lang.String;
+import java.util.HashMap;
 
 public class ComplexDatabase_Impl extends ComplexDatabase {
     private volatile ComplexDao _complexDao;
@@ -31,8 +35,19 @@
                 internalInitInvalidationTracker(_db);
             }
 
-            protected boolean validateMigration(SupportSQLiteDatabase _db) {
-                return true;
+            protected void validateMigration(SupportSQLiteDatabase _db) {
+                final HashMap<String, TableInfo.Column> _columnsUser = new HashMap<String, TableInfo.Column>(4);
+                _columnsUser.put("uid", new TableInfo.Column("uid", "INTEGER", 1));
+                _columnsUser.put("name", new TableInfo.Column("name", "TEXT", 0));
+                _columnsUser.put("lastName", new TableInfo.Column("lastName", "TEXT", 0));
+                _columnsUser.put("ageColumn", new TableInfo.Column("ageColumn", "INTEGER", 0));
+                final TableInfo _infoUser = new TableInfo("User", _columnsUser);
+                final TableInfo _existingUser = TableInfo.read(_db, "User");
+                if (! _infoUser.equals(_existingUser)) {
+                    throw new IllegalStateException("Migration didn't properly handle User(foo.bar.User).\n"
+                            + " Expected:\n" + _infoUser + "\n"
+                            + " Found:\n" + _existingUser);
+                }
             }
         }, "d4b1d59e1344d0db40fe2cd3fe64d02f");
         final SupportSQLiteOpenHelper.Configuration _sqliteConfig = SupportSQLiteOpenHelper.Configuration.builder(configuration.context)
diff --git a/room/integration-tests/testapp/schemas/com.android.support.room.integration.testapp.migration.MigrationDb/1.json b/room/integration-tests/testapp/schemas/com.android.support.room.integration.testapp.migration.MigrationDb/1.json
index ca4d63a..fba47c4 100644
--- a/room/integration-tests/testapp/schemas/com.android.support.room.integration.testapp.migration.MigrationDb/1.json
+++ b/room/integration-tests/testapp/schemas/com.android.support.room.integration.testapp.migration.MigrationDb/1.json
@@ -2,10 +2,10 @@
   "formatVersion": 1,
   "database": {
     "version": 1,
-    "identityHash": "e496317e42c1a8a681df3f4f8012562e",
+    "identityHash": "2f3557e56d7f665363f3e20d14787a59",
     "entities": [
       {
-        "tableName": "Vo1",
+        "tableName": "Entity1",
         "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER, `name` TEXT, PRIMARY KEY(`id`))",
         "fields": [
           {
@@ -30,7 +30,7 @@
     ],
     "setupQueries": [
       "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
-      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"e496317e42c1a8a681df3f4f8012562e\")"
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"2f3557e56d7f665363f3e20d14787a59\")"
     ]
   }
 }
\ No newline at end of file
diff --git a/room/integration-tests/testapp/schemas/com.android.support.room.integration.testapp.migration.MigrationDb/2.json b/room/integration-tests/testapp/schemas/com.android.support.room.integration.testapp.migration.MigrationDb/2.json
index 70e4b5c..db6af46 100644
--- a/room/integration-tests/testapp/schemas/com.android.support.room.integration.testapp.migration.MigrationDb/2.json
+++ b/room/integration-tests/testapp/schemas/com.android.support.room.integration.testapp.migration.MigrationDb/2.json
@@ -2,10 +2,10 @@
   "formatVersion": 1,
   "database": {
     "version": 2,
-    "identityHash": "18b84433116f1cfd478e0a9fe25c9121",
+    "identityHash": "aee9a6eed720c059df0f2ee0d6e96d89",
     "entities": [
       {
-        "tableName": "Vo1",
+        "tableName": "Entity1",
         "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER, `name` TEXT, PRIMARY KEY(`id`))",
         "fields": [
           {
@@ -28,7 +28,7 @@
         "indices": []
       },
       {
-        "tableName": "Vo2",
+        "tableName": "Entity2",
         "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER, `name` TEXT, PRIMARY KEY(`id`))",
         "fields": [
           {
@@ -53,7 +53,7 @@
     ],
     "setupQueries": [
       "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
-      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"18b84433116f1cfd478e0a9fe25c9121\")"
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"aee9a6eed720c059df0f2ee0d6e96d89\")"
     ]
   }
 }
\ No newline at end of file
diff --git a/room/integration-tests/testapp/schemas/com.android.support.room.integration.testapp.migration.MigrationDb/3.json b/room/integration-tests/testapp/schemas/com.android.support.room.integration.testapp.migration.MigrationDb/3.json
new file mode 100644
index 0000000..4d9dcb3
--- /dev/null
+++ b/room/integration-tests/testapp/schemas/com.android.support.room.integration.testapp.migration.MigrationDb/3.json
@@ -0,0 +1,64 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 3,
+    "identityHash": "3f2a99b6d768af0184e077808f7348fe",
+    "entities": [
+      {
+        "tableName": "Entity1",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER, `name` TEXT, PRIMARY KEY(`id`))",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER"
+          },
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT"
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": false
+        },
+        "indices": []
+      },
+      {
+        "tableName": "Entity2",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER, `addedInV3` TEXT, `name` TEXT, PRIMARY KEY(`id`))",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER"
+          },
+          {
+            "fieldPath": "addedInV3",
+            "columnName": "addedInV3",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT"
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": false
+        },
+        "indices": []
+      }
+    ],
+    "setupQueries": [
+      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"3f2a99b6d768af0184e077808f7348fe\")"
+    ]
+  }
+}
\ No newline at end of file
diff --git a/room/integration-tests/testapp/schemas/com.android.support.room.integration.testapp.migration.MigrationDb/4.json b/room/integration-tests/testapp/schemas/com.android.support.room.integration.testapp.migration.MigrationDb/4.json
new file mode 100644
index 0000000..7bc6842
--- /dev/null
+++ b/room/integration-tests/testapp/schemas/com.android.support.room.integration.testapp.migration.MigrationDb/4.json
@@ -0,0 +1,92 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 4,
+    "identityHash": "abbae5f17d94ff7c2c7e05ca217ccc31",
+    "entities": [
+      {
+        "tableName": "Entity1",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER, `name` TEXT, PRIMARY KEY(`id`))",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER"
+          },
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT"
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": false
+        },
+        "indices": []
+      },
+      {
+        "tableName": "Entity2",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER, `addedInV3` TEXT, `name` TEXT, PRIMARY KEY(`id`))",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER"
+          },
+          {
+            "fieldPath": "addedInV3",
+            "columnName": "addedInV3",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT"
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": false
+        },
+        "indices": []
+      },
+      {
+        "tableName": "Entity3",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER, `removedInV5` TEXT, `name` TEXT, PRIMARY KEY(`id`))",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER"
+          },
+          {
+            "fieldPath": "removedInV5",
+            "columnName": "removedInV5",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT"
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": false
+        },
+        "indices": []
+      }
+    ],
+    "setupQueries": [
+      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"abbae5f17d94ff7c2c7e05ca217ccc31\")"
+    ]
+  }
+}
\ No newline at end of file
diff --git a/room/integration-tests/testapp/schemas/com.android.support.room.integration.testapp.migration.MigrationDb/5.json b/room/integration-tests/testapp/schemas/com.android.support.room.integration.testapp.migration.MigrationDb/5.json
new file mode 100644
index 0000000..c7d2dd1
--- /dev/null
+++ b/room/integration-tests/testapp/schemas/com.android.support.room.integration.testapp.migration.MigrationDb/5.json
@@ -0,0 +1,87 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 5,
+    "identityHash": "5543c44fe679f4cf8f03093d66838068",
+    "entities": [
+      {
+        "tableName": "Entity1",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER, `name` TEXT, PRIMARY KEY(`id`))",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER"
+          },
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT"
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": false
+        },
+        "indices": []
+      },
+      {
+        "tableName": "Entity2",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER, `addedInV3` TEXT, `name` TEXT, PRIMARY KEY(`id`))",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER"
+          },
+          {
+            "fieldPath": "addedInV3",
+            "columnName": "addedInV3",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT"
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": false
+        },
+        "indices": []
+      },
+      {
+        "tableName": "Entity3",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER, `name` TEXT, PRIMARY KEY(`id`))",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER"
+          },
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT"
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": false
+        },
+        "indices": []
+      }
+    ],
+    "setupQueries": [
+      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"5543c44fe679f4cf8f03093d66838068\")"
+    ]
+  }
+}
\ No newline at end of file
diff --git a/room/integration-tests/testapp/schemas/com.android.support.room.integration.testapp.migration.MigrationDb/6.json b/room/integration-tests/testapp/schemas/com.android.support.room.integration.testapp.migration.MigrationDb/6.json
new file mode 100644
index 0000000..a31ad21
--- /dev/null
+++ b/room/integration-tests/testapp/schemas/com.android.support.room.integration.testapp.migration.MigrationDb/6.json
@@ -0,0 +1,64 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 6,
+    "identityHash": "3f2a99b6d768af0184e077808f7348fe",
+    "entities": [
+      {
+        "tableName": "Entity1",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER, `name` TEXT, PRIMARY KEY(`id`))",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER"
+          },
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT"
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": false
+        },
+        "indices": []
+      },
+      {
+        "tableName": "Entity2",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER, `addedInV3` TEXT, `name` TEXT, PRIMARY KEY(`id`))",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER"
+          },
+          {
+            "fieldPath": "addedInV3",
+            "columnName": "addedInV3",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT"
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": false
+        },
+        "indices": []
+      }
+    ],
+    "setupQueries": [
+      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"3f2a99b6d768af0184e077808f7348fe\")"
+    ]
+  }
+}
\ No newline at end of file
diff --git a/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/migration/MigrationDb.java b/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/migration/MigrationDb.java
index 64a37aa..43ee591 100644
--- a/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/migration/MigrationDb.java
+++ b/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/migration/MigrationDb.java
@@ -22,6 +22,7 @@
 import com.android.support.room.Dao;
 import com.android.support.room.Database;
 import com.android.support.room.Entity;
+import com.android.support.room.Ignore;
 import com.android.support.room.Insert;
 import com.android.support.room.PrimaryKey;
 import com.android.support.room.Query;
@@ -31,32 +32,50 @@
 
 @SuppressWarnings("WeakerAccess")
 @Database(version = MigrationDb.LATEST_VERSION,
-        entities = {MigrationDb.Vo1.class, MigrationDb.Vo2.class})
+        entities = {MigrationDb.Entity1.class, MigrationDb.Entity2.class})
 public abstract class MigrationDb extends RoomDatabase {
-    static final int LATEST_VERSION = 2;
+    static final int LATEST_VERSION = 6;
     abstract MigrationDao dao();
     @Entity
-    static class Vo1 {
+    static class Entity1 {
+        public static final String TABLE_NAME = "Entity1";
         @PrimaryKey
         public int id;
         public String name;
     }
 
     @Entity
-    static class Vo2 {
+    static class Entity2 {
+        public static final String TABLE_NAME = "Entity2";
         @PrimaryKey
         public int id;
+        public String addedInV3;
+        public String name;
+    }
+
+    @Entity
+    static class Entity3 { // added in version 4, removed at 6
+        public static final String TABLE_NAME = "Entity3";
+        @PrimaryKey
+        public int id;
+        @Ignore //removed at 5
+        public String removedInV5;
         public String name;
     }
 
     @Dao
     interface MigrationDao {
-        @Query("SELECT * from Vo1 ORDER BY id ASC")
-        List<Vo1> loadAllVo1s();
-        @Query("SELECT * from Vo2 ORDER BY id ASC")
-        List<Vo1> loadAllVo2s();
+        @Query("SELECT * from Entity1 ORDER BY id ASC")
+        List<Entity1> loadAllEntity1s();
+        @Query("SELECT * from Entity2 ORDER BY id ASC")
+        List<Entity2> loadAllEntity2s();
+        @Query("SELECT * from Entity2 ORDER BY id ASC")
+        List<Entity2Pojo> loadAllEntity2sAsPojo();
         @Insert
-        void insert(Vo2... vo2);
+        void insert(Entity2... entity2);
+    }
+
+    static class Entity2Pojo extends Entity2 {
     }
 
     /**
@@ -69,11 +88,32 @@
             mDb = db;
         }
 
-        public void insertIntoVo1(int id, String name) {
+        public void insertIntoEntity1(int id, String name) {
             ContentValues values = new ContentValues();
             values.put("id", id);
             values.put("name", name);
-            long insertionId = mDb.insert("Vo1", null, values);
+            long insertionId = mDb.insert(Entity1.TABLE_NAME, null, values);
+            if (insertionId == -1) {
+                throw new RuntimeException("test sanity failure");
+            }
+        }
+    }
+
+    /**
+     * not a real dao because database will change.
+     */
+    static class Dao_V2 {
+        final SupportSQLiteDatabase mDb;
+
+        Dao_V2(SupportSQLiteDatabase db) {
+            mDb = db;
+        }
+
+        public void insertIntoEntity2(int id, String name) {
+            ContentValues values = new ContentValues();
+            values.put("id", id);
+            values.put("name", name);
+            long insertionId = mDb.insert(Entity2.TABLE_NAME, null, values);
             if (insertionId == -1) {
                 throw new RuntimeException("test sanity failure");
             }
diff --git a/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/migration/MigrationTest.java b/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/migration/MigrationTest.java
index a5bf4b9..32a365a 100644
--- a/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/migration/MigrationTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/migration/MigrationTest.java
@@ -16,7 +16,9 @@
 
 package com.android.support.room.integration.testapp.migration;
 
+import static org.hamcrest.CoreMatchers.instanceOf;
 import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.nullValue;
 import static org.hamcrest.MatcherAssert.assertThat;
 
 import android.support.test.InstrumentationRegistry;
@@ -28,6 +30,7 @@
 import com.android.support.room.Room;
 import com.android.support.room.migration.Migration;
 import com.android.support.room.testing.MigrationTestHelper;
+import com.android.support.room.util.TableInfo;
 
 import org.junit.Rule;
 import org.junit.Test;
@@ -42,6 +45,7 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class MigrationTest {
+    private static final String TEST_DB = "migration-test";
     @Rule
     public MigrationTestHelper helper;
 
@@ -52,43 +56,192 @@
 
     @Test
     public void startInCurrentVersion() throws IOException {
-        SupportSQLiteDatabase db = helper.createDatabase("migration-test",
+        SupportSQLiteDatabase db = helper.createDatabase(TEST_DB,
                 MigrationDb.LATEST_VERSION);
         final MigrationDb.Dao_V1 dao = new MigrationDb.Dao_V1(db);
-        dao.insertIntoVo1(2, "x");
+        dao.insertIntoEntity1(2, "x");
         db.close();
-        MigrationDb migrationDb = Room.databaseBuilder(InstrumentationRegistry.getContext(),
-                MigrationDb.class, "migration-test").build();
-        List<MigrationDb.Vo1> items = migrationDb.dao().loadAllVo1s();
+        MigrationDb migrationDb = getLatestDb();
+        List<MigrationDb.Entity1> items = migrationDb.dao().loadAllEntity1s();
+        helper.closeWhenFinished(migrationDb.getDatabase());
         assertThat(items.size(), is(1));
     }
 
     @Test
     public void addTable() throws IOException {
-        SupportSQLiteDatabase db = helper.createDatabase("migration-test", 1);
+        SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1);
         final MigrationDb.Dao_V1 dao = new MigrationDb.Dao_V1(db);
-        dao.insertIntoVo1(2, "foo");
-        dao.insertIntoVo1(3, "bar");
+        dao.insertIntoEntity1(2, "foo");
+        dao.insertIntoEntity1(3, "bar");
         db.close();
+        db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2);
+        new MigrationDb.Dao_V2(db).insertIntoEntity2(3, "blah");
+        db.close();
+        MigrationDb migrationDb = getLatestDb();
+        List<MigrationDb.Entity1> entity1s = migrationDb.dao().loadAllEntity1s();
 
-        MigrationDb migrationDb = Room.databaseBuilder(InstrumentationRegistry.getContext(),
-                MigrationDb.class,
-                "migration-test")
-                .addMigrations(new Migration(1, 2) {
-                    @Override
-                    public void migrate(SupportSQLiteDatabase database) {
-                        database.execSQL("CREATE TABLE IF NOT EXISTS `Vo2` (`id` INTEGER,"
-                                + " `name` TEXT, PRIMARY KEY(`id`))");
-                    }
-                }).build();
-        List<MigrationDb.Vo1> vo1s = migrationDb.dao().loadAllVo1s();
-        assertThat(vo1s.size(), is(2));
-        MigrationDb.Vo2 vo2 = new MigrationDb.Vo2();
-        vo2.id = 2;
-        vo2.name = "bar";
+        assertThat(entity1s.size(), is(2));
+        MigrationDb.Entity2 entity2 = new MigrationDb.Entity2();
+        entity2.id = 2;
+        entity2.name = "bar";
         // assert no error happens
-        migrationDb.dao().insert(vo2);
-        List<MigrationDb.Vo1> vo2s = migrationDb.dao().loadAllVo2s();
-        assertThat(vo2s.size(), is(1));
+        migrationDb.dao().insert(entity2);
+        List<MigrationDb.Entity2> entity2s = migrationDb.dao().loadAllEntity2s();
+        assertThat(entity2s.size(), is(2));
+    }
+
+    private MigrationDb getLatestDb() {
+        MigrationDb db = Room.databaseBuilder(InstrumentationRegistry.getContext(),
+                MigrationDb.class, TEST_DB).addMigrations(ALL_MIGRATIONS).build();
+        // trigger open
+        db.beginTransaction();
+        db.endTransaction();
+        helper.closeWhenFinished(db.getDatabase());
+        return db;
+    }
+
+    @Test
+    public void addTableFailure() throws IOException {
+        testFailure(1, 2);
+    }
+
+    @Test
+    public void addColumnFailure() throws IOException {
+        SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 2);
+        db.close();
+        IllegalStateException caught = null;
+        try {
+            helper.runMigrationsAndValidate(TEST_DB, 3, true, new EmptyMigration(2, 3));
+        } catch (IllegalStateException ex) {
+            caught = ex;
+        }
+        assertThat(caught, instanceOf(IllegalStateException.class));
+    }
+
+    @Test
+    public void addColumn() throws IOException {
+        SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 2);
+        MigrationDb.Dao_V2 v2Dao = new MigrationDb.Dao_V2(db);
+        v2Dao.insertIntoEntity2(7, "blah");
+        db.close();
+        helper.runMigrationsAndValidate(TEST_DB, 3, true, MIGRATION_2_3);
+        // trigger open.
+        MigrationDb migrationDb = getLatestDb();
+        List<MigrationDb.Entity2> entity2s = migrationDb.dao().loadAllEntity2s();
+        assertThat(entity2s.size(), is(1));
+        assertThat(entity2s.get(0).name, is("blah"));
+        assertThat(entity2s.get(0).addedInV3, is(nullValue()));
+
+        List<MigrationDb.Entity2Pojo> entity2Pojos = migrationDb.dao().loadAllEntity2sAsPojo();
+        assertThat(entity2Pojos.size(), is(1));
+        assertThat(entity2Pojos.get(0).name, is("blah"));
+        assertThat(entity2Pojos.get(0).addedInV3, is(nullValue()));
+    }
+
+    @Test
+    public void failedToRemoveColumn() throws IOException {
+        testFailure(4, 5);
+    }
+
+    @Test
+    public void removeColumn() throws IOException {
+        helper.createDatabase(TEST_DB, 4);
+        final SupportSQLiteDatabase db = helper.runMigrationsAndValidate(TEST_DB,
+                5, true, MIGRATION_4_5);
+        final TableInfo info = TableInfo.read(db, MigrationDb.Entity3.TABLE_NAME);
+        assertThat(info.columns.size(), is(2));
+    }
+
+    @Test
+    public void dropTable() throws IOException {
+        helper.createDatabase(TEST_DB, 5);
+        final SupportSQLiteDatabase db = helper.runMigrationsAndValidate(TEST_DB,
+                6, true, MIGRATION_5_6);
+        final TableInfo info = TableInfo.read(db, MigrationDb.Entity3.TABLE_NAME);
+        assertThat(info.columns.size(), is(0));
+    }
+
+    @Test
+    public void failedToDropTable() throws IOException {
+        testFailure(5, 6);
+    }
+
+    @Test
+    public void failedToDropTableDontVerify() throws IOException {
+        helper.createDatabase(TEST_DB, 5);
+        final SupportSQLiteDatabase db = helper.runMigrationsAndValidate(TEST_DB,
+                6, false, new EmptyMigration(5, 6));
+        final TableInfo info = TableInfo.read(db, MigrationDb.Entity3.TABLE_NAME);
+        assertThat(info.columns.size(), is(2));
+    }
+
+    private void testFailure(int startVersion, int endVersion) throws IOException {
+        final SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, startVersion);
+        db.close();
+        Throwable throwable = null;
+        try {
+            helper.runMigrationsAndValidate(TEST_DB, endVersion, true,
+                    new EmptyMigration(startVersion, endVersion));
+        } catch (Throwable t) {
+            throwable = t;
+        }
+        assertThat(throwable, instanceOf(IllegalStateException.class));
+    }
+
+    static final Migration MIGRATION_1_2 = new Migration(1, 2) {
+        @Override
+        public void migrate(SupportSQLiteDatabase database) {
+            database.execSQL("CREATE TABLE IF NOT EXISTS `Entity2` (`id` INTEGER,"
+                    + " `name` TEXT, PRIMARY KEY(`id`))");
+        }
+    };
+
+    static final Migration MIGRATION_2_3 = new Migration(2, 3) {
+        @Override
+        public void migrate(SupportSQLiteDatabase database) {
+            database.execSQL("ALTER TABLE " + MigrationDb.Entity2.TABLE_NAME
+                    + " ADD COLUMN addedInV3 TEXT");
+        }
+    };
+
+    static final Migration MIGRATION_3_4 = new Migration(3, 4) {
+        @Override
+        public void migrate(SupportSQLiteDatabase database) {
+            database.execSQL("CREATE TABLE IF NOT EXISTS `Entity3` (`id` INTEGER,"
+                    + " `removedInV5` TEXT, `name` TEXT, PRIMARY KEY(`id`))");
+        }
+    };
+
+    static final Migration MIGRATION_4_5 = new Migration(4, 5) {
+        @Override
+        public void migrate(SupportSQLiteDatabase database) {
+            database.execSQL("CREATE TABLE IF NOT EXISTS `Entity3_New` (`id` INTEGER,"
+                    + " `name` TEXT, PRIMARY KEY(`id`))");
+            database.execSQL("INSERT INTO Entity3_New(`id`, `name`) "
+                    + "SELECT `id`, `name` FROM Entity3");
+            database.execSQL("DROP TABLE Entity3");
+            database.execSQL("ALTER TABLE Entity3_New RENAME TO Entity3");
+        }
+    };
+
+    static final Migration MIGRATION_5_6 = new Migration(5, 6) {
+        @Override
+        public void migrate(SupportSQLiteDatabase database) {
+            database.execSQL("DROP TABLE " + MigrationDb.Entity3.TABLE_NAME);
+        }
+    };
+
+    private static final Migration[] ALL_MIGRATIONS = new Migration[]{MIGRATION_1_2,
+            MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6};
+
+    static final class EmptyMigration extends Migration {
+        EmptyMigration(int startVersion, int endVersion) {
+            super(startVersion, endVersion);
+        }
+
+        @Override
+        public void migrate(SupportSQLiteDatabase database) {
+            // do nothing
+        }
     }
 }
diff --git a/room/runtime/src/androidTest/java/com/android/support/room/migration/TableInfoTest.java b/room/runtime/src/androidTest/java/com/android/support/room/migration/TableInfoTest.java
new file mode 100644
index 0000000..35e2a21
--- /dev/null
+++ b/room/runtime/src/androidTest/java/com/android/support/room/migration/TableInfoTest.java
@@ -0,0 +1,141 @@
+/*
+ * 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 com.android.support.room.migration;
+
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.support.db.SupportSQLiteDatabase;
+import com.android.support.db.SupportSQLiteOpenHelper;
+import com.android.support.db.framework.FrameworkSQLiteOpenHelperFactory;
+import com.android.support.room.util.TableInfo;
+
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+@SuppressWarnings("ArraysAsListWithZeroOrOneArgument")
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class TableInfoTest {
+    private SupportSQLiteDatabase mDb;
+
+    @Test
+    public void readSimple() {
+        mDb = createDatabase(
+                "CREATE TABLE foo (id INTEGER PRIMARY KEY AUTOINCREMENT,"
+                        + "name TEXT)");
+        TableInfo info = TableInfo.read(mDb, "foo");
+        assertThat(info, is(new TableInfo("foo",
+                toMap(new TableInfo.Column("id", "INTEGER", 1),
+                        new TableInfo.Column("name", "TEXT", 0)))));
+    }
+
+    @Test
+    public void multiplePrimaryKeys() {
+        mDb = createDatabase(
+                "CREATE TABLE foo (id INTEGER,"
+                        + "name TEXT, PRIMARY KEY(name, id))");
+        TableInfo info = TableInfo.read(mDb, "foo");
+        assertThat(info, is(new TableInfo("foo",
+                toMap(new TableInfo.Column("id", "INTEGER", 2),
+                        new TableInfo.Column("name", "TEXT", 1))
+        )));
+    }
+
+    @Test
+    public void alteredTable() {
+        mDb = createDatabase(
+                "CREATE TABLE foo (id INTEGER,"
+                        + "name TEXT, PRIMARY KEY(name))");
+        mDb.execSQL("ALTER TABLE foo ADD COLUMN added REAL;");
+        TableInfo info = TableInfo.read(mDb, "foo");
+        assertThat(info, is(new TableInfo("foo",
+                toMap(new TableInfo.Column("id", "INTEGER", 0),
+                        new TableInfo.Column("name", "TEXT", 1),
+                        new TableInfo.Column("added", "REAL", 0))
+        )));
+    }
+
+    @Test
+    public void nonNull() {
+        mDb = createDatabase(
+                "CREATE TABLE foo (name TEXT NOT NULL)");
+        TableInfo info = TableInfo.read(mDb, "foo");
+        assertThat(info, is(new TableInfo("foo",
+                toMap(new TableInfo.Column("name", "TEXT", 0))
+        )));
+    }
+
+    @Test
+    public void defaultValue() {
+        mDb = createDatabase(
+                "CREATE TABLE foo (name TEXT DEFAULT blah)");
+        TableInfo info = TableInfo.read(mDb, "foo");
+        assertThat(info, is(new TableInfo(
+                "foo",
+                toMap(new TableInfo.Column("name", "TEXT", 0))
+        )));
+    }
+
+    private static Map<String, TableInfo.Column> toMap(TableInfo.Column... columns) {
+        Map<String, TableInfo.Column> result = new HashMap<>();
+        for (TableInfo.Column column : columns) {
+            result.put(column.name, column);
+        }
+        return result;
+    }
+
+    @After
+    public void closeDb() throws IOException {
+        if (mDb != null && mDb.isOpen()) {
+            mDb.close();
+        }
+    }
+
+    private static SupportSQLiteDatabase createDatabase(final String... queries) {
+        return new FrameworkSQLiteOpenHelperFactory().create(
+                SupportSQLiteOpenHelper.Configuration
+                        .builder(InstrumentationRegistry.getTargetContext())
+                        .name(null)
+                        .version(1)
+                        .callback(new SupportSQLiteOpenHelper.Callback() {
+                            @Override
+                            public void onCreate(SupportSQLiteDatabase db) {
+                                for (String query : queries) {
+                                    db.execSQL(query);
+                                }
+                            }
+
+                            @Override
+                            public void onUpgrade(SupportSQLiteDatabase db, int oldVersion,
+                                    int newVersion) {
+                                throw new IllegalStateException("should not be upgrading");
+                            }
+                        }).build()
+        ).getWritableDatabase();
+    }
+}
diff --git a/room/runtime/src/main/java/com/android/support/room/DatabaseConfiguration.java b/room/runtime/src/main/java/com/android/support/room/DatabaseConfiguration.java
index 6a8d226..d4e0e1a 100644
--- a/room/runtime/src/main/java/com/android/support/room/DatabaseConfiguration.java
+++ b/room/runtime/src/main/java/com/android/support/room/DatabaseConfiguration.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
 
 import com.android.support.db.SupportSQLiteOpenHelper;
 
@@ -49,7 +50,18 @@
     @NonNull
     public final RoomDatabase.MigrationContainer migrationContainer;
 
-    DatabaseConfiguration(@NonNull Context context, @Nullable String name,
+    /**
+     * Creates a database configuration with the given values.
+     *
+     * @param context The application context.
+     * @param name Name of the database, can be null if it is in memory.
+     * @param sqliteOpenHelperFactory The open helper factory to use.
+     * @param migrationContainer The migration container for migrations.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public DatabaseConfiguration(@NonNull Context context, @Nullable String name,
             @NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory,
             @NonNull RoomDatabase.MigrationContainer migrationContainer) {
         this.sqliteOpenHelperFactory = sqliteOpenHelperFactory;
diff --git a/room/runtime/src/main/java/com/android/support/room/RoomOpenHelper.java b/room/runtime/src/main/java/com/android/support/room/RoomOpenHelper.java
index e03685d..2e9dc6a 100644
--- a/room/runtime/src/main/java/com/android/support/room/RoomOpenHelper.java
+++ b/room/runtime/src/main/java/com/android/support/room/RoomOpenHelper.java
@@ -71,12 +71,8 @@
                 for (Migration migration : migrations) {
                     migration.migrate(db);
                 }
-                if (mDelegate.validateMigration(db)) {
-                    updateIdentity(db);
-                } else {
-                    // TODO
-                    throw new RuntimeException("cannot validate the migration result");
-                }
+                mDelegate.validateMigration(db);
+                updateIdentity(db);
                 migrated = true;
             }
         }
@@ -144,7 +140,7 @@
          *
          * @param db The SQLite database.
          */
-        protected abstract boolean validateMigration(SupportSQLiteDatabase db);
+        protected abstract void validateMigration(SupportSQLiteDatabase db);
     }
 
 }
diff --git a/room/runtime/src/main/java/com/android/support/room/migration/Migration.java b/room/runtime/src/main/java/com/android/support/room/migration/Migration.java
index 462e4a1..24d4cce 100644
--- a/room/runtime/src/main/java/com/android/support/room/migration/Migration.java
+++ b/room/runtime/src/main/java/com/android/support/room/migration/Migration.java
@@ -26,6 +26,10 @@
  * <p>
  * Usually, you would need 1 Migration class for each version change but you can also provide
  * Migrations that can handle multiple version changes.
+ * <p>
+ * Room expects the migrated table to have the exact same structure as if it is created from
+ * scratch. This means the order of columns in the table must also be modified during the migration
+ * if necessary.
  */
 public abstract class Migration {
     public final int startVersion;
diff --git a/room/runtime/src/main/java/com/android/support/room/util/TableInfo.java b/room/runtime/src/main/java/com/android/support/room/util/TableInfo.java
new file mode 100644
index 0000000..ba18998
--- /dev/null
+++ b/room/runtime/src/main/java/com/android/support/room/util/TableInfo.java
@@ -0,0 +1,174 @@
+/*
+ * 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 com.android.support.room.util;
+
+import android.database.Cursor;
+import android.support.annotation.RestrictTo;
+
+import com.android.support.db.SupportSQLiteDatabase;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A data class that holds the information about a table.
+ * <p>
+ * It directly maps to the result of {@code PRAGMA table_info(<table_name>)}. Check the
+ * <a href="http://www.sqlite.org/pragma.html#pragma_table_info">PRAGMA table_info</a>
+ * documentation for more details.
+ * <p>
+ * Even though SQLite column names are case insensitive, this class uses case sensitive matching.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@SuppressWarnings({"WeakerAccess", "unused"})
+// if you change this class, you must change TableInfoWriter.kt
+public class TableInfo {
+    /**
+     * The table name.
+     */
+    public final String name;
+    /**
+     * Unmodifiable map of columns keyed by column name.
+     */
+    public final Map<String, Column> columns;
+
+    @SuppressWarnings("unused")
+    public TableInfo(String name, Map<String, Column> columns) {
+        this.name = name;
+        this.columns = Collections.unmodifiableMap(columns);
+    }
+
+    /**
+     * Reads the table information from the given database.
+     *
+     * @param database  The database to read the information from.
+     * @param tableName The table name.
+     * @return A TableInfo containing the schema information for the provided table name.
+     */
+    @SuppressWarnings("SameParameterValue")
+    public static TableInfo read(SupportSQLiteDatabase database, String tableName) {
+        Cursor cursor = database.rawQuery("PRAGMA table_info(`" + tableName + "`)",
+                StringUtil.EMPTY_STRING_ARRAY);
+        //noinspection TryFinallyCanBeTryWithResources
+        try {
+            Map<String, Column> columns = extractColumns(cursor);
+            return new TableInfo(tableName, columns);
+        } finally {
+            cursor.close();
+        }
+    }
+
+    private static Map<String, Column> extractColumns(Cursor cursor) {
+        Map<String, Column> columns = new HashMap<>();
+        if (cursor.getColumnCount() > 0) {
+            int nameIndex = cursor.getColumnIndex("name");
+            int typeIndex = cursor.getColumnIndex("type");
+            int pkIndex = cursor.getColumnIndex("pk");
+
+            while (cursor.moveToNext()) {
+                final String name = cursor.getString(nameIndex);
+                final String type = cursor.getString(typeIndex);
+                final int primaryKeyPosition = cursor.getInt(pkIndex);
+                columns.put(name, new Column(name, type, primaryKeyPosition));
+            }
+        }
+        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;
+
+        //noinspection SimplifiableIfStatement
+        if (name != null ? !name.equals(tableInfo.name) : tableInfo.name != null) return false;
+        return columns != null ? columns.equals(tableInfo.columns) : tableInfo.columns == null;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = name != null ? name.hashCode() : 0;
+        result = 31 * result + (columns != null ? columns.hashCode() : 0);
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return "TableInfo{name='" + name + '\'' + ", columns=" + columns + '}';
+    }
+
+    /**
+     * Holds the information about a database column.
+     */
+    @SuppressWarnings("WeakerAccess")
+    public static class Column {
+        /**
+         * The column name.
+         */
+        public final String name;
+        /**
+         * The column type affinity.
+         */
+        public final String type;
+        /**
+         * The position of the column in the list of primary keys, 0 if the column is not part
+         * of the primary key.
+         */
+        public final int primaryKeyPosition;
+
+        // if you change this constructor, you must change TableInfoWriter.kt
+        public Column(String name, String type, int primaryKeyPosition) {
+            this.name = name;
+            this.type = type;
+            this.primaryKeyPosition = primaryKeyPosition;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+
+            Column column = (Column) o;
+
+            if (primaryKeyPosition != column.primaryKeyPosition) return false;
+            //noinspection SimplifiableIfStatement
+            if (!name.equals(column.name)) return false;
+            return type != null ? type.equals(column.type) : column.type == null;
+        }
+
+        @Override
+        public int hashCode() {
+            int result = name.hashCode();
+            result = 31 * result + (type != null ? type.hashCode() : 0);
+            result = 31 * result + primaryKeyPosition;
+            return result;
+        }
+
+        @Override
+        public String toString() {
+            return "Column{"
+                    + "name='" + name + '\''
+                    + ", type='" + type + '\''
+                    + ", primaryKeyPosition=" + primaryKeyPosition
+                    + '}';
+        }
+    }
+}
diff --git a/room/testing/build.gradle b/room/testing/build.gradle
index 61790e0..0618794 100644
--- a/room/testing/build.gradle
+++ b/room/testing/build.gradle
@@ -48,6 +48,7 @@
 
 dependencies {
     compile project(":room:common")
+    compile project(":room:runtime")
     compile project(":room:db")
     compile project(":room:db-impl")
     compile project(":room:migration")
diff --git a/room/testing/src/main/java/com/android/support/room/testing/MigrationTestHelper.java b/room/testing/src/main/java/com/android/support/room/testing/MigrationTestHelper.java
index 6239239..cfdecc5 100644
--- a/room/testing/src/main/java/com/android/support/room/testing/MigrationTestHelper.java
+++ b/room/testing/src/main/java/com/android/support/room/testing/MigrationTestHelper.java
@@ -17,12 +17,21 @@
 package com.android.support.room.testing;
 
 import android.content.Context;
+import android.database.Cursor;
 import android.util.Log;
 
 import com.android.support.db.SupportSQLiteDatabase;
 import com.android.support.db.SupportSQLiteOpenHelper;
+import com.android.support.room.DatabaseConfiguration;
+import com.android.support.room.Room;
+import com.android.support.room.RoomDatabase;
+import com.android.support.room.RoomOpenHelper;
+import com.android.support.room.migration.Migration;
 import com.android.support.room.migration.bundle.DatabaseBundle;
+import com.android.support.room.migration.bundle.EntityBundle;
+import com.android.support.room.migration.bundle.FieldBundle;
 import com.android.support.room.migration.bundle.SchemaBundle;
+import com.android.support.room.util.TableInfo;
 
 import org.junit.rules.TestWatcher;
 import org.junit.runner.Description;
@@ -32,7 +41,9 @@
 import java.io.InputStream;
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 /**
  * A class that can be used in your Instrumentation tests that can create the database in an
@@ -55,19 +66,19 @@
  *   }
  * }
  * </pre>
- *
  */
 public class MigrationTestHelper extends TestWatcher {
     private static final String TAG = "MigrationTestHelper";
     private final Context mContext;
     private final String mAssetsFolder;
     private final SupportSQLiteOpenHelper.Factory mOpenFactory;
-    private List<WeakReference<SupportSQLiteDatabase>> mCreatedDatabases = new ArrayList<>();
+    private List<WeakReference<SupportSQLiteDatabase>> mManagedDatabases = new ArrayList<>();
+    private boolean mTestStarted;
 
     /**
      * Creates a new migration helper with an asset folder and the context.
      *
-     * @param context The context to read assets and create the database.
+     * @param context      The context to read assets and create the database.
      * @param assetsFolder The asset folder in the assets directory.
      */
     public MigrationTestHelper(Context context, String assetsFolder,
@@ -80,14 +91,19 @@
         mOpenFactory = openFactory;
     }
 
+    @Override
+    protected void starting(Description description) {
+        super.starting(description);
+        mTestStarted = true;
+    }
+
     /**
      * Creates the database in the given version.
      * If the database file already exists, it tries to delete it first. If delete fails, throws
      * an exception.
      *
-     * @param name The name of the database.
+     * @param name    The name of the database.
      * @param version The version in which the database should be created.
-     *
      * @return A database connection which has the schema in the requested version.
      * @throws IOException If it cannot find the schema description in the assets folder.
      */
@@ -103,22 +119,74 @@
             }
         }
         SchemaBundle schemaBundle = loadSchema(version);
+        RoomDatabase.MigrationContainer container = new RoomDatabase.MigrationContainer();
+        DatabaseConfiguration configuration = new DatabaseConfiguration(
+                mContext, name, mOpenFactory, container);
+        RoomOpenHelper roomOpenHelper = new RoomOpenHelper(configuration,
+                new CreatingDelegate(schemaBundle.getDatabase()),
+                schemaBundle.getDatabase().getIdentityHash());
+        return openDatabase(name, version, roomOpenHelper);
+    }
+
+    /**
+     * Runs the given set of migrations on the provided database.
+     * <p>
+     * It uses the same algorithm that Room uses to choose migrations so the migrations instances
+     * that are provided to this method must be sufficient to bring the database from current
+     * version to the desired version.
+     * <p>
+     * After the migration, the method validates the database schema to ensure that migration
+     * result matches the expected schema. Handling of dropped tables depends on the
+     * {@code validateDroppedTables} argument. If set to true, the verification will fail if it
+     * finds a table that is not registered in the Database. If set to false, extra tables in the
+     * database will be ignored (this is the runtime library behavior).
+     *
+     * @param name       The database name. You must first create this database via
+     *                   {@link #createDatabase(String, int)}.
+     * @param version    The final version after applying the migrations.
+     * @param validateDroppedTables If set to true, validation will fail if the database has unknown
+     *                           tables.
+     * @param migrations The list of available migrations.
+     * @throws IOException           If it cannot find the schema for {@code toVersion}.
+     * @throws IllegalStateException If the schema validation fails.
+     */
+    public SupportSQLiteDatabase runMigrationsAndValidate(String name, int version,
+            boolean validateDroppedTables, Migration... migrations) throws IOException {
+        File dbPath = mContext.getDatabasePath(name);
+        if (!dbPath.exists()) {
+            throw new IllegalStateException("Cannot find the database file for " + name + ". "
+                    + "Before calling runMigrations, you must first create the database via "
+                    + "createDatabase.");
+        }
+        SchemaBundle schemaBundle = loadSchema(version);
+        RoomDatabase.MigrationContainer container = new RoomDatabase.MigrationContainer();
+        container.addMigrations(migrations);
+        DatabaseConfiguration configuration = new DatabaseConfiguration(
+                mContext, name, mOpenFactory, container);
+        RoomOpenHelper roomOpenHelper = new RoomOpenHelper(configuration,
+                new MigratingDelegate(schemaBundle.getDatabase(), validateDroppedTables),
+                schemaBundle.getDatabase().getIdentityHash());
+        return openDatabase(name, version, roomOpenHelper);
+    }
+
+    private SupportSQLiteDatabase openDatabase(String name, int version,
+            RoomOpenHelper roomOpenHelper) {
         SupportSQLiteOpenHelper.Configuration config =
                 SupportSQLiteOpenHelper.Configuration
                         .builder(mContext)
-                        .callback(new SchemaOpenCallback(schemaBundle.getDatabase()))
+                        .callback(roomOpenHelper)
                         .name(name)
                         .version(version)
                         .build();
         SupportSQLiteDatabase db = mOpenFactory.create(config).getWritableDatabase();
-        mCreatedDatabases.add(new WeakReference<>(db));
+        mManagedDatabases.add(new WeakReference<>(db));
         return db;
     }
 
     @Override
     protected void finished(Description description) {
         super.finished(description);
-        for (WeakReference<SupportSQLiteDatabase> dbRef : mCreatedDatabases) {
+        for (WeakReference<SupportSQLiteDatabase> dbRef : mManagedDatabases) {
             SupportSQLiteDatabase db = dbRef.get();
             if (db != null && db.isOpen()) {
                 try {
@@ -129,27 +197,137 @@
         }
     }
 
+    /**
+     * Registers a database connection to be automatically closed when the test finishes.
+     * <p>
+     * This only works if {@code MigrationTestHelper} is registered as a Junit test rule via
+     * {@link org.junit.Rule Rule} annotation.
+     *
+     * @param db The database connection that should be closed after the test finishes.
+     */
+    public void closeWhenFinished(SupportSQLiteDatabase db) {
+        if (!mTestStarted) {
+            throw new IllegalStateException("You cannot register a database to be closed before"
+                    + " the test starts. Maybe you forgot to annotate MigrationTestHelper as a"
+                    + " test rule? (@Rule)");
+        }
+        mManagedDatabases.add(new WeakReference<>(db));
+    }
+
     private SchemaBundle loadSchema(int version) throws IOException {
         InputStream input = mContext.getAssets().open(mAssetsFolder + "/" + version + ".json");
         return SchemaBundle.deserialize(input);
     }
 
-    static class SchemaOpenCallback extends SupportSQLiteOpenHelper.Callback {
-        private final DatabaseBundle mDatabaseBundle;
-        SchemaOpenCallback(DatabaseBundle databaseBundle) {
-            mDatabaseBundle = databaseBundle;
+    private static TableInfo toTableInfo(EntityBundle entityBundle) {
+        return new TableInfo(entityBundle.getTableName(), toColumnMap(entityBundle));
+    }
+
+    private static Map<String, TableInfo.Column> toColumnMap(EntityBundle entity) {
+        Map<String, TableInfo.Column> result = new HashMap<>();
+        for (FieldBundle bundle : entity.getFields()) {
+            TableInfo.Column column = toColumn(entity, bundle);
+            result.put(column.name, column);
+        }
+        return result;
+    }
+
+    private static TableInfo.Column toColumn(EntityBundle entity, FieldBundle field) {
+        return new TableInfo.Column(field.getColumnName(), field.getAffinity(),
+                findPrimaryKeyPosition(entity, field));
+    }
+
+    private static int findPrimaryKeyPosition(EntityBundle entity, FieldBundle field) {
+        List<String> columnNames = entity.getPrimaryKey().getColumnNames();
+        int i = 0;
+        for (String columnName : columnNames) {
+            i++;
+            if (field.getColumnName().equalsIgnoreCase(columnName)) {
+                return i;
+            }
+        }
+        return 0;
+    }
+
+    class MigratingDelegate extends RoomOpenHelperDelegate {
+        private final boolean mVerifyDroppedTables;
+        MigratingDelegate(DatabaseBundle databaseBundle, boolean verifyDroppedTables) {
+            super(databaseBundle);
+            mVerifyDroppedTables = verifyDroppedTables;
         }
 
         @Override
-        public void onCreate(SupportSQLiteDatabase db) {
+        protected void createAllTables(SupportSQLiteDatabase database) {
+            throw new UnsupportedOperationException("Was expecting to migrate but received create."
+                    + "Make sure you have created the database first.");
+        }
+
+        @Override
+        protected void validateMigration(SupportSQLiteDatabase db) {
+            final Map<String, EntityBundle> tables = mDatabaseBundle.getEntitiesByTableName();
+            for (EntityBundle entity : tables.values()) {
+                final TableInfo expected = toTableInfo(entity);
+                final TableInfo found = TableInfo.read(db, entity.getTableName());
+                if (!expected.equals(found)) {
+                    throw new IllegalStateException(
+                            "Migration failed. expected:" + expected + " , found:" + found);
+                }
+            }
+            if (mVerifyDroppedTables) {
+                // now ensure tables that should be removed are removed.
+                Cursor cursor = db.rawQuery("SELECT name FROM sqlite_master WHERE type='table'"
+                                + " AND name NOT IN(?, ?)",
+                        new String[]{Room.MASTER_TABLE_NAME, "android_metadata"});
+                //noinspection TryFinallyCanBeTryWithResources
+                try {
+                    while (cursor.moveToNext()) {
+                        final String tableName = cursor.getString(0);
+                        if (!tables.containsKey(tableName)) {
+                            throw new IllegalStateException("unexpected table " + tableName);
+                        }
+                    }
+                } finally {
+                    cursor.close();
+                }
+            }
+        }
+    }
+
+    static class CreatingDelegate extends RoomOpenHelperDelegate {
+
+        CreatingDelegate(DatabaseBundle databaseBundle) {
+            super(databaseBundle);
+        }
+
+        @Override
+        protected void createAllTables(SupportSQLiteDatabase database) {
             for (String query : mDatabaseBundle.buildCreateQueries()) {
-                db.execSQL(query);
+                database.execSQL(query);
             }
         }
 
         @Override
-        public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
-            throw new UnsupportedOperationException("cannot upgrade when creating database");
+        protected void validateMigration(SupportSQLiteDatabase db) {
+            throw new UnsupportedOperationException("This open helper just creates the database but"
+                    + " it received a migration request.");
+        }
+    }
+
+    abstract static class RoomOpenHelperDelegate extends RoomOpenHelper.Delegate {
+        final DatabaseBundle mDatabaseBundle;
+
+        RoomOpenHelperDelegate(DatabaseBundle databaseBundle) {
+            mDatabaseBundle = databaseBundle;
+        }
+
+        @Override
+        protected void dropAllTables(SupportSQLiteDatabase database) {
+            throw new UnsupportedOperationException("cannot drop all tables in the test");
+        }
+
+        @Override
+        protected void onOpen(SupportSQLiteDatabase database) {
+
         }
     }
 }