Room Migrations, Step 1

This CL adds ability to specify migration callbacks.
Developer can specify a list of migrations while building the database
and Room will run them. If a path cannot be found, Room will nuke the
database and recreate.

This CL moves the "version" parameter of the database from the
builder into the annotation. This allows us to export the schema
at compile time to be used later.

This CL also introduces a new "testing" package for Room which is
capable of initializing the database based on the exported schema.
This can be used by the developers to create the database in a
previous schema and migrate to the current one.

Test: MigrationTest, BuilderTest, TableInfoTest
Bug: 36602348
Change-Id: I7d543b20410b14ff61ef1b433c38c44009c86e8d
diff --git a/app-toolkit/dependencies.gradle b/app-toolkit/dependencies.gradle
index c6cb27d..86aa6ab 100644
--- a/app-toolkit/dependencies.gradle
+++ b/app-toolkit/dependencies.gradle
@@ -37,6 +37,7 @@
 ffVersions.xerial = "3.16.1"
 ffVersions.antlr = "4.5.3"
 ffVersions.commons_codec = "1.10"
+ffVersions.gson = "2.8.0"
 
 ffLibs.kotlin = [
         stdlib : "org.jetbrains.kotlin:kotlin-stdlib:$ffVersions.kotlin",
@@ -77,6 +78,7 @@
 ffLibs.ij_annotations = "com.intellij:annotations:$ffVersions.intellij_annotations"
 ffLibs.reactive_streams = "org.reactivestreams:reactive-streams:$ffVersions.reactivestreams"
 ffLibs.rx_java = "io.reactivex.rxjava2:rxjava:$ffVersions.rxjava2"
+ffLibs.gson = "com.google.code.gson:gson:$ffVersions.gson"
 
 ext.tools = [:]
 ext.tools.current_sdk = gradle.ext.currentSdk
diff --git a/app-toolkit/settings.gradle b/app-toolkit/settings.gradle
index a88d833..61f2e38 100644
--- a/app-toolkit/settings.gradle
+++ b/app-toolkit/settings.gradle
@@ -60,12 +60,18 @@
 include ':room:compiler'
 project(':room:compiler').projectDir = new File(supportRoot, "room/compiler")
 
+include ':room:migration'
+project(':room:migration').projectDir = new File(supportRoot, "room/migration")
+
 include ':room:db'
 project(':room:db').projectDir = new File(supportRoot, "room/db")
 
 include ":room:db-impl"
 project(':room:db-impl').projectDir = new File(supportRoot, "room/db-impl")
 
+include ":room:testing"
+project(':room:testing').projectDir = new File(supportRoot, "room/testing")
+
 include ':room:integration-tests:testapp'
 project(':room:integration-tests:testapp').projectDir = new File(supportRoot, "room/integration-tests/testapp")
 
diff --git a/room/common/src/main/java/com/android/support/room/Database.java b/room/common/src/main/java/com/android/support/room/Database.java
index 7e40019..b2082c6 100644
--- a/room/common/src/main/java/com/android/support/room/Database.java
+++ b/room/common/src/main/java/com/android/support/room/Database.java
@@ -69,4 +69,28 @@
      * @return The list of entities in the database.
      */
     Class[] entities();
+
+    /**
+     * The database version.
+     *
+     * @return The database version.
+     */
+    int version();
+
+    /**
+     * You can set annotation processor argument ({@code room.schemaLocation})
+     * to tell Room to export the schema into a folder. Even though it is not mandatory, it is a
+     * good practice to have version history in your codebase and you should commit that file into
+     * your version control system (but don't ship it with your app!).
+     * <p>
+     * When {@code room.schemaLocation} is set, Room will check this variable and if it is set to
+     * {@code true}, its schema will be exported into the given folder.
+     * <p>
+     * {@code exportSchema} is {@code true} by default but you can disable it for databases when
+     * you don't want to keep history of versions (like an in-memory only database).
+     *
+     * @return Whether the schema should be exported to the given folder when the
+     * {@code room.schemaLocation} argument is set. Defaults to {@code true}.
+     */
+    boolean exportSchema() default true;
 }
diff --git a/room/common/src/main/java/com/android/support/room/RoomMasterTable.java b/room/common/src/main/java/com/android/support/room/RoomMasterTable.java
new file mode 100644
index 0000000..5606daa
--- /dev/null
+++ b/room/common/src/main/java/com/android/support/room/RoomMasterTable.java
@@ -0,0 +1,55 @@
+/*
+ * 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;
+
+import android.support.annotation.RestrictTo;
+
+/**
+ * Schema information about Room's master table.
+ *
+ * @hide
+ */
+@SuppressWarnings("WeakerAccess")
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class RoomMasterTable {
+    /**
+     * The master table where room keeps its metadata information.
+     */
+    public static final String TABLE_NAME = "room_master_table";
+    // must match the runtime property Room#MASTER_TABLE_NAME
+    public static final String NAME = "room_master_table";
+    private static final String COLUMN_ID = "id";
+    private static final String COLUMN_IDENTITY_HASH = "identity_hash";
+    public static final String DEFAULT_ID = "42";
+
+    public static final String CREATE_QUERY = "CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " ("
+            + COLUMN_ID + " INTEGER PRIMARY KEY,"
+            + COLUMN_IDENTITY_HASH + " TEXT)";
+
+    public static final String READ_QUERY = "SELECT " + COLUMN_IDENTITY_HASH
+            + " FROM " + TABLE_NAME + " WHERE "
+            + COLUMN_ID + " = " + DEFAULT_ID + " LIMIT 1";
+
+    /**
+     * We don't escape here since we know what we are passing.
+     */
+    public static String createInsertQuery(String hash) {
+        return "INSERT OR REPLACE INTO " + TABLE_NAME + " ("
+                + COLUMN_ID + "," + COLUMN_IDENTITY_HASH + ")"
+                + " VALUES(" + DEFAULT_ID + ", \"" + hash + "\")";
+    }
+}
diff --git a/room/common/src/main/java/com/android/support/room/RoomWarnings.java b/room/common/src/main/java/com/android/support/room/RoomWarnings.java
index 6d0aaeb..06cea02 100644
--- a/room/common/src/main/java/com/android/support/room/RoomWarnings.java
+++ b/room/common/src/main/java/com/android/support/room/RoomWarnings.java
@@ -98,4 +98,12 @@
      * in the parent. Room will still do the matching using {@code String} representations.
      */
     public static final String RELATION_TYPE_MISMATCH = "ROOM_RELATION_TYPE_MISMATCH";
+
+    /**
+     * Reported when a `room.schemaLocation` argument is not provided into the annotation processor.
+     * You can either set {@link Database#exportSchema()} to {@code false} or provide
+     * `room.schemaLocation` to the annotation processor. You are strongly adviced to provide it
+     * and also commit them into your version control system.
+     */
+    public static final String MISSING_SCHEMA_LOCATION = "ROOM_MISSING_SCHEMA_LOCATION";
 }
diff --git a/room/compiler/build.gradle b/room/compiler/build.gradle
index 27492f4..dacdf90 100644
--- a/room/compiler/build.gradle
+++ b/room/compiler/build.gradle
@@ -30,6 +30,7 @@
     def sdkHandler = new com.android.build.gradle.internal.SdkHandler(project, logger)
 
     compile project(":room:common")
+    compile project(":room:migration")
     compile libs.kotlin.stdlib
     compile libs.auto_common
     compile libs.javapoet
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/RoomProcessor.kt b/room/compiler/src/main/kotlin/com/android/support/room/RoomProcessor.kt
index d3dda6f..1e3f7f3 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/RoomProcessor.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/RoomProcessor.kt
@@ -18,12 +18,15 @@
 
 import com.android.support.room.processor.Context
 import com.android.support.room.processor.DatabaseProcessor
+import com.android.support.room.processor.ProcessorErrors
 import com.android.support.room.vo.DaoMethod
+import com.android.support.room.vo.Warning
 import com.android.support.room.writer.DaoWriter
 import com.android.support.room.writer.DatabaseWriter
 import com.google.auto.common.BasicAnnotationProcessor
 import com.google.auto.common.MoreElements
 import com.google.common.collect.SetMultimap
+import java.io.File
 import javax.annotation.processing.SupportedSourceVersion
 import javax.lang.model.SourceVersion
 import javax.lang.model.element.Element
@@ -38,6 +41,10 @@
         return arrayListOf(DatabaseProcessingStep(context))
     }
 
+    override fun getSupportedOptions(): MutableSet<String> {
+        return Context.ARG_OPTIONS.toMutableSet()
+    }
+
     class DatabaseProcessingStep(context: Context) : ContextBoundProcessingStep(context) {
         override fun process(elementsByAnnotation: SetMultimap<Class<out Annotation>, Element>)
                 : MutableSet<Element> {
@@ -54,15 +61,31 @@
                 }
             }
 
-            databases?.forEach {
-                DatabaseWriter(it).write(context.processingEnv)
+            databases?.forEach { db ->
+                DatabaseWriter(db).write(context.processingEnv)
+                if (db.exportSchema) {
+                    val schemaOutFolder = context.schemaOutFolder
+                    if (schemaOutFolder == null) {
+                        context.logger.w(Warning.MISSING_SCHEMA_LOCATION, db.element,
+                                ProcessorErrors.MISSING_SCHEMA_EXPORT_DIRECTORY)
+                    } else {
+                        if (!schemaOutFolder.exists()) {
+                            schemaOutFolder.mkdirs()
+                        }
+                        val qName = db.element.qualifiedName.toString()
+                        val dbSchemaFolder = File(schemaOutFolder, qName)
+                        if (!dbSchemaFolder.exists()) {
+                            dbSchemaFolder.mkdirs()
+                        }
+                        db.exportSchema(File(dbSchemaFolder, "${db.version}.json"))
+                    }
+                }
             }
             context.databaseVerifier?.let {
                 it.closeConnection()
             }
             return mutableSetOf()
         }
-
         override fun annotations(): MutableSet<out Class<out Annotation>> {
             return mutableSetOf(Database::class.java, Dao::class.java, Entity::class.java)
         }
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 d0a66e1..31ac485 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
@@ -65,6 +65,10 @@
             ClassName.get("com.android.support.room.InvalidationTracker", "Observer")
     val ROOM_SQL_QUERY : ClassName =
             ClassName.get("com.android.support.room", "RoomSQLiteQuery")
+    val OPEN_HELPER : ClassName =
+            ClassName.get("com.android.support.room", "RoomOpenHelper")
+    val OPEN_HELPER_DELEGATE: ClassName =
+            ClassName.get("com.android.support.room", "RoomOpenHelper.Delegate")
 }
 
 object LifecyclesTypeNames {
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/processor/Context.kt b/room/compiler/src/main/kotlin/com/android/support/room/processor/Context.kt
index d505775..9bcd95d 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/processor/Context.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/processor/Context.kt
@@ -21,11 +21,12 @@
 import com.android.support.room.solver.TypeAdapterStore
 import com.android.support.room.solver.types.TypeConverter
 import com.android.support.room.verifier.DatabaseVerifier
+import java.io.File
 import javax.annotation.processing.ProcessingEnvironment
 import javax.lang.model.element.Element
 import javax.lang.model.type.TypeMirror
 
-data class Context private constructor(val processingEnv: ProcessingEnvironment,
+class Context private constructor(val processingEnv: ProcessingEnvironment,
                                        val logger: RLog, val typeConverters: List<TypeConverter>) {
     val checker: Checks = Checks(logger)
     val COMMON_TYPES: Context.CommonTypes = Context.CommonTypes(processingEnv)
@@ -35,6 +36,12 @@
     // set when database and its entities are processed.
     var databaseVerifier : DatabaseVerifier? = null
 
+    companion object {
+        val ARG_OPTIONS by lazy {
+            ProcessorOptions.values().map { it.argName }
+        }
+    }
+
     constructor(processingEnv: ProcessingEnvironment) : this(processingEnv,
             RLog(RLog.ProcessingEnvMessager(processingEnv), emptySet(), null), emptyList()) {
     }
@@ -45,6 +52,15 @@
         }
     }
 
+    val schemaOutFolder by lazy {
+        val arg = processingEnv.options[ProcessorOptions.OPTION_SCHEMA_FOLDER.argName]
+        if (arg?.isNotEmpty() ?: false) {
+            File(arg)
+        } else {
+            null
+        }
+    }
+
     fun <T> collectLogs(handler: (Context) -> T): Pair<T, RLog.CollectingMessager> {
         val collector = RLog.CollectingMessager()
         val subContext = Context(processingEnv,
@@ -64,4 +80,8 @@
         subContext.databaseVerifier = databaseVerifier
         return subContext
     }
+
+    enum class ProcessorOptions(val argName : String) {
+        OPTION_SCHEMA_FOLDER("room.schemaLocation")
+    }
 }
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/processor/DatabaseProcessor.kt b/room/compiler/src/main/kotlin/com/android/support/room/processor/DatabaseProcessor.kt
index 7a036a8..452f798 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/processor/DatabaseProcessor.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/processor/DatabaseProcessor.kt
@@ -18,6 +18,8 @@
 
 import com.android.support.room.SkipQueryVerification
 import com.android.support.room.ext.RoomTypeNames
+import com.android.support.room.ext.getAsBoolean
+import com.android.support.room.ext.getAsInt
 import com.android.support.room.ext.hasAnnotation
 import com.android.support.room.ext.hasAnyOf
 import com.android.support.room.ext.toListOfClassTypes
@@ -81,10 +83,17 @@
         }
         validateUniqueDaoClasses(element, daoMethods)
         validateUniqueIndices(element, entities)
-        val database = Database(element = element,
+        val version = AnnotationMirrors.getAnnotationValue(dbAnnotation, "version")
+                .getAsInt(1)!!.toInt()
+        val exportSchema = AnnotationMirrors.getAnnotationValue(dbAnnotation, "exportSchema")
+                .getAsBoolean(true)
+        val database = Database(
+                version = version,
+                element = element,
                 type = MoreElements.asType(element).asType(),
                 entities = entities,
-                daoMethods = daoMethods)
+                daoMethods = daoMethods,
+                exportSchema = exportSchema)
         return database
     }
 
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/processor/ProcessorErrors.kt b/room/compiler/src/main/kotlin/com/android/support/room/processor/ProcessorErrors.kt
index 7e78e50..4c1b75b 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/processor/ProcessorErrors.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/processor/ProcessorErrors.kt
@@ -336,4 +336,8 @@
         Available columns are: ${availableColumnNames.joinToString(",")}
         """.trim()
     }
+
+    val MISSING_SCHEMA_EXPORT_DIRECTORY = "Schema export directory is not provided to the" +
+            " annotation processor so we cannot export the schema. You can either provide" +
+            " `room.schemaLocation` annotation processor argument OR set exportSchema to false."
 }
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/vo/Database.kt b/room/compiler/src/main/kotlin/com/android/support/room/vo/Database.kt
index dda4a3d..1b637bc 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/vo/Database.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/vo/Database.kt
@@ -16,33 +16,45 @@
 
 package com.android.support.room.vo
 
+import com.android.support.room.RoomMasterTable
+import com.android.support.room.migration.bundle.DatabaseBundle
+import com.android.support.room.migration.bundle.SchemaBundle
 import com.squareup.javapoet.ClassName
 import org.apache.commons.codec.digest.DigestUtils
+import java.io.File
 import javax.lang.model.element.TypeElement
 import javax.lang.model.type.TypeMirror
 
 /**
  * Holds information about a class annotated with Database.
  */
-data class Database(val element : TypeElement,
-                    val type : TypeMirror,
-                    val entities : List<Entity>,
-                    val daoMethods : List<DaoMethod>) {
-    val typeName : ClassName by lazy { ClassName.get(element)}
+data class Database(val element: TypeElement,
+                    val type: TypeMirror,
+                    val entities: List<Entity>,
+                    val daoMethods: List<DaoMethod>,
+                    val version: Int,
+                    val exportSchema: Boolean) {
+    val typeName: ClassName by lazy { ClassName.get(element) }
 
     private val implClassName by lazy {
         "${typeName.simpleNames().joinToString("_")}_Impl"
     }
 
-    val implTypeName : ClassName by lazy {
+    val implTypeName: ClassName by lazy {
         ClassName.get(typeName.packageName(), implClassName)
     }
 
+    val bundle by lazy {
+        DatabaseBundle(version, identityHash, entities.map(Entity::toBundle),
+                listOf(RoomMasterTable.CREATE_QUERY,
+                        RoomMasterTable.createInsertQuery(identityHash)))
+    }
+
     /**
      * Create a has that identifies this database definition so that at runtime we can check to
      * ensure developer didn't forget to update the version.
      */
-    val identityHash : String by lazy {
+    val identityHash: String by lazy {
         val entityDescriptions = entities
                 .sortedBy { it.tableName }
                 .map { it.createTableQuery }
@@ -55,4 +67,9 @@
         val input = (entityDescriptions + indexDescriptions).joinToString("¯\\_(ツ)_/¯")
         DigestUtils.md5Hex(input)
     }
+
+    fun exportSchema(file: File) {
+        val schemaBundle = SchemaBundle(SchemaBundle.LATEST_FORMAT, bundle)
+        SchemaBundle.serialize(schemaBundle, file)
+    }
 }
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/vo/Entity.kt b/room/compiler/src/main/kotlin/com/android/support/room/vo/Entity.kt
index be5b613..a49052f 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/vo/Entity.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/vo/Entity.kt
@@ -16,6 +16,8 @@
 
 package com.android.support.room.vo
 
+import com.android.support.room.migration.bundle.BundleUtil
+import com.android.support.room.migration.bundle.EntityBundle
 import javax.lang.model.element.TypeElement
 import javax.lang.model.type.DeclaredType
 
@@ -24,13 +26,16 @@
              fields: List<Field>, decomposedFields: List<DecomposedField>,
              val primaryKey: PrimaryKey, val indices: List<Index>)
     : Pojo(element, type, fields, decomposedFields, emptyList()) {
-
     val createTableQuery by lazy {
+        createTableQuery(tableName)
+    }
+
+    fun createTableQuery(tableName : String) : String {
         val definitions = (fields.map {
             val autoIncrement = primaryKey.autoGenerateId && primaryKey.fields.contains(it)
             it.databaseDefinition(autoIncrement)
         } + createPrimaryKeyDefinition()).filterNotNull()
-        "CREATE TABLE IF NOT EXISTS `$tableName` (${definitions.joinToString(", ")})"
+        return "CREATE TABLE IF NOT EXISTS `$tableName` (${definitions.joinToString(", ")})"
     }
 
     val createIndexQueries by lazy {
@@ -49,4 +54,11 @@
             "PRIMARY KEY($keys)"
         }
     }
+
+    fun toBundle(): EntityBundle = EntityBundle(
+            tableName,
+            createTableQuery(BundleUtil.TABLE_NAME_PLACEHOLDER),
+            fields.map {it.toBundle()},
+            primaryKey.toBundle(),
+            indices.map { it.toBundle() })
 }
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/vo/Field.kt b/room/compiler/src/main/kotlin/com/android/support/room/vo/Field.kt
index e5f946d..06be7cb 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/vo/Field.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/vo/Field.kt
@@ -17,6 +17,7 @@
 package com.android.support.room.vo
 
 import com.android.support.room.ext.typeName
+import com.android.support.room.migration.bundle.FieldBundle
 import com.android.support.room.parser.SQLTypeAffinity
 import com.android.support.room.solver.types.CursorValueReader
 import com.android.support.room.solver.types.StatementValueBinder
@@ -112,4 +113,8 @@
         }
         return "`$columnName` ${(affinity ?: SQLTypeAffinity.TEXT).name}$columnSpec"
     }
+
+    fun toBundle(): FieldBundle = FieldBundle(pathWithDotNotation, columnName,
+            affinity?.name ?: SQLTypeAffinity.TEXT.name
+    )
 }
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/vo/Index.kt b/room/compiler/src/main/kotlin/com/android/support/room/vo/Index.kt
index a5db625..78c486e 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/vo/Index.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/vo/Index.kt
@@ -16,6 +16,9 @@
 
 package com.android.support.room.vo
 
+import com.android.support.room.migration.bundle.BundleUtil
+import com.android.support.room.migration.bundle.IndexBundle
+
 /**
  * Represents a processed index.
  */
@@ -32,4 +35,7 @@
             ON `$tableName` (${fields.map { it.columnName }.joinToString(", ") { "`$it`"}})
             """.trimIndent().replace(System.lineSeparator(), " ")
     }
+
+    fun toBundle(): IndexBundle = IndexBundle(name, unique, fields.map { it.columnName },
+            createQuery(BundleUtil.TABLE_NAME_PLACEHOLDER))
 }
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/vo/PrimaryKey.kt b/room/compiler/src/main/kotlin/com/android/support/room/vo/PrimaryKey.kt
index 59be322..b085b32 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/vo/PrimaryKey.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/vo/PrimaryKey.kt
@@ -15,6 +15,7 @@
  */
 package com.android.support.room.vo
 
+import com.android.support.room.migration.bundle.PrimaryKeyBundle
 import javax.lang.model.element.Element
 
 /**
@@ -30,4 +31,7 @@
         return "PrimaryKey[" +
                 fields.joinToString(separator = ", ", transform = Field::getPath) + "]"
     }
+
+    fun toBundle(): PrimaryKeyBundle = PrimaryKeyBundle(
+            autoGenerateId, fields.map { it.columnName })
 }
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/vo/Warning.kt b/room/compiler/src/main/kotlin/com/android/support/room/vo/Warning.kt
index 9bca6c9..6438e3b 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/vo/Warning.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/vo/Warning.kt
@@ -29,7 +29,8 @@
     INDEX_FROM_DECOMPOSED_ENTITY_IS_DROPPED("ROOM_DECOMPOSED_ENTITY_INDEX_IS_DROPPED"),
     INDEX_FROM_PARENT_IS_DROPPED("ROOM_PARENT_INDEX_IS_DROPPED"),
     INDEX_FROM_PARENT_FIELD_IS_DROPPED("ROOM_PARENT_FIELD_INDEX_IS_DROPPED"),
-    RELATION_TYPE_MISMATCH("ROOM_RELATION_TYPE_MISMATCH");
+    RELATION_TYPE_MISMATCH("ROOM_RELATION_TYPE_MISMATCH"),
+    MISSING_SCHEMA_LOCATION("ROOM_MISSING_SCHEMA_LOCATION");
 
     companion object {
         val PUBLIC_KEY_MAP = Warning.values().associateBy { it.publicKey }
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 2ae4bf7..a42ba76 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
@@ -17,14 +17,12 @@
 package com.android.support.room.writer
 
 import android.support.annotation.VisibleForTesting
-import com.android.support.room.ext.AndroidTypeNames
 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.SupportDbTypeNames
 import com.android.support.room.ext.T
-import com.android.support.room.ext.typeName
 import com.android.support.room.solver.CodeGenScope
 import com.android.support.room.vo.Database
 import com.android.support.room.vo.Entity
@@ -32,38 +30,33 @@
 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
 
 /**
  * Create an open helper using SupportSQLiteOpenHelperFactory
  */
 class SQLiteOpenHelperWriter(val database : Database) {
-    companion object {
-        // must match the runtime property Room#MASTER_TABLE_NAME
-        val MASTER_TABLE_NAME = "room_master_table"
-        val MASTER_TABLE_ID_COLUMN = "id"
-        val MASTER_TABLE_IDENTITY_HASH_COLUMN = "identity_hash"
-        val MASTER_TABLE_ID = 42
-    }
     fun write(outVar : String, configuration : ParameterSpec, scope: CodeGenScope) {
         scope.builder().apply {
             val sqliteConfigVar = scope.getTmpVar("_sqliteConfig")
             val callbackVar = scope.getTmpVar("_openCallback")
-            addStatement("final $T $L = $L",
+            addStatement("final $T $L = new $T($N, $L, $S)",
                     SupportDbTypeNames.SQLITE_OPEN_HELPER_CALLBACK,
-                    callbackVar, createOpenCallback())
+                    callbackVar, RoomTypeNames.OPEN_HELPER, configuration, createOpenCallback(),
+                    database.identityHash)
             // build configuration
             addStatement(
                     """
                     final $T $L = $T.builder($N.context)
                     .name($N.name)
-                    .version($N.version)
+                    .version($L)
                     .callback($L)
                     .build()
                     """.trimIndent(),
                     SupportDbTypeNames.SQLITE_OPEN_HELPER_CONFIG, sqliteConfigVar,
                     SupportDbTypeNames.SQLITE_OPEN_HELPER_CONFIG,
-                    configuration, configuration, configuration, callbackVar)
+                    configuration, configuration, database.version, callbackVar)
             addStatement("final $T $N = $N.sqliteOpenHelperFactory.create($L)",
                     SupportDbTypeNames.SQLITE_OPEN_HELPER, outVar,
                     configuration, sqliteConfigVar)
@@ -72,11 +65,20 @@
 
     private fun createOpenCallback() : TypeSpec {
         return TypeSpec.anonymousClassBuilder("").apply {
-            superclass(SupportDbTypeNames.SQLITE_OPEN_HELPER_CALLBACK)
-            addMethod(createOnCreate())
-            addMethod(createOnUpgrade())
-            addMethod(createOnDowngrade())
+            superclass(RoomTypeNames.OPEN_HELPER_DELEGATE)
+            addMethod(createCreateAllTables())
+            addMethod(createDropAllTables())
             addMethod(createOnOpen())
+            addMethod(createValidateMigration())
+        }.build()
+    }
+
+    private fun createValidateMigration(): MethodSpec {
+        return MethodSpec.methodBuilder("validateMigration").apply {
+            addModifiers(PROTECTED)
+            returns(TypeName.BOOLEAN)
+            addParameter(SupportDbTypeNames.DB, "_db")
+            addStatement("return true")
         }.build()
     }
 
@@ -84,100 +86,31 @@
         return MethodSpec.methodBuilder("onOpen").apply {
             addModifiers(PUBLIC)
             addParameter(SupportDbTypeNames.DB, "_db")
-            addStatement("String identityHash = \"\"")
-            addStatement("$T cursor = _db.rawQuery($S, $T.EMPTY_STRING_ARRAY)",
-                    AndroidTypeNames.CURSOR, readIdentityHashQuery(),
-                    RoomTypeNames.STRING_UTIL)
-            beginControlFlow("try").apply {
-                beginControlFlow("if (cursor.moveToFirst())").apply {
-                    addStatement("identityHash = cursor.getString(0)")
-                }
-                endControlFlow()
-            }
-            nextControlFlow("finally").apply {
-                addStatement("cursor.close()")
-            }
-            endControlFlow()
-            beginControlFlow("if(!$S.equals(identityHash))", database.identityHash).apply {
-                addStatement("throw new $T($S)", IllegalStateException::class.typeName(),
-                        "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.")
-            }
-            endControlFlow()
             addStatement("mDatabase = _db")
             addStatement("internalInitInvalidationTracker(_db)")
         }.build()
     }
 
-    private fun MethodSpec.Builder.writeCreateStatements() {
-        addStatement("_db.execSQL($S)", createMasterTableQuery())
-        addStatement("_db.execSQL($S)", setIdentityHashQuery())
-        // this is already called in transaction so no need for a transaction
-        database.entities.forEach {
-            addStatement("_db.execSQL($S)", createQuery(it))
-        }
-        database.entities.forEach {
-            it.createIndexQueries.forEach {
-                addStatement("_db.execSQL($S)", it)
-            }
-        }
-    }
-
-    private fun setIdentityHashQuery(): String {
-        return "INSERT OR REPLACE INTO $MASTER_TABLE_NAME VALUES($MASTER_TABLE_ID," +
-                "\"${database.identityHash}\")"
-    }
-
-    private fun readIdentityHashQuery() : String {
-        return "SELECT $MASTER_TABLE_IDENTITY_HASH_COLUMN FROM $MASTER_TABLE_NAME WHERE" +
-                " $MASTER_TABLE_ID_COLUMN = $MASTER_TABLE_ID LIMIT 1"
-    }
-
-    private fun createMasterTableQuery() : String {
-        return "CREATE TABLE IF NOT EXISTS `$MASTER_TABLE_NAME`(" +
-                "$MASTER_TABLE_ID_COLUMN INTEGER PRIMARY KEY," +
-                "$MASTER_TABLE_IDENTITY_HASH_COLUMN TEXT)"
-    }
-
-    private fun dropMasterTableQuery() : String {
-        return "DROP TABLE IF EXISTS `$MASTER_TABLE_NAME`"
-    }
-
-    private fun createOnCreate() : MethodSpec {
-        return MethodSpec.methodBuilder("onCreate").apply {
+    private fun createCreateAllTables() : MethodSpec {
+        return MethodSpec.methodBuilder("createAllTables").apply {
             addModifiers(PUBLIC)
             addParameter(SupportDbTypeNames.DB, "_db")
-            writeCreateStatements()
+            database.bundle.buildCreateQueries().forEach {
+                addStatement("_db.execSQL($S)", it)
+            }
         }.build()
     }
 
-    private fun createOnUpgrade() : MethodSpec {
-        return MethodSpec.methodBuilder("onUpgrade").apply {
+    private fun createDropAllTables() : MethodSpec {
+        return MethodSpec.methodBuilder("dropAllTables").apply {
             addModifiers(PUBLIC)
             addParameter(SupportDbTypeNames.DB, "_db")
-            addParameter(TypeName.INT, "_oldVersion")
-            addParameter(TypeName.INT, "_newVersion")
             database.entities.forEach {
                 addStatement("_db.execSQL($S)", createDropTableQuery(it))
             }
-            writeCreateStatements()
         }.build()
     }
 
-    private fun createOnDowngrade() : MethodSpec {
-        return MethodSpec.methodBuilder("onDowngrade").apply {
-            addModifiers(PUBLIC)
-            addParameter(SupportDbTypeNames.DB, "_db")
-            addParameter(TypeName.INT, "_oldVersion")
-            addParameter(TypeName.INT, "_newVersion")
-            // TODO better handle this
-            addStatement("onUpgrade(_db, _oldVersion, _newVersion)")
-        }.build()
-    }
-
-
-
     @VisibleForTesting
     fun createQuery(entity : Entity) : String {
         return entity.createTableQuery
diff --git a/room/compiler/src/test/data/databasewriter/input/ComplexDatabase.java b/room/compiler/src/test/data/databasewriter/input/ComplexDatabase.java
index eba60cc..5bb8161 100644
--- a/room/compiler/src/test/data/databasewriter/input/ComplexDatabase.java
+++ b/room/compiler/src/test/data/databasewriter/input/ComplexDatabase.java
@@ -17,7 +17,7 @@
 package foo.bar;
 import com.android.support.room.*;
 import java.util.List;
-@Database(entities = {User.class})
+@Database(entities = {User.class}, version = 1923)
 abstract class ComplexDatabase extends RoomDatabase {
     abstract ComplexDao getComplexDao();
 }
diff --git a/room/compiler/src/test/data/databasewriter/output/ComplexDatabase.java b/room/compiler/src/test/data/databasewriter/output/ComplexDatabase.java
index 5db1335..eb2cc8f 100644
--- a/room/compiler/src/test/data/databasewriter/output/ComplexDatabase.java
+++ b/room/compiler/src/test/data/databasewriter/output/ComplexDatabase.java
@@ -1,58 +1,43 @@
+
 package foo.bar;
 
-import android.database.Cursor;
 import com.android.support.db.SupportSQLiteDatabase;
 import com.android.support.db.SupportSQLiteOpenHelper;
 import com.android.support.db.SupportSQLiteOpenHelper.Callback;
 import com.android.support.db.SupportSQLiteOpenHelper.Configuration;
 import com.android.support.room.DatabaseConfiguration;
 import com.android.support.room.InvalidationTracker;
-import com.android.support.room.util.StringUtil;
-import java.lang.IllegalStateException;
+import com.android.support.room.RoomOpenHelper;
+import com.android.support.room.RoomOpenHelper.Delegate;
 import java.lang.Override;
 
 public class ComplexDatabase_Impl extends ComplexDatabase {
     private volatile ComplexDao _complexDao;
 
     protected SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration configuration) {
-        final SupportSQLiteOpenHelper.Callback _openCallback = new SupportSQLiteOpenHelper.Callback() {
-            public void onCreate(SupportSQLiteDatabase _db) {
-                _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 VALUES(42,\"d4b1d59e1344d0db40fe2cd3fe64d02f\")");
+        final SupportSQLiteOpenHelper.Callback _openCallback = new RoomOpenHelper(configuration, new RoomOpenHelper.Delegate() {
+            public void createAllTables(SupportSQLiteDatabase _db) {
                 _db.execSQL("CREATE TABLE IF NOT EXISTS `User` (`uid` INTEGER, `name` TEXT, `lastName` TEXT, `ageColumn` INTEGER, 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, \"d4b1d59e1344d0db40fe2cd3fe64d02f\")");
             }
 
-            public void onUpgrade(SupportSQLiteDatabase _db, int _oldVersion, int _newVersion) {
+            public void dropAllTables(SupportSQLiteDatabase _db) {
                 _db.execSQL("DROP TABLE IF EXISTS `User`");
-                _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 VALUES(42,\"d4b1d59e1344d0db40fe2cd3fe64d02f\")");
-                _db.execSQL("CREATE TABLE IF NOT EXISTS `User` (`uid` INTEGER, `name` TEXT, `lastName` TEXT, `ageColumn` INTEGER, PRIMARY KEY(`uid`))");
-            }
-
-            public void onDowngrade(SupportSQLiteDatabase _db, int _oldVersion, int _newVersion) {
-                onUpgrade(_db, _oldVersion, _newVersion);
             }
 
             public void onOpen(SupportSQLiteDatabase _db) {
-                String identityHash = "";
-                Cursor cursor = _db.rawQuery("SELECT identity_hash FROM room_master_table WHERE id = 42 LIMIT 1", StringUtil.EMPTY_STRING_ARRAY);
-                try {
-                    if (cursor.moveToFirst()) {
-                        identityHash = cursor.getString(0);
-                    }
-                } finally {
-                    cursor.close();
-                }
-                if(!"d4b1d59e1344d0db40fe2cd3fe64d02f".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.");
-                }
                 mDatabase = _db;
                 internalInitInvalidationTracker(_db);
             }
-        };
+
+            protected boolean validateMigration(SupportSQLiteDatabase _db) {
+                return true;
+            }
+        }, "d4b1d59e1344d0db40fe2cd3fe64d02f");
         final SupportSQLiteOpenHelper.Configuration _sqliteConfig = SupportSQLiteOpenHelper.Configuration.builder(configuration.context)
                 .name(configuration.name)
-                .version(configuration.version)
+                .version(1923)
                 .callback(_openCallback)
                 .build();
         final SupportSQLiteOpenHelper _helper = configuration.sqliteOpenHelperFactory.create(_sqliteConfig);
diff --git a/room/compiler/src/test/kotlin/com/android/support/room/processor/DatabaseProcessorTest.kt b/room/compiler/src/test/kotlin/com/android/support/room/processor/DatabaseProcessorTest.kt
index 5524ef6..5be94da 100644
--- a/room/compiler/src/test/kotlin/com/android/support/room/processor/DatabaseProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/com/android/support/room/processor/DatabaseProcessorTest.kt
@@ -87,7 +87,7 @@
     @Test
     fun simple() {
         singleDb("""
-            @Database(entities = {User.class})
+            @Database(entities = {User.class}, version = 42)
             public abstract class MyDb extends RoomDatabase {
                 abstract UserDao userDao();
             }
@@ -100,7 +100,7 @@
     @Test
     fun multiple() {
         singleDb("""
-            @Database(entities = {User.class, Book.class})
+            @Database(entities = {User.class, Book.class}, version = 42)
             public abstract class MyDb extends RoomDatabase {
                 abstract UserDao userDao();
                 abstract BookDao bookDao();
@@ -117,7 +117,7 @@
     @Test
     fun detectMissingBaseClass() {
         singleDb("""
-            @Database(entities = {User.class, Book.class})
+            @Database(entities = {User.class, Book.class}, version = 42)
             public abstract class MyDb {
             }
             """, USER, BOOK) { db, invocation ->
@@ -128,7 +128,7 @@
     fun detectMissingTable() {
         singleDb(
                 """
-                @Database(entities = {Book.class})
+                @Database(entities = {Book.class}, version = 42)
                 public abstract class MyDb extends RoomDatabase {
                     abstract BookDao bookDao();
                 }
@@ -149,7 +149,7 @@
     @Test
     fun detectDuplicateTableNames() {
         singleDb("""
-                @Database(entities = {User.class, AnotherClass.class})
+                @Database(entities = {User.class, AnotherClass.class}, version = 42)
                 public abstract class MyDb extends RoomDatabase {
                     abstract UserDao userDao();
                 }
@@ -174,7 +174,7 @@
         singleDb(
                 """
                 @SkipQueryVerification
-                @Database(entities = {Book.class})
+                @Database(entities = {Book.class}, version = 42)
                 public abstract class MyDb extends RoomDatabase {
                     abstract BookDao bookDao();
                 }
@@ -197,7 +197,7 @@
         val db1 = JavaFileObjects.forSourceString("foo.bar.Db1",
                 """
                 $DATABASE_PREFIX
-                @Database(entities = {Book.class})
+                @Database(entities = {Book.class}, version = 42)
                 public abstract class Db1 extends RoomDatabase {
                     abstract BookDao bookDao();
                 }
@@ -205,7 +205,7 @@
         val db2 = JavaFileObjects.forSourceString("foo.bar.Db2",
                 """
                 $DATABASE_PREFIX
-                @Database(entities = {Book.class})
+                @Database(entities = {Book.class}, version = 42)
                 public abstract class Db2 extends RoomDatabase {
                     abstract BookDao bookDao();
                 }
@@ -215,7 +215,7 @@
                 package foo.barx;
                 import com.android.support.room.*;
                 import foo.bar.*;
-                @Database(entities = {Book.class})
+                @Database(entities = {Book.class}, version = 42)
                 public abstract class Db1 extends RoomDatabase {
                     abstract BookDao bookDao();
                 }
@@ -245,7 +245,7 @@
     fun twoDaoMethodsForTheSameDao() {
         singleDb(
                 """
-                @Database(entities = {User.class})
+                @Database(entities = {User.class}, version = 42)
                 public abstract class MyDb extends RoomDatabase {
                     abstract UserDao userDao();
                     abstract UserDao userDao2();
@@ -264,7 +264,7 @@
         singleDb(
                 """
                 @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
-                @Database(entities = {User.class})
+                @Database(entities = {User.class}, version = 42)
                 public abstract class MyDb extends RoomDatabase {
                     abstract UserDao userDao();
                 }
@@ -300,7 +300,7 @@
                 }
                 """)
         singleDb("""
-                @Database(entities = {Entity1.class, Entity2.class})
+                @Database(entities = {Entity1.class, Entity2.class}, version = 42)
                 public abstract class MyDb extends RoomDatabase {
                 }
                 """, entity1, entity2){ db, invocation ->
diff --git a/room/compiler/src/test/kotlin/com/android/support/room/solver/CustomTypeConverterResolutionTest.kt b/room/compiler/src/test/kotlin/com/android/support/room/solver/CustomTypeConverterResolutionTest.kt
index 92b8926..f7ecd4c 100644
--- a/room/compiler/src/test/kotlin/com/android/support/room/solver/CustomTypeConverterResolutionTest.kt
+++ b/room/compiler/src/test/kotlin/com/android/support/room/solver/CustomTypeConverterResolutionTest.kt
@@ -238,6 +238,7 @@
             addAnnotation(
                     AnnotationSpec.builder(Database::class.java).apply {
                         addMember("entities", "{$T.class}", ENTITY)
+                        addMember("version", "42")
                     }.build()
             )
         }.build()
diff --git a/room/compiler/src/test/kotlin/com/android/support/room/verifier/DatabaseVerifierTest.kt b/room/compiler/src/test/kotlin/com/android/support/room/verifier/DatabaseVerifierTest.kt
index e134fd4..968f177 100644
--- a/room/compiler/src/test/kotlin/com/android/support/room/verifier/DatabaseVerifierTest.kt
+++ b/room/compiler/src/test/kotlin/com/android/support/room/verifier/DatabaseVerifierTest.kt
@@ -180,7 +180,9 @@
                 element = mock(TypeElement::class.java),
                 type = mock(TypeMirror::class.java),
                 entities = entities.toList(),
-                daoMethods = emptyList())
+                daoMethods = emptyList(),
+                version = -1,
+                exportSchema = false)
     }
 
     private fun entity(tableName: String, vararg fields: Field): Entity {
diff --git a/room/compiler/src/test/kotlin/com/android/support/room/writer/DatabaseWriterTest.kt b/room/compiler/src/test/kotlin/com/android/support/room/writer/DatabaseWriterTest.kt
index 0ee6073..d9f20ff 100644
--- a/room/compiler/src/test/kotlin/com/android/support/room/writer/DatabaseWriterTest.kt
+++ b/room/compiler/src/test/kotlin/com/android/support/room/writer/DatabaseWriterTest.kt
@@ -29,7 +29,6 @@
 
 @RunWith(JUnit4::class)
 class DatabaseWriterTest {
-
     @Test
     fun simpleDb() {
         singleDb(
diff --git a/room/compiler/src/test/kotlin/com/android/support/room/writer/SQLiteOpenHelperWriterTest.kt b/room/compiler/src/test/kotlin/com/android/support/room/writer/SQLiteOpenHelperWriterTest.kt
index 02a8e97..c84b561 100644
--- a/room/compiler/src/test/kotlin/com/android/support/room/writer/SQLiteOpenHelperWriterTest.kt
+++ b/room/compiler/src/test/kotlin/com/android/support/room/writer/SQLiteOpenHelperWriterTest.kt
@@ -44,7 +44,7 @@
         const val DATABASE_CODE = """
             package foo.bar;
             import com.android.support.room.*;
-            @Database(entities = {MyEntity.class})
+            @Database(entities = {MyEntity.class}, version = 3)
             abstract public class MyDatabase extends RoomDatabase {
             }
             """
diff --git a/room/db-impl/src/main/java/com/android/support/db/framework/FrameworkSQLiteDatabase.java b/room/db-impl/src/main/java/com/android/support/db/framework/FrameworkSQLiteDatabase.java
index 36798da..8717cc0 100644
--- a/room/db-impl/src/main/java/com/android/support/db/framework/FrameworkSQLiteDatabase.java
+++ b/room/db-impl/src/main/java/com/android/support/db/framework/FrameworkSQLiteDatabase.java
@@ -35,6 +35,7 @@
 import com.android.support.db.SupportSQLiteQuery;
 import com.android.support.db.SupportSQLiteStatement;
 
+import java.io.IOException;
 import java.util.List;
 import java.util.Locale;
 
@@ -351,4 +352,9 @@
     public boolean isDatabaseIntegrityOk() {
         return mDelegate.isDatabaseIntegrityOk();
     }
+
+    @Override
+    public void close() throws IOException {
+        mDelegate.close();
+    }
 }
diff --git a/room/db/src/main/java/com/android/support/db/SupportSQLiteDatabase.java b/room/db/src/main/java/com/android/support/db/SupportSQLiteDatabase.java
index 9909b8f..4d23e21 100644
--- a/room/db/src/main/java/com/android/support/db/SupportSQLiteDatabase.java
+++ b/room/db/src/main/java/com/android/support/db/SupportSQLiteDatabase.java
@@ -29,6 +29,7 @@
 import android.support.annotation.RequiresApi;
 import android.util.Pair;
 
+import java.io.Closeable;
 import java.util.List;
 import java.util.Locale;
 
@@ -37,7 +38,7 @@
  * sql versions. It mimics the behavior of {@link android.database.sqlite.SQLiteDatabase}
  */
 @SuppressWarnings("unused")
-public interface SupportSQLiteDatabase {
+public interface SupportSQLiteDatabase extends Closeable {
     // TODO override all methods
     /**
      * Compiles the given SQL statement.
diff --git a/room/integration-tests/testapp/build.gradle b/room/integration-tests/testapp/build.gradle
index af70ff6..81706d4 100644
--- a/room/integration-tests/testapp/build.gradle
+++ b/room/integration-tests/testapp/build.gradle
@@ -28,7 +28,11 @@
         versionName "1.0"
 
         testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
-
+        javaCompileOptions {
+            annotationProcessorOptions {
+                arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
+            }
+        }
     }
     testOptions {
         unitTests.returnDefaultValues = true
@@ -37,6 +41,9 @@
         sourceCompatibility JavaVersion.VERSION_1_7
         targetCompatibility JavaVersion.VERSION_1_7
     }
+    sourceSets {
+        androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
+    }
 }
 
 dependencies {
@@ -60,6 +67,7 @@
     androidTestCompile project(':lifecycle:extensions')
     androidTestCompile project(':lifecycle:common')
     androidTestCompile project(':lifecycle:runtime')
+    androidTestCompile project(':room:testing')
     testCompile libs.junit
     testCompile libs.mockito_core
 
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
new file mode 100644
index 0000000..ca4d63a
--- /dev/null
+++ b/room/integration-tests/testapp/schemas/com.android.support.room.integration.testapp.migration.MigrationDb/1.json
@@ -0,0 +1,36 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 1,
+    "identityHash": "e496317e42c1a8a681df3f4f8012562e",
+    "entities": [
+      {
+        "tableName": "Vo1",
+        "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, \"e496317e42c1a8a681df3f4f8012562e\")"
+    ]
+  }
+}
\ 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
new file mode 100644
index 0000000..70e4b5c
--- /dev/null
+++ b/room/integration-tests/testapp/schemas/com.android.support.room.integration.testapp.migration.MigrationDb/2.json
@@ -0,0 +1,59 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 2,
+    "identityHash": "18b84433116f1cfd478e0a9fe25c9121",
+    "entities": [
+      {
+        "tableName": "Vo1",
+        "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": "Vo2",
+        "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, \"18b84433116f1cfd478e0a9fe25c9121\")"
+    ]
+  }
+}
\ No newline at end of file
diff --git a/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/PKeyTestDatabase.java b/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/PKeyTestDatabase.java
index 5dc2e43..9031eaf 100644
--- a/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/PKeyTestDatabase.java
+++ b/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/PKeyTestDatabase.java
@@ -26,7 +26,8 @@
 
 import java.util.List;
 
-@Database(entities = {IntAutoIncPKeyEntity.class, IntegerAutoIncPKeyEntity.class})
+@Database(entities = {IntAutoIncPKeyEntity.class, IntegerAutoIncPKeyEntity.class}, version = 1,
+        exportSchema = false)
 public abstract class PKeyTestDatabase extends RoomDatabase {
     public abstract IntPKeyDao intPKeyDao();
     public abstract IntegerPKeyDao integerPKeyDao();
diff --git a/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/TestDatabase.java b/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/TestDatabase.java
index fdb289d..29b97ce 100644
--- a/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/TestDatabase.java
+++ b/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/TestDatabase.java
@@ -34,7 +34,8 @@
 
 import java.util.Date;
 
-@Database(entities = {User.class, Pet.class, School.class, PetCouple.class, Toy.class})
+@Database(entities = {User.class, Pet.class, School.class, PetCouple.class, Toy.class},
+        version = 1, exportSchema = false)
 @TypeConverters(TestDatabase.Converters.class)
 public abstract class TestDatabase extends RoomDatabase {
     public abstract UserDao getUserDao();
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
new file mode 100644
index 0000000..64a37aa
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/migration/MigrationDb.java
@@ -0,0 +1,82 @@
+/*
+ * 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.integration.testapp.migration;
+
+import android.content.ContentValues;
+
+import com.android.support.db.SupportSQLiteDatabase;
+import com.android.support.room.Dao;
+import com.android.support.room.Database;
+import com.android.support.room.Entity;
+import com.android.support.room.Insert;
+import com.android.support.room.PrimaryKey;
+import com.android.support.room.Query;
+import com.android.support.room.RoomDatabase;
+
+import java.util.List;
+
+@SuppressWarnings("WeakerAccess")
+@Database(version = MigrationDb.LATEST_VERSION,
+        entities = {MigrationDb.Vo1.class, MigrationDb.Vo2.class})
+public abstract class MigrationDb extends RoomDatabase {
+    static final int LATEST_VERSION = 2;
+    abstract MigrationDao dao();
+    @Entity
+    static class Vo1 {
+        @PrimaryKey
+        public int id;
+        public String name;
+    }
+
+    @Entity
+    static class Vo2 {
+        @PrimaryKey
+        public int id;
+        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();
+        @Insert
+        void insert(Vo2... vo2);
+    }
+
+    /**
+     * not a real dao because database will change.
+     */
+    static class Dao_V1 {
+        final SupportSQLiteDatabase mDb;
+
+        Dao_V1(SupportSQLiteDatabase db) {
+            mDb = db;
+        }
+
+        public void insertIntoVo1(int id, String name) {
+            ContentValues values = new ContentValues();
+            values.put("id", id);
+            values.put("name", name);
+            long insertionId = mDb.insert("Vo1", 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
new file mode 100644
index 0000000..a5bf4b9
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/migration/MigrationTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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.integration.testapp.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.framework.FrameworkSQLiteOpenHelperFactory;
+import com.android.support.room.Room;
+import com.android.support.room.migration.Migration;
+import com.android.support.room.testing.MigrationTestHelper;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Test custom database migrations.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class MigrationTest {
+    @Rule
+    public MigrationTestHelper helper;
+
+    public MigrationTest() {
+        helper = new MigrationTestHelper(InstrumentationRegistry.getContext(),
+                MigrationDb.class.getCanonicalName(), new FrameworkSQLiteOpenHelperFactory());
+    }
+
+    @Test
+    public void startInCurrentVersion() throws IOException {
+        SupportSQLiteDatabase db = helper.createDatabase("migration-test",
+                MigrationDb.LATEST_VERSION);
+        final MigrationDb.Dao_V1 dao = new MigrationDb.Dao_V1(db);
+        dao.insertIntoVo1(2, "x");
+        db.close();
+        MigrationDb migrationDb = Room.databaseBuilder(InstrumentationRegistry.getContext(),
+                MigrationDb.class, "migration-test").build();
+        List<MigrationDb.Vo1> items = migrationDb.dao().loadAllVo1s();
+        assertThat(items.size(), is(1));
+    }
+
+    @Test
+    public void addTable() throws IOException {
+        SupportSQLiteDatabase db = helper.createDatabase("migration-test", 1);
+        final MigrationDb.Dao_V1 dao = new MigrationDb.Dao_V1(db);
+        dao.insertIntoVo1(2, "foo");
+        dao.insertIntoVo1(3, "bar");
+        db.close();
+
+        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";
+        // assert no error happens
+        migrationDb.dao().insert(vo2);
+        List<MigrationDb.Vo1> vo2s = migrationDb.dao().loadAllVo2s();
+        assertThat(vo2s.size(), is(1));
+    }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/test/IndexingTest.java b/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/test/IndexingTest.java
index cbef422..3f9e068 100644
--- a/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/test/IndexingTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/test/IndexingTest.java
@@ -71,7 +71,7 @@
         List<IndexInfo> loadIndices();
     }
 
-    @Database(entities = {Entity1.class})
+    @Database(entities = {Entity1.class}, version = 1, exportSchema = false)
     abstract static class IndexingDb extends RoomDatabase {
         abstract SqlMasterDao sqlMasterDao();
     }
diff --git a/room/migration/build.gradle b/room/migration/build.gradle
new file mode 100644
index 0000000..7e3794b
--- /dev/null
+++ b/room/migration/build.gradle
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+
+apply plugin: 'maven'
+apply plugin: 'java'
+
+sourceCompatibility = 1.7
+
+sourceSets {
+    test.java.srcDirs += 'src/tests/kotlin'
+}
+project.ext.noDocs = true
+dependencies {
+    compile project(":room:common")
+    compile libs.kotlin.stdlib
+    compile libs.gson
+    testCompile libs.junit
+    testCompile libs.ij_annotations
+    testCompile libs.mockito_core
+}
+
+archivesBaseName = "migration"
diff --git a/room/migration/src/main/java/com/android/support/room/migration/bundle/BundleUtil.java b/room/migration/src/main/java/com/android/support/room/migration/bundle/BundleUtil.java
new file mode 100644
index 0000000..567fea8
--- /dev/null
+++ b/room/migration/src/main/java/com/android/support/room/migration/bundle/BundleUtil.java
@@ -0,0 +1,36 @@
+/*
+ * 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.bundle;
+
+import android.support.annotation.RestrictTo;
+
+/**
+ * Utility functions for bundling.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class BundleUtil {
+    /**
+     * Placeholder for table names in queries.
+     */
+    public static final String TABLE_NAME_PLACEHOLDER = "${TABLE_NAME}";
+
+    static String replaceTableName(String contents, String tableName) {
+        return contents.replace(TABLE_NAME_PLACEHOLDER, tableName);
+    }
+}
diff --git a/room/migration/src/main/java/com/android/support/room/migration/bundle/DatabaseBundle.java b/room/migration/src/main/java/com/android/support/room/migration/bundle/DatabaseBundle.java
new file mode 100644
index 0000000..9da99c8
--- /dev/null
+++ b/room/migration/src/main/java/com/android/support/room/migration/bundle/DatabaseBundle.java
@@ -0,0 +1,107 @@
+/*
+ * 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.bundle;
+
+import android.support.annotation.RestrictTo;
+
+import com.google.gson.annotations.SerializedName;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Data class that holds the schema information for a
+ * {@link com.android.support.room.Database Database}.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class DatabaseBundle {
+    @SerializedName("version")
+    private int mVersion;
+    @SerializedName("identityHash")
+    private String mIdentityHash;
+    @SerializedName("entities")
+    private List<EntityBundle> mEntities;
+    // then entity where we keep room information
+    @SerializedName("setupQueries")
+    private List<String> mSetupQueries;
+    private transient Map<String, EntityBundle> mEntitiesByTableName;
+
+    /**
+     * Creates a new database
+     * @param version Version
+     * @param identityHash Identity hash
+     * @param entities List of entities
+     */
+    public DatabaseBundle(int version, String identityHash, List<EntityBundle> entities,
+            List<String> setupQueries) {
+        mVersion = version;
+        mIdentityHash = identityHash;
+        mEntities = entities;
+        mSetupQueries = setupQueries;
+    }
+
+    /**
+     * @return The identity has of the Database.
+     */
+    public String getIdentityHash() {
+        return mIdentityHash;
+    }
+
+    /**
+     * @return The database version.
+     */
+    public int getVersion() {
+        return mVersion;
+    }
+
+    /**
+     * @return List of entities.
+     */
+    public List<EntityBundle> getEntities() {
+        return mEntities;
+    }
+
+    /**
+     * @return Map of entities, keyed by table name.
+     */
+    @SuppressWarnings("unused")
+    public Map<String, EntityBundle> getEntitiesByTableName() {
+        if (mEntitiesByTableName == null) {
+            mEntitiesByTableName = new HashMap<>();
+            for (EntityBundle bundle : mEntities) {
+                mEntitiesByTableName.put(bundle.getTableName(), bundle);
+            }
+        }
+        return mEntitiesByTableName;
+    }
+
+    /**
+     * @return List of SQL queries to build this database from scratch.
+     */
+    public List<String> buildCreateQueries() {
+        List<String> result = new ArrayList<>();
+        for (EntityBundle entityBundle : mEntities) {
+            result.addAll(entityBundle.buildCreateQueries());
+        }
+        result.addAll(mSetupQueries);
+        return result;
+    }
+}
diff --git a/room/migration/src/main/java/com/android/support/room/migration/bundle/EntityBundle.java b/room/migration/src/main/java/com/android/support/room/migration/bundle/EntityBundle.java
new file mode 100644
index 0000000..d32f9a8
--- /dev/null
+++ b/room/migration/src/main/java/com/android/support/room/migration/bundle/EntityBundle.java
@@ -0,0 +1,167 @@
+/*
+ * 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.bundle;
+
+import android.support.annotation.RestrictTo;
+
+import com.google.gson.annotations.SerializedName;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Data class that holds the schema information about an
+ * {@link com.android.support.room.Entity Entity}.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class EntityBundle {
+
+    static final String NEW_TABLE_PREFIX = "_new_";
+
+    @SerializedName("tableName")
+    private String mTableName;
+    @SerializedName("createSql")
+    private String mCreateSql;
+    @SerializedName("fields")
+    private List<FieldBundle> mFields;
+    @SerializedName("primaryKey")
+    private PrimaryKeyBundle mPrimaryKey;
+    @SerializedName("indices")
+    private List<IndexBundle> mIndices;
+
+    private transient String mNewTableName;
+    private transient Map<String, FieldBundle> mFieldsByColumnName;
+
+    /**
+     * Creates a new bundle.
+     *
+     * @param tableName The table name.
+     * @param createSql Create query with the table name placeholder.
+     * @param fields The list of fields.
+     * @param primaryKey The primary key.
+     * @param indices The list of indices.
+     */
+    public EntityBundle(String tableName, String createSql,
+            List<FieldBundle> fields,
+            PrimaryKeyBundle primaryKey,
+            List<IndexBundle> indices) {
+        mTableName = tableName;
+        mCreateSql = createSql;
+        mFields = fields;
+        mPrimaryKey = primaryKey;
+        mIndices = indices;
+    }
+
+    /**
+     * @return The table name if it is created during a table schema modification.
+     */
+    public String getNewTableName() {
+        if (mNewTableName == null) {
+            mNewTableName = NEW_TABLE_PREFIX + mTableName;
+        }
+        return mNewTableName;
+    }
+
+    /**
+     * @return Map of fields keyed by their column names.
+     */
+    public Map<String, FieldBundle> getFieldsByColumnName() {
+        if (mFieldsByColumnName == null) {
+            mFieldsByColumnName = new HashMap<>();
+            for (FieldBundle bundle : mFields) {
+                mFieldsByColumnName.put(bundle.getColumnName(), bundle);
+            }
+        }
+        return mFieldsByColumnName;
+    }
+
+    /**
+     * @return The table name.
+     */
+    public String getTableName() {
+        return mTableName;
+    }
+
+    /**
+     * @return The create query with table name placeholder.
+     */
+    public String getCreateSql() {
+        return mCreateSql;
+    }
+
+    /**
+     * @return List of fields.
+     */
+    public List<FieldBundle> getFields() {
+        return mFields;
+    }
+
+    /**
+     * @return The primary key description.
+     */
+    public PrimaryKeyBundle getPrimaryKey() {
+        return mPrimaryKey;
+    }
+
+    /**
+     * @return List of indices.
+     */
+    public List<IndexBundle> getIndices() {
+        return mIndices;
+    }
+
+    /**
+     * @return Create table SQL query that uses the actual table name.
+     */
+    public String createTable() {
+        return BundleUtil.replaceTableName(mCreateSql, getTableName());
+    }
+
+    /**
+     * @return Create table SQL query that uses the table name with "new" prefix.
+     */
+    public String createNewTable() {
+        return BundleUtil.replaceTableName(mCreateSql, getNewTableName());
+    }
+
+    /**
+     * @return Renames the table with {@link #getNewTableName()} to {@link #getTableName()}.
+     */
+    @NotNull
+    public String renameToOriginal() {
+        return "ALTER TABLE " + getNewTableName() + " RENAME TO " + getTableName();
+    }
+
+    /**
+     * @return Creates the list of SQL queries that are necessary to create this entitiy.
+     */
+    public Collection<String> buildCreateQueries() {
+        List<String> result = new ArrayList<>();
+        result.add(createTable());
+        for (IndexBundle indexBundle : mIndices) {
+            result.add(indexBundle.create(getTableName()));
+        }
+        return result;
+    }
+}
diff --git a/room/migration/src/main/java/com/android/support/room/migration/bundle/FieldBundle.java b/room/migration/src/main/java/com/android/support/room/migration/bundle/FieldBundle.java
new file mode 100644
index 0000000..e55e3d9
--- /dev/null
+++ b/room/migration/src/main/java/com/android/support/room/migration/bundle/FieldBundle.java
@@ -0,0 +1,55 @@
+/*
+ * 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.bundle;
+
+import android.support.annotation.RestrictTo;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Data class that holds the schema information for an
+ * {@link com.android.support.room.Entity Entity} field.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class FieldBundle {
+    @SerializedName("fieldPath")
+    private String mFieldPath;
+    @SerializedName("columnName")
+    private String mColumnName;
+    @SerializedName("affinity")
+    private String mAffinity;
+
+    public FieldBundle(String fieldPath, String columnName, String affinity) {
+        mFieldPath = fieldPath;
+        mColumnName = columnName;
+        mAffinity = affinity;
+    }
+
+    public String getFieldPath() {
+        return mFieldPath;
+    }
+
+    public String getColumnName() {
+        return mColumnName;
+    }
+
+    public String getAffinity() {
+        return mAffinity;
+    }
+}
diff --git a/room/migration/src/main/java/com/android/support/room/migration/bundle/IndexBundle.java b/room/migration/src/main/java/com/android/support/room/migration/bundle/IndexBundle.java
new file mode 100644
index 0000000..a472e42
--- /dev/null
+++ b/room/migration/src/main/java/com/android/support/room/migration/bundle/IndexBundle.java
@@ -0,0 +1,68 @@
+/*
+ * 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.bundle;
+
+import android.support.annotation.RestrictTo;
+
+import com.google.gson.annotations.SerializedName;
+
+import java.util.List;
+
+/**
+ * Data class that holds the schema information about a table Index.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class IndexBundle {
+    @SerializedName("name")
+    private String mName;
+    @SerializedName("unique")
+    private boolean mUnique;
+    @SerializedName("columnNames")
+    private List<String> mColumnNames;
+    @SerializedName("createSql")
+    private String mCreateSql;
+
+    public IndexBundle(String name, boolean unique, List<String> columnNames,
+            String createSql) {
+        mName = name;
+        mUnique = unique;
+        mColumnNames = columnNames;
+        mCreateSql = createSql;
+    }
+
+    public String getName() {
+        return mName;
+    }
+
+    public boolean isUnique() {
+        return mUnique;
+    }
+
+    public List<String> getColumnNames() {
+        return mColumnNames;
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public String create(String tableName) {
+        return BundleUtil.replaceTableName(mCreateSql, tableName);
+    }
+}
diff --git a/room/migration/src/main/java/com/android/support/room/migration/bundle/PrimaryKeyBundle.java b/room/migration/src/main/java/com/android/support/room/migration/bundle/PrimaryKeyBundle.java
new file mode 100644
index 0000000..37bfe9e
--- /dev/null
+++ b/room/migration/src/main/java/com/android/support/room/migration/bundle/PrimaryKeyBundle.java
@@ -0,0 +1,49 @@
+/*
+ * 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.bundle;
+
+import android.support.annotation.RestrictTo;
+
+import com.google.gson.annotations.SerializedName;
+
+import java.util.List;
+
+/**
+ * Data class that holds the schema information about a primary key.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class PrimaryKeyBundle {
+    @SerializedName("columnNames")
+    private List<String> mColumnNames;
+    @SerializedName("autoGenerate")
+    private boolean mAutoGenerate;
+
+    public PrimaryKeyBundle(boolean autoGenerate, List<String> columnNames) {
+        mColumnNames = columnNames;
+        mAutoGenerate = autoGenerate;
+    }
+
+    public List<String> getColumnNames() {
+        return mColumnNames;
+    }
+
+    public boolean isAutoGenerate() {
+        return mAutoGenerate;
+    }
+}
diff --git a/room/migration/src/main/java/com/android/support/room/migration/bundle/SchemaBundle.java b/room/migration/src/main/java/com/android/support/room/migration/bundle/SchemaBundle.java
new file mode 100644
index 0000000..04acd70
--- /dev/null
+++ b/room/migration/src/main/java/com/android/support/room/migration/bundle/SchemaBundle.java
@@ -0,0 +1,107 @@
+/*
+ * 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.bundle;
+
+import android.support.annotation.RestrictTo;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.annotations.SerializedName;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * Data class that holds the information about a database schema export.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class SchemaBundle {
+
+    @SerializedName("formatVersion")
+    private int mFormatVersion;
+    @SerializedName("database")
+    private DatabaseBundle mDatabase;
+
+    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();
+    }
+
+    public SchemaBundle(int formatVersion, DatabaseBundle database) {
+        mFormatVersion = formatVersion;
+        mDatabase = database;
+    }
+
+    @SuppressWarnings("unused")
+    public int getFormatVersion() {
+        return mFormatVersion;
+    }
+
+    public DatabaseBundle getDatabase() {
+        return mDatabase;
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public static SchemaBundle deserialize(InputStream fis)
+            throws UnsupportedEncodingException {
+        InputStreamReader is = new InputStreamReader(fis, CHARSET);
+        try {
+            return GSON.fromJson(is, SchemaBundle.class);
+        } finally {
+            safeClose(is);
+            safeClose(fis);
+        }
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public static void serialize(SchemaBundle bundle, File file) throws IOException {
+        FileOutputStream fos = new FileOutputStream(file, false);
+        OutputStreamWriter osw = new OutputStreamWriter(fos, CHARSET);
+        try {
+            GSON.toJson(bundle, osw);
+        } finally {
+            safeClose(osw);
+            safeClose(fos);
+        }
+    }
+
+    private static void safeClose(Closeable closeable) {
+        if (closeable != null) {
+            try {
+                closeable.close();
+            } catch (Throwable ignored) {
+            }
+        }
+    }
+
+}
diff --git a/room/runtime/build.gradle b/room/runtime/build.gradle
index 13882fc..566720f 100644
--- a/room/runtime/build.gradle
+++ b/room/runtime/build.gradle
@@ -53,9 +53,19 @@
     compile project(":apptoolkit:core")
     compile libs.support.core_utils
 
+
     testCompile project(":apptoolkit:core-testing")
     testCompile libs.junit
     testCompile libs.mockito.all
+    testCompile libs.support.annotations
+
+    androidTestCompile libs.junit
+    androidTestCompile(libs.test_runner) {
+        exclude module: 'support-annotations'
+    }
+    androidTestCompile(libs.espresso_core, {
+        exclude group: 'com.android.support', module: 'support-annotations'
+    })
 }
 
 archivesBaseName = "runtime"
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 32ec9b0..6a8d226 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
@@ -42,16 +42,19 @@
      */
     @Nullable
     public final String name;
-    /**
-     * The version of the database.
-     */
-    public final int version;
 
-    DatabaseConfiguration(@NonNull Context context, @Nullable String name, int version,
-                                  SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory) {
+    /**
+     * Collection of available migrations.
+     */
+    @NonNull
+    public final RoomDatabase.MigrationContainer migrationContainer;
+
+    DatabaseConfiguration(@NonNull Context context, @Nullable String name,
+            @NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory,
+            @NonNull RoomDatabase.MigrationContainer migrationContainer) {
         this.sqliteOpenHelperFactory = sqliteOpenHelperFactory;
         this.context = context;
         this.name = name;
-        this.version = version;
+        this.migrationContainer = migrationContainer;
     }
 }
diff --git a/room/runtime/src/main/java/com/android/support/room/Room.java b/room/runtime/src/main/java/com/android/support/room/Room.java
index 922a4a5..bbb3075 100644
--- a/room/runtime/src/main/java/com/android/support/room/Room.java
+++ b/room/runtime/src/main/java/com/android/support/room/Room.java
@@ -28,7 +28,7 @@
     /**
      * The master table where room keeps its metadata information.
      */
-    public static final String MASTER_TABLE_NAME = "room_master_table";
+    public static final String MASTER_TABLE_NAME = RoomMasterTable.TABLE_NAME;
     private static final String CURSOR_CONV_SUFFIX = "_CursorConverter";
 
     /**
diff --git a/room/runtime/src/main/java/com/android/support/room/RoomDatabase.java b/room/runtime/src/main/java/com/android/support/room/RoomDatabase.java
index e31406d..76e5b78 100644
--- a/room/runtime/src/main/java/com/android/support/room/RoomDatabase.java
+++ b/room/runtime/src/main/java/com/android/support/room/RoomDatabase.java
@@ -21,12 +21,19 @@
 import android.support.annotation.CallSuper;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
+import android.support.v4.util.SparseArrayCompat;
+import android.util.Log;
 
 import com.android.support.db.SupportSQLiteDatabase;
 import com.android.support.db.SupportSQLiteOpenHelper;
 import com.android.support.db.SupportSQLiteQuery;
 import com.android.support.db.SupportSQLiteStatement;
 import com.android.support.db.framework.FrameworkSQLiteOpenHelperFactory;
+import com.android.support.room.migration.Migration;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 
 /**
  * Base class for all Room databases. All classes that are annotated with {@link Database} must
@@ -108,7 +115,7 @@
 
     // Below, there are wrapper methods for SupportSQLiteDatabase. This helps us track which
     // methods we are using and also helps unit tests to mock this class without mocking
-    // all sqlite database methods.
+    // all SQLite database methods.
 
     /**
      * Wrapper for {@link SupportSQLiteDatabase#rawQuery(String, String[])}.
@@ -124,7 +131,7 @@
     /**
      * Wrapper for {@link SupportSQLiteDatabase#rawQuery(SupportSQLiteQuery)}.
      *
-     * @param query The Query which includes the SQL and a bind callback for bind argumetns.
+     * @param query The Query which includes the SQL and a bind callback for bind arguments.
      *
      * @return Result of the query.
      */
@@ -211,13 +218,17 @@
         private final Context mContext;
 
         private SupportSQLiteOpenHelper.Factory mFactory;
-        private int mVersion = 1;
         private boolean mInMemory;
+        /**
+         * Migrations, mapped by from-to pairs.
+         */
+        private MigrationContainer mMigrationContainer;
 
         Builder(@NonNull Context context, @NonNull Class<T> klass, @Nullable String name) {
             mContext = context;
             mDatabaseClass = klass;
             mName = name;
+            mMigrationContainer = new MigrationContainer();
         }
 
         /**
@@ -233,13 +244,27 @@
         }
 
         /**
-         * Version of the database, defaults to 1.
+         * Adds a migration to the builder.
+         * <p>
+         * Each Migration has a start and end versions and Room runs these migrations to bring the
+         * database to the latest version.
+         * <p>
+         * If a migration item is missing between current version and the latest version, Room
+         * will clear the database and recreate so even if you have no changes between 2 versions,
+         * you should still provide a Migration object to the builder.
+         * <p>
+         * A migration can handle more than 1 version (e.g. if you have a faster path to choose when
+         * going version 3 to 5 without going to version 4). If Room opens a database at version
+         * 3 and latest version is &gt;= 5, Room will use the migration object that can migrate from
+         * 3 to 5 instead of 3 to 4 and 4 to 5.
          *
-         * @param version The database version to use
+         * @param migrations The migration object that can modify the database and to the necessary
+         *                   changes.
+         *
          * @return this
          */
-        public Builder<T> version(int version) {
-            mVersion = version;
+        public Builder<T> addMigrations(Migration... migrations) {
+            mMigrationContainer.addMigrations(migrations);
             return this;
         }
 
@@ -264,10 +289,102 @@
                 mFactory = new FrameworkSQLiteOpenHelperFactory();
             }
             DatabaseConfiguration configuration =
-                    new DatabaseConfiguration(mContext, mName, mVersion, mFactory);
+                    new DatabaseConfiguration(mContext, mName, mFactory, mMigrationContainer);
             T db = Room.getGeneratedImplementation(mDatabaseClass, DB_IMPL_SUFFIX);
             db.init(configuration);
             return db;
         }
     }
+
+    /**
+     * A container to hold migrations. It also allows querying its contents to find migrations
+     * between two versions.
+     */
+    public static class MigrationContainer {
+        private SparseArrayCompat<SparseArrayCompat<Migration>> mMigrations =
+                new SparseArrayCompat<>();
+
+        /**
+         * Adds the given migrations to the list of available migrations. If 2 migrations have the
+         * same start-end versions, the latter migration overrides the previous one.
+         *
+         * @param migrations List of available migrations.
+         */
+        public void addMigrations(Migration... migrations) {
+            for (Migration migration : migrations) {
+                addMigration(migration);
+            }
+        }
+
+        private void addMigration(Migration migration) {
+            final int start = migration.startVersion;
+            final int end = migration.endVersion;
+            SparseArrayCompat<Migration> targetMap = mMigrations.get(start);
+            if (targetMap == null) {
+                targetMap = new SparseArrayCompat<>();
+                mMigrations.put(start, targetMap);
+            }
+            Migration existing = targetMap.get(end);
+            if (existing != null) {
+                Log.w(Room.LOG_TAG, "Overriding migration " + existing + " with " + migration);
+            }
+            targetMap.append(end, migration);
+        }
+
+        /**
+         * Finds the list of migrations that should be run to move from {@code start} version to
+         * {@code end} version.
+         *
+         * @param start The current database version
+         * @param end The target database version
+         * @return An ordered list of {@link Migration} objects that should be run to migrate
+         * between the given versions. If a migration path cannot be found, returns {@code null}.
+         */
+        @Nullable
+        public List<Migration> findMigrationPath(int start, int end) {
+            if (start == end) {
+                return Collections.emptyList();
+            }
+            boolean migrateUp = end > start;
+            List<Migration> result = new ArrayList<>();
+            return findUpMigrationPath(result, migrateUp, start, end);
+        }
+
+        private List<Migration> findUpMigrationPath(List<Migration> result, boolean upgrade,
+                int start, int end) {
+            final int searchDirection = upgrade ? -1 : 1;
+            while (upgrade ? start < end : start > end) {
+                SparseArrayCompat<Migration> targetNodes = mMigrations.get(start);
+                if (targetNodes == null) {
+                    return null;
+                }
+                // keys are ordered so we can start searching from one end of them.
+                final int size = targetNodes.size();
+                final int firstIndex;
+                final int lastIndex;
+
+                if (upgrade) {
+                    firstIndex = size - 1;
+                    lastIndex = -1;
+                } else {
+                    firstIndex = 0;
+                    lastIndex = size;
+                }
+                boolean found = false;
+                for (int i = firstIndex; i != lastIndex; i += searchDirection) {
+                    int targetVersion = targetNodes.keyAt(i);
+                    if (targetVersion <= end && targetVersion > start) {
+                        result.add(targetNodes.valueAt(i));
+                        start = targetVersion;
+                        found = true;
+                        break;
+                    }
+                }
+                if (!found) {
+                    return null;
+                }
+            }
+            return result;
+        }
+    }
 }
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
new file mode 100644
index 0000000..e03685d
--- /dev/null
+++ b/room/runtime/src/main/java/com/android/support/room/RoomOpenHelper.java
@@ -0,0 +1,150 @@
+/*
+ * 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;
+
+import android.database.Cursor;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+
+import com.android.support.db.SupportSQLiteDatabase;
+import com.android.support.db.SupportSQLiteOpenHelper;
+import com.android.support.room.migration.Migration;
+import com.android.support.room.util.StringUtil;
+
+import java.util.List;
+
+/**
+ * An open helper that holds a reference to the configuration until the database is opened.
+ *
+ * @hide
+ */
+@SuppressWarnings("unused")
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class RoomOpenHelper extends SupportSQLiteOpenHelper.Callback {
+    @Nullable
+    private DatabaseConfiguration mConfiguration;
+    @NonNull
+    private final Delegate mDelegate;
+    @NonNull
+    private final String mIdentityHash;
+
+    public RoomOpenHelper(@NonNull DatabaseConfiguration configuration, @NonNull Delegate delegate,
+            @NonNull String identityHash) {
+        mConfiguration = configuration;
+        mDelegate = delegate;
+        mIdentityHash = identityHash;
+    }
+
+    @Override
+    public void onConfigure(SupportSQLiteDatabase db) {
+        super.onConfigure(db);
+    }
+
+    @Override
+    public void onCreate(SupportSQLiteDatabase db) {
+        updateIdentity(db);
+        mDelegate.createAllTables(db);
+    }
+
+    @Override
+    public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
+        boolean migrated = false;
+        if (mConfiguration != null) {
+            List<Migration> migrations = mConfiguration.migrationContainer.findMigrationPath(
+                    oldVersion, newVersion);
+            if (migrations != null) {
+                for (Migration migration : migrations) {
+                    migration.migrate(db);
+                }
+                if (mDelegate.validateMigration(db)) {
+                    updateIdentity(db);
+                } else {
+                    // TODO
+                    throw new RuntimeException("cannot validate the migration result");
+                }
+                migrated = true;
+            }
+        }
+        if (!migrated) {
+            mDelegate.dropAllTables(db);
+            mDelegate.createAllTables(db);
+        }
+    }
+
+    @Override
+    public void onDowngrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
+        onUpgrade(db, oldVersion, newVersion);
+    }
+
+    @Override
+    public void onOpen(SupportSQLiteDatabase db) {
+        super.onOpen(db);
+        checkIdentity(db);
+        mDelegate.onOpen(db);
+        // there might be too many configurations etc, just clear it.
+        mConfiguration = null;
+    }
+
+    private void checkIdentity(SupportSQLiteDatabase db) {
+        createMasterTableIfNotExists(db);
+        String identityHash = "";
+        Cursor cursor = db.rawQuery(RoomMasterTable.READ_QUERY, StringUtil.EMPTY_STRING_ARRAY);
+        //noinspection TryFinallyCanBeTryWithResources
+        try {
+            if (cursor.moveToFirst()) {
+                identityHash = cursor.getString(0);
+            }
+        } finally {
+            cursor.close();
+        }
+        if (!mIdentityHash.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.");
+        }
+    }
+
+    private void updateIdentity(SupportSQLiteDatabase db) {
+        createMasterTableIfNotExists(db);
+        db.execSQL(RoomMasterTable.createInsertQuery(mIdentityHash));
+    }
+
+    private void createMasterTableIfNotExists(SupportSQLiteDatabase db) {
+        db.execSQL(RoomMasterTable.CREATE_QUERY);
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public abstract static class Delegate {
+        protected abstract void dropAllTables(SupportSQLiteDatabase database);
+
+        protected abstract void createAllTables(SupportSQLiteDatabase database);
+
+        protected abstract void onOpen(SupportSQLiteDatabase database);
+
+        /**
+         * Called after a migration run to validate database integrity.
+         *
+         * @param db The SQLite database.
+         */
+        protected abstract boolean 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
new file mode 100644
index 0000000..462e4a1
--- /dev/null
+++ b/room/runtime/src/main/java/com/android/support/room/migration/Migration.java
@@ -0,0 +1,53 @@
+/*
+ * 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 com.android.support.db.SupportSQLiteDatabase;
+
+/**
+ * Base class for a database migration.
+ * <p>
+ * Each migration can move between 2 versions that are defined by {@link #startVersion} and
+ * {@link #endVersion}.
+ * <p>
+ * Usually, you would need 1 Migration class for each version change but you can also provide
+ * Migrations that can handle multiple version changes.
+ */
+public abstract class Migration {
+    public final int startVersion;
+    public final int endVersion;
+
+    /**
+     * Creates a new migration between {@code startVersion} and {@code endVersion}.
+     *
+     * @param startVersion The start version of the database.
+     * @param endVersion The end version of the database after this migration is applied.
+     */
+    public Migration(int startVersion, int endVersion) {
+        this.startVersion = startVersion;
+        this.endVersion = endVersion;
+    }
+
+    /**
+     * Should run the necessary migrations.
+     * <p>
+     * This class cannot access any generated Dao in this method.
+     *
+     * @param database The database instance
+     */
+    public abstract void migrate(SupportSQLiteDatabase database);
+}
diff --git a/room/runtime/src/test/java/com/android/support/room/BuilderTest.java b/room/runtime/src/test/java/com/android/support/room/BuilderTest.java
index ec3c6cb..741a27d 100644
--- a/room/runtime/src/test/java/com/android/support/room/BuilderTest.java
+++ b/room/runtime/src/test/java/com/android/support/room/BuilderTest.java
@@ -23,15 +23,23 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.mockito.Mockito.mock;
 
+import static java.util.Arrays.asList;
+
 import android.content.Context;
 
+import com.android.support.db.SupportSQLiteDatabase;
 import com.android.support.db.SupportSQLiteOpenHelper;
 import com.android.support.db.framework.FrameworkSQLiteOpenHelperFactory;
+import com.android.support.room.migration.Migration;
 
+import org.hamcrest.CoreMatchers;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
+import java.util.List;
+
+@SuppressWarnings("ArraysAsListWithZeroOrOneArgument")
 @RunWith(JUnit4.class)
 public class BuilderTest {
     @Test(expected = IllegalArgumentException.class)
@@ -58,6 +66,49 @@
     }
 
     @Test
+    public void migration() {
+        Migration m1 = new EmptyMigration(0, 1);
+        Migration m2 = new EmptyMigration(1, 2);
+        TestDatabase db = Room.databaseBuilder(mock(Context.class), TestDatabase.class, "foo")
+                .addMigrations(m1, m2).build();
+        DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
+        RoomDatabase.MigrationContainer migrations = config.migrationContainer;
+        assertThat(migrations.findMigrationPath(0, 1), is(asList(m1)));
+        assertThat(migrations.findMigrationPath(1, 2), is(asList(m2)));
+        assertThat(migrations.findMigrationPath(0, 2), is(asList(m1, m2)));
+        assertThat(migrations.findMigrationPath(2, 0), CoreMatchers.<List<Migration>>nullValue());
+        assertThat(migrations.findMigrationPath(0, 3), CoreMatchers.<List<Migration>>nullValue());
+    }
+
+    @Test
+    public void migrationOverride() {
+        Migration m1 = new EmptyMigration(0, 1);
+        Migration m2 = new EmptyMigration(1, 2);
+        Migration m3 = new EmptyMigration(0, 1);
+        TestDatabase db = Room.databaseBuilder(mock(Context.class), TestDatabase.class, "foo")
+                .addMigrations(m1, m2, m3).build();
+        DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
+        RoomDatabase.MigrationContainer migrations = config.migrationContainer;
+        assertThat(migrations.findMigrationPath(0, 1), is(asList(m3)));
+        assertThat(migrations.findMigrationPath(1, 2), is(asList(m2)));
+        assertThat(migrations.findMigrationPath(0, 3), CoreMatchers.<List<Migration>>nullValue());
+    }
+
+    @Test
+    public void migrationJump() {
+        Migration m1 = new EmptyMigration(0, 1);
+        Migration m2 = new EmptyMigration(1, 2);
+        Migration m3 = new EmptyMigration(2, 3);
+        Migration m4 = new EmptyMigration(0, 3);
+        TestDatabase db = Room.databaseBuilder(mock(Context.class), TestDatabase.class, "foo")
+                .addMigrations(m1, m2, m3, m4).build();
+        DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
+        RoomDatabase.MigrationContainer migrations = config.migrationContainer;
+        assertThat(migrations.findMigrationPath(0, 3), is(asList(m4)));
+        assertThat(migrations.findMigrationPath(1, 3), is(asList(m2, m3)));
+    }
+
+    @Test
     public void createBasic() {
         Context context = mock(Context.class);
         TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build();
@@ -66,7 +117,6 @@
         assertThat(config, notNullValue());
         assertThat(config.context, is(context));
         assertThat(config.name, is(nullValue()));
-        assertThat(config.version, is(1));
         assertThat(config.sqliteOpenHelperFactory,
                 instanceOf(FrameworkSQLiteOpenHelperFactory.class));
     }
@@ -77,15 +127,24 @@
         SupportSQLiteOpenHelper.Factory factory = mock(SupportSQLiteOpenHelper.Factory.class);
 
         TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
-                .version(41)
                 .openHelperFactory(factory)
                 .build();
         assertThat(db, instanceOf(BuilderTest_TestDatabase_Impl.class));
         DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
         assertThat(config, notNullValue());
-        assertThat(config.version, is(41));
         assertThat(config.sqliteOpenHelperFactory, is(factory));
     }
 
     abstract static class TestDatabase extends RoomDatabase {}
+
+    static class EmptyMigration extends Migration {
+        EmptyMigration(int start, int end) {
+            super(start, end);
+        }
+
+        @Override
+        public void migrate(SupportSQLiteDatabase database) {
+        }
+    }
+
 }
diff --git a/room/testing/build.gradle b/room/testing/build.gradle
new file mode 100644
index 0000000..61790e0
--- /dev/null
+++ b/room/testing/build.gradle
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ */
+import com.android.builder.core.BuilderConstants
+apply plugin: 'com.android.library'
+apply plugin: 'maven'
+
+android {
+    compileSdkVersion tools.current_sdk
+    buildToolsVersion tools.build_tools_version
+
+    defaultConfig {
+        minSdkVersion flatfoot.min_sdk
+        targetSdkVersion tools.current_sdk
+        versionCode 1
+        versionName "1.0"
+
+        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+
+    }
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+        }
+    }
+
+    testOptions {
+        unitTests.returnDefaultValues = true
+    }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_7
+        targetCompatibility JavaVersion.VERSION_1_7
+    }
+}
+
+dependencies {
+    compile project(":room:common")
+    compile project(":room:db")
+    compile project(":room:db-impl")
+    compile project(":room:migration")
+    compile project(":apptoolkit:core")
+    compile libs.support.core_utils
+    compile libs.junit
+}
+
+archivesBaseName = "testing"
+
+createAndroidCheckstyle(project)
+
+android.libraryVariants.all { variant ->
+    def name = variant.buildType.name
+    def suffix = name.capitalize()
+    project.tasks.create(name: "jar${suffix}", type: Jar){
+        dependsOn variant.javaCompile
+        from variant.javaCompile.destinationDir
+        destinationDir new File(project.buildDir, "libJar")
+    }
+}
diff --git a/room/testing/src/main/AndroidManifest.xml b/room/testing/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..73007ca
--- /dev/null
+++ b/room/testing/src/main/AndroidManifest.xml
@@ -0,0 +1,19 @@
+<!--
+  ~ 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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.support.room.testing">
+</manifest>
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
new file mode 100644
index 0000000..6239239
--- /dev/null
+++ b/room/testing/src/main/java/com/android/support/room/testing/MigrationTestHelper.java
@@ -0,0 +1,155 @@
+/*
+ * 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.testing;
+
+import android.content.Context;
+import android.util.Log;
+
+import com.android.support.db.SupportSQLiteDatabase;
+import com.android.support.db.SupportSQLiteOpenHelper;
+import com.android.support.room.migration.bundle.DatabaseBundle;
+import com.android.support.room.migration.bundle.SchemaBundle;
+
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A class that can be used in your Instrumentation tests that can create the database in an
+ * older schema.
+ * <p>
+ * You must copy the schema json files (created by passing {@code room.schemaLocation} argument
+ * into the annotation processor) into your test assets and pass in the path for that folder into
+ * the constructor. This class will read the folder and extract the schemas from there.
+ * <pre>
+ * android {
+ *   defaultConfig {
+ *     javaCompileOptions {
+ *       annotationProcessorOptions {
+ *         arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
+ *       }
+ *     }
+ *   }
+ *   sourceSets {
+ *     androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
+ *   }
+ * }
+ * </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<>();
+
+    /**
+     * Creates a new migration helper with an asset folder and the context.
+     *
+     * @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,
+            SupportSQLiteOpenHelper.Factory openFactory) {
+        mContext = context;
+        if (assetsFolder.endsWith("/")) {
+            assetsFolder = assetsFolder.substring(0, assetsFolder.length() - 1);
+        }
+        mAssetsFolder = assetsFolder;
+        mOpenFactory = openFactory;
+    }
+
+    /**
+     * 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 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.
+     */
+    @SuppressWarnings("SameParameterValue")
+    public SupportSQLiteDatabase createDatabase(String name, int version) throws IOException {
+        File dbPath = mContext.getDatabasePath(name);
+        if (dbPath.exists()) {
+            Log.d(TAG, "deleting database file " + name);
+            if (!dbPath.delete()) {
+                throw new IllegalStateException("there is a database file and i could not delete"
+                        + " it. Make sure you don't have any open connections to that database"
+                        + " before calling this method.");
+            }
+        }
+        SchemaBundle schemaBundle = loadSchema(version);
+        SupportSQLiteOpenHelper.Configuration config =
+                SupportSQLiteOpenHelper.Configuration
+                        .builder(mContext)
+                        .callback(new SchemaOpenCallback(schemaBundle.getDatabase()))
+                        .name(name)
+                        .version(version)
+                        .build();
+        SupportSQLiteDatabase db = mOpenFactory.create(config).getWritableDatabase();
+        mCreatedDatabases.add(new WeakReference<>(db));
+        return db;
+    }
+
+    @Override
+    protected void finished(Description description) {
+        super.finished(description);
+        for (WeakReference<SupportSQLiteDatabase> dbRef : mCreatedDatabases) {
+            SupportSQLiteDatabase db = dbRef.get();
+            if (db != null && db.isOpen()) {
+                try {
+                    db.close();
+                } catch (Throwable ignored) {
+                }
+            }
+        }
+    }
+
+    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;
+        }
+
+        @Override
+        public void onCreate(SupportSQLiteDatabase db) {
+            for (String query : mDatabaseBundle.buildCreateQueries()) {
+                db.execSQL(query);
+            }
+        }
+
+        @Override
+        public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
+            throw new UnsupportedOperationException("cannot upgrade when creating database");
+        }
+    }
+}
diff --git a/samples-flatfoot/ApiReviewDemo/app/src/main/java/com/android/flatfoot/apireviewdemo/common/github/GithubDatabase.java b/samples-flatfoot/ApiReviewDemo/app/src/main/java/com/android/flatfoot/apireviewdemo/common/github/GithubDatabase.java
index 8a8fffc..30540e2 100644
--- a/samples-flatfoot/ApiReviewDemo/app/src/main/java/com/android/flatfoot/apireviewdemo/common/github/GithubDatabase.java
+++ b/samples-flatfoot/ApiReviewDemo/app/src/main/java/com/android/flatfoot/apireviewdemo/common/github/GithubDatabase.java
@@ -23,7 +23,7 @@
 /**
  * Database for Github entities.
  */
-@Database(entities = {Person.class})
+@Database(entities = {Person.class}, version = 1)
 public abstract class GithubDatabase extends RoomDatabase {
     /**
      * Gets the data access object.
diff --git a/samples-flatfoot/ApiReviewDemo/app/src/main/java/com/android/flatfoot/apireviewdemo/db_01_basic/BasicDatabase.java b/samples-flatfoot/ApiReviewDemo/app/src/main/java/com/android/flatfoot/apireviewdemo/db_01_basic/BasicDatabase.java
index 78e428c..ef942ac 100644
--- a/samples-flatfoot/ApiReviewDemo/app/src/main/java/com/android/flatfoot/apireviewdemo/db_01_basic/BasicDatabase.java
+++ b/samples-flatfoot/ApiReviewDemo/app/src/main/java/com/android/flatfoot/apireviewdemo/db_01_basic/BasicDatabase.java
@@ -20,7 +20,7 @@
 import com.android.support.room.Database;
 import com.android.support.room.RoomDatabase;
 
-@Database(entities = User.class)
+@Database(entities = User.class, version = 1)
 public abstract class BasicDatabase extends RoomDatabase {
 
 }
diff --git a/samples-flatfoot/ApiReviewDemo/app/src/main/java/com/android/flatfoot/apireviewdemo/db_02_dao/AppDatabase_02.java b/samples-flatfoot/ApiReviewDemo/app/src/main/java/com/android/flatfoot/apireviewdemo/db_02_dao/AppDatabase_02.java
index f99e5bc..278444b 100644
--- a/samples-flatfoot/ApiReviewDemo/app/src/main/java/com/android/flatfoot/apireviewdemo/db_02_dao/AppDatabase_02.java
+++ b/samples-flatfoot/ApiReviewDemo/app/src/main/java/com/android/flatfoot/apireviewdemo/db_02_dao/AppDatabase_02.java
@@ -20,7 +20,7 @@
 import com.android.support.room.Database;
 import com.android.support.room.RoomDatabase;
 
-@Database(entities = User.class)
+@Database(entities = User.class, version = 2)
 public abstract class AppDatabase_02 extends RoomDatabase {
     public abstract UserCrudDao userDao();
 }
diff --git a/samples-flatfoot/ApiReviewDemo/app/src/main/java/com/android/flatfoot/apireviewdemo/db_03_entity/AppDatabase_03.java b/samples-flatfoot/ApiReviewDemo/app/src/main/java/com/android/flatfoot/apireviewdemo/db_03_entity/AppDatabase_03.java
index 5c513c4..c052ee6 100644
--- a/samples-flatfoot/ApiReviewDemo/app/src/main/java/com/android/flatfoot/apireviewdemo/db_03_entity/AppDatabase_03.java
+++ b/samples-flatfoot/ApiReviewDemo/app/src/main/java/com/android/flatfoot/apireviewdemo/db_03_entity/AppDatabase_03.java
@@ -21,7 +21,7 @@
 import com.android.support.room.Database;
 import com.android.support.room.RoomDatabase;
 
-@Database(entities = {User.class, Pet.class})
+@Database(entities = {User.class, Pet.class}, version = 3)
 public abstract class AppDatabase_03 extends RoomDatabase {
     public abstract UserCrudDao userDao();
     public abstract PetDao petDao();
diff --git a/samples-flatfoot/ApiReviewDemo/app/src/main/java/com/android/flatfoot/apireviewdemo/db_04_pojo/AppDatabase_04.java b/samples-flatfoot/ApiReviewDemo/app/src/main/java/com/android/flatfoot/apireviewdemo/db_04_pojo/AppDatabase_04.java
index def9303..ab3b133 100644
--- a/samples-flatfoot/ApiReviewDemo/app/src/main/java/com/android/flatfoot/apireviewdemo/db_04_pojo/AppDatabase_04.java
+++ b/samples-flatfoot/ApiReviewDemo/app/src/main/java/com/android/flatfoot/apireviewdemo/db_04_pojo/AppDatabase_04.java
@@ -23,7 +23,7 @@
 import com.android.support.room.Database;
 import com.android.support.room.RoomDatabase;
 
-@Database(entities = {User.class, Pet.class})
+@Database(entities = {User.class, Pet.class}, version = 4)
 public abstract class AppDatabase_04 extends RoomDatabase {
     public abstract UserPetDao userPetDao();
 }
diff --git a/samples-flatfoot/ApiReviewDemo/app/src/main/java/com/android/flatfoot/apireviewdemo/db_05_converters/AppDatabase_05.java b/samples-flatfoot/ApiReviewDemo/app/src/main/java/com/android/flatfoot/apireviewdemo/db_05_converters/AppDatabase_05.java
index 6e48f22..d0bf1bb 100644
--- a/samples-flatfoot/ApiReviewDemo/app/src/main/java/com/android/flatfoot/apireviewdemo/db_05_converters/AppDatabase_05.java
+++ b/samples-flatfoot/ApiReviewDemo/app/src/main/java/com/android/flatfoot/apireviewdemo/db_05_converters/AppDatabase_05.java
@@ -19,7 +19,7 @@
 import com.android.support.room.Database;
 import com.android.support.room.RoomDatabase;
 
-@Database(entities = Game.class)
+@Database(entities = Game.class, version = 5)
 public abstract class AppDatabase_05 extends RoomDatabase {
     public abstract GameDao gameDao();
 }
diff --git a/samples-flatfoot/ApiReviewDemo/app/src/main/java/com/android/flatfoot/apireviewdemo/db_06_decompose/AppDatabase_06.java b/samples-flatfoot/ApiReviewDemo/app/src/main/java/com/android/flatfoot/apireviewdemo/db_06_decompose/AppDatabase_06.java
index 5cd4e4c..003a26c 100644
--- a/samples-flatfoot/ApiReviewDemo/app/src/main/java/com/android/flatfoot/apireviewdemo/db_06_decompose/AppDatabase_06.java
+++ b/samples-flatfoot/ApiReviewDemo/app/src/main/java/com/android/flatfoot/apireviewdemo/db_06_decompose/AppDatabase_06.java
@@ -19,6 +19,6 @@
 import com.android.support.room.Database;
 import com.android.support.room.RoomDatabase;
 
-@Database(entities = {School.class})
+@Database(entities = {School.class}, version = 6)
 public abstract class AppDatabase_06 extends RoomDatabase {
 }
diff --git a/samples-flatfoot/ApiReviewDemo/app/src/main/java/com/android/flatfoot/apireviewdemo/full_sample_xxx/AccountViewModel.java b/samples-flatfoot/ApiReviewDemo/app/src/main/java/com/android/flatfoot/apireviewdemo/full_sample_xxx/AccountViewModel.java
index 21d8a72..fe7835d 100644
--- a/samples-flatfoot/ApiReviewDemo/app/src/main/java/com/android/flatfoot/apireviewdemo/full_sample_xxx/AccountViewModel.java
+++ b/samples-flatfoot/ApiReviewDemo/app/src/main/java/com/android/flatfoot/apireviewdemo/full_sample_xxx/AccountViewModel.java
@@ -82,7 +82,7 @@
         mLogin = login;
         mPrivatePersonData = GithubDatabaseHelper.getDatabase().getGithubDao().getLivePerson(
                 mLogin);
-        mPrivatePersonData.observe(mObserver);
+        mPrivatePersonData.observeForever(mObserver);
         statusData.setValue(new Status(0, true));
         mCurrentRequest = mDataManagement.refreshIfNeeded(mLogin, mCallback);
     }
diff --git a/samples-flatfoot/GithubBrowser/app/build.gradle b/samples-flatfoot/GithubBrowser/app/build.gradle
index 8a8ba1a..b1f12ac 100644
--- a/samples-flatfoot/GithubBrowser/app/build.gradle
+++ b/samples-flatfoot/GithubBrowser/app/build.gradle
@@ -26,6 +26,12 @@
         versionCode 1
         versionName "1.0"
         testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+
+        javaCompileOptions {
+            annotationProcessorOptions {
+                arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
+            }
+        }
     }
     dataBinding {
         enabled = true
diff --git a/samples-flatfoot/GithubBrowser/app/schemas/com.android.sample.githubbrowser.db.GithubDatabase/1.json b/samples-flatfoot/GithubBrowser/app/schemas/com.android.sample.githubbrowser.db.GithubDatabase/1.json
new file mode 100644
index 0000000..1409973
--- /dev/null
+++ b/samples-flatfoot/GithubBrowser/app/schemas/com.android.sample.githubbrowser.db.GithubDatabase/1.json
@@ -0,0 +1,393 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 1,
+    "identityHash": "b1b39c866af4b7b44d7ea3c6202c4352",
+    "entities": [
+      {
+        "tableName": "PersonData",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`login` TEXT, `id` TEXT, `avatar_url` TEXT, `repos_url` TEXT, `name` TEXT, `company` TEXT, `blog` TEXT, `location` TEXT, `email` TEXT, `public_repos` INTEGER, `followers` INTEGER, `following` INTEGER, `created_at` TEXT, PRIMARY KEY(`login`))",
+        "fields": [
+          {
+            "fieldPath": "login",
+            "columnName": "login",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "avatar_url",
+            "columnName": "avatar_url",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "repos_url",
+            "columnName": "repos_url",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "company",
+            "columnName": "company",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "blog",
+            "columnName": "blog",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "location",
+            "columnName": "location",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "email",
+            "columnName": "email",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "public_repos",
+            "columnName": "public_repos",
+            "affinity": "INTEGER"
+          },
+          {
+            "fieldPath": "followers",
+            "columnName": "followers",
+            "affinity": "INTEGER"
+          },
+          {
+            "fieldPath": "following",
+            "columnName": "following",
+            "affinity": "INTEGER"
+          },
+          {
+            "fieldPath": "created_at",
+            "columnName": "created_at",
+            "affinity": "TEXT"
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "login"
+          ],
+          "autoGenerate": false
+        },
+        "indices": []
+      },
+      {
+        "tableName": "SearchQueryData",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`searchQuery` TEXT, `searchKind` INTEGER, `timestamp` INTEGER, `indexOfLastFetchedPage` INTEGER, `numberOfFetchedItems` INTEGER, `hasNoMoreData` INTEGER, PRIMARY KEY(`searchQuery`, `searchKind`))",
+        "fields": [
+          {
+            "fieldPath": "searchQuery",
+            "columnName": "searchQuery",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "searchKind",
+            "columnName": "searchKind",
+            "affinity": "INTEGER"
+          },
+          {
+            "fieldPath": "timestamp",
+            "columnName": "timestamp",
+            "affinity": "INTEGER"
+          },
+          {
+            "fieldPath": "indexOfLastFetchedPage",
+            "columnName": "indexOfLastFetchedPage",
+            "affinity": "INTEGER"
+          },
+          {
+            "fieldPath": "numberOfFetchedItems",
+            "columnName": "numberOfFetchedItems",
+            "affinity": "INTEGER"
+          },
+          {
+            "fieldPath": "hasNoMoreData",
+            "columnName": "hasNoMoreData",
+            "affinity": "INTEGER"
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "searchQuery",
+            "searchKind"
+          ],
+          "autoGenerate": false
+        },
+        "indices": []
+      },
+      {
+        "tableName": "GeneralRepoSearchData",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`searchQuery` TEXT, `resultIndex` INTEGER, `repoId` TEXT, PRIMARY KEY(`searchQuery`, `resultIndex`, `repoId`))",
+        "fields": [
+          {
+            "fieldPath": "searchQuery",
+            "columnName": "searchQuery",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "resultIndex",
+            "columnName": "resultIndex",
+            "affinity": "INTEGER"
+          },
+          {
+            "fieldPath": "repoId",
+            "columnName": "repoId",
+            "affinity": "TEXT"
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "searchQuery",
+            "resultIndex",
+            "repoId"
+          ],
+          "autoGenerate": false
+        },
+        "indices": []
+      },
+      {
+        "tableName": "RepositoryData",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT, `name` TEXT, `full_name` TEXT, `description` TEXT, `created_at` TEXT, `stargazers_count` INTEGER, `language` TEXT, `forks_count` INTEGER, `open_issues_count` INTEGER, `subscribers_count` INTEGER, `owner_login` TEXT, `owner_id` TEXT, `owner_avatar_url` TEXT, `owner_repos_url` TEXT, `owner_name` TEXT, `owner_company` TEXT, `owner_blog` TEXT, `owner_location` TEXT, `owner_email` TEXT, `owner_public_repos` INTEGER, `owner_followers` INTEGER, `owner_following` INTEGER, `owner_created_at` TEXT, `organization_login` TEXT, `organization_id` TEXT, `organization_avatar_url` TEXT, `organization_repos_url` TEXT, `organization_name` TEXT, `organization_company` TEXT, `organization_blog` TEXT, `organization_location` TEXT, `organization_email` TEXT, `organization_public_repos` INTEGER, `organization_followers` INTEGER, `organization_following` INTEGER, `organization_created_at` TEXT, PRIMARY KEY(`id`))",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "full_name",
+            "columnName": "full_name",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "description",
+            "columnName": "description",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "created_at",
+            "columnName": "created_at",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "stargazers_count",
+            "columnName": "stargazers_count",
+            "affinity": "INTEGER"
+          },
+          {
+            "fieldPath": "language",
+            "columnName": "language",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "forks_count",
+            "columnName": "forks_count",
+            "affinity": "INTEGER"
+          },
+          {
+            "fieldPath": "open_issues_count",
+            "columnName": "open_issues_count",
+            "affinity": "INTEGER"
+          },
+          {
+            "fieldPath": "subscribers_count",
+            "columnName": "subscribers_count",
+            "affinity": "INTEGER"
+          },
+          {
+            "fieldPath": "owner.login",
+            "columnName": "owner_login",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "owner.id",
+            "columnName": "owner_id",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "owner.avatar_url",
+            "columnName": "owner_avatar_url",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "owner.repos_url",
+            "columnName": "owner_repos_url",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "owner.name",
+            "columnName": "owner_name",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "owner.company",
+            "columnName": "owner_company",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "owner.blog",
+            "columnName": "owner_blog",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "owner.location",
+            "columnName": "owner_location",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "owner.email",
+            "columnName": "owner_email",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "owner.public_repos",
+            "columnName": "owner_public_repos",
+            "affinity": "INTEGER"
+          },
+          {
+            "fieldPath": "owner.followers",
+            "columnName": "owner_followers",
+            "affinity": "INTEGER"
+          },
+          {
+            "fieldPath": "owner.following",
+            "columnName": "owner_following",
+            "affinity": "INTEGER"
+          },
+          {
+            "fieldPath": "owner.created_at",
+            "columnName": "owner_created_at",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "organization.login",
+            "columnName": "organization_login",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "organization.id",
+            "columnName": "organization_id",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "organization.avatar_url",
+            "columnName": "organization_avatar_url",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "organization.repos_url",
+            "columnName": "organization_repos_url",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "organization.name",
+            "columnName": "organization_name",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "organization.company",
+            "columnName": "organization_company",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "organization.blog",
+            "columnName": "organization_blog",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "organization.location",
+            "columnName": "organization_location",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "organization.email",
+            "columnName": "organization_email",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "organization.public_repos",
+            "columnName": "organization_public_repos",
+            "affinity": "INTEGER"
+          },
+          {
+            "fieldPath": "organization.followers",
+            "columnName": "organization_followers",
+            "affinity": "INTEGER"
+          },
+          {
+            "fieldPath": "organization.following",
+            "columnName": "organization_following",
+            "affinity": "INTEGER"
+          },
+          {
+            "fieldPath": "organization.created_at",
+            "columnName": "organization_created_at",
+            "affinity": "TEXT"
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": false
+        },
+        "indices": []
+      },
+      {
+        "tableName": "ContributorSearchData",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`searchQuery` TEXT, `resultIndex` INTEGER, `contributorId` TEXT, `contributions` INTEGER, PRIMARY KEY(`searchQuery`, `resultIndex`, `contributorId`))",
+        "fields": [
+          {
+            "fieldPath": "searchQuery",
+            "columnName": "searchQuery",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "resultIndex",
+            "columnName": "resultIndex",
+            "affinity": "INTEGER"
+          },
+          {
+            "fieldPath": "contributorId",
+            "columnName": "contributorId",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "contributions",
+            "columnName": "contributions",
+            "affinity": "INTEGER"
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "searchQuery",
+            "resultIndex",
+            "contributorId"
+          ],
+          "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, \"b1b39c866af4b7b44d7ea3c6202c4352\")"
+    ]
+  }
+}
\ No newline at end of file
diff --git a/samples-flatfoot/GithubBrowser/app/src/main/java/com/android/sample/githubbrowser/db/GithubDatabase.java b/samples-flatfoot/GithubBrowser/app/src/main/java/com/android/sample/githubbrowser/db/GithubDatabase.java
index 1b43e41..91f7fbe 100644
--- a/samples-flatfoot/GithubBrowser/app/src/main/java/com/android/sample/githubbrowser/db/GithubDatabase.java
+++ b/samples-flatfoot/GithubBrowser/app/src/main/java/com/android/sample/githubbrowser/db/GithubDatabase.java
@@ -27,8 +27,8 @@
 /**
  * Database for Github entities.
  */
-@Database(entities = {PersonData.class, SearchQueryData.class, GeneralRepoSearchData.class,
-        RepositoryData.class, ContributorSearchData.class})
+@Database(version = 1, entities = {PersonData.class, SearchQueryData.class,
+        GeneralRepoSearchData.class, RepositoryData.class, ContributorSearchData.class})
 public abstract class GithubDatabase extends RoomDatabase {
     /**
      * Gets the data access object.
diff --git a/samples-flatfoot/GithubBrowser/app/src/main/java/com/android/sample/githubbrowser/util/ChainedLiveData.java b/samples-flatfoot/GithubBrowser/app/src/main/java/com/android/sample/githubbrowser/util/ChainedLiveData.java
index 5671a5f..edba758 100644
--- a/samples-flatfoot/GithubBrowser/app/src/main/java/com/android/sample/githubbrowser/util/ChainedLiveData.java
+++ b/samples-flatfoot/GithubBrowser/app/src/main/java/com/android/sample/githubbrowser/util/ChainedLiveData.java
@@ -50,7 +50,7 @@
             setValue(null);
         } else {
             if (getActiveObserverCount() > 0) {
-                backingLiveData.observe(mObserver);
+                backingLiveData.observeForever(mObserver);
             } else {
                 setValue(backingLiveData.getValue());
             }
@@ -60,7 +60,7 @@
     @Override
     protected void onActive() {
         if (mBackingLiveData != null) {
-            mBackingLiveData.observe(mObserver);
+            mBackingLiveData.observeForever(mObserver);
         }
     }
 
diff --git a/samples-flatfoot/flatfootcodelab/app/src/main/java/com/example/android/flatfoot/codelab/flatfootcodelab/orm_db/AppDatabase.java b/samples-flatfoot/flatfootcodelab/app/src/main/java/com/example/android/flatfoot/codelab/flatfootcodelab/orm_db/AppDatabase.java
index 52080df..b743c78 100644
--- a/samples-flatfoot/flatfootcodelab/app/src/main/java/com/example/android/flatfoot/codelab/flatfootcodelab/orm_db/AppDatabase.java
+++ b/samples-flatfoot/flatfootcodelab/app/src/main/java/com/example/android/flatfoot/codelab/flatfootcodelab/orm_db/AppDatabase.java
@@ -22,7 +22,7 @@
 import com.android.support.room.Room;
 import com.android.support.room.RoomDatabase;
 
-@Database(entities = {User.class, Book.class, Loan.class})
+@Database(entities = {User.class, Book.class, Loan.class}, version = 1)
 public abstract class AppDatabase extends RoomDatabase {
 
     private static AppDatabase INSTANCE;
diff --git a/samples-flatfoot/flatfootcodelab/app/src/main/java/com/example/android/flatfoot/codelab/flatfootcodelab/step10/internal/ProductsDatabase.java b/samples-flatfoot/flatfootcodelab/app/src/main/java/com/example/android/flatfoot/codelab/flatfootcodelab/step10/internal/ProductsDatabase.java
index 659a147..4b72e60 100644
--- a/samples-flatfoot/flatfootcodelab/app/src/main/java/com/example/android/flatfoot/codelab/flatfootcodelab/step10/internal/ProductsDatabase.java
+++ b/samples-flatfoot/flatfootcodelab/app/src/main/java/com/example/android/flatfoot/codelab/flatfootcodelab/step10/internal/ProductsDatabase.java
@@ -20,7 +20,7 @@
 import com.android.support.room.Database;
 import com.android.support.room.RoomDatabase;
 
-@Database(entities = {InternalProduct.class, InternalComment.class})
+@Database(entities = {InternalProduct.class, InternalComment.class}, version = 1)
 abstract class ProductsDatabase extends RoomDatabase {
     abstract ProductReviewDao dao();
 }
diff --git a/samples-flatfoot/flatfootcodelab/app/src/main/java/com/example/android/flatfoot/codelab/flatfootcodelab/step10_solution/db/MyDatabase.java b/samples-flatfoot/flatfootcodelab/app/src/main/java/com/example/android/flatfoot/codelab/flatfootcodelab/step10_solution/db/MyDatabase.java
index 32f21fc..c127831 100644
--- a/samples-flatfoot/flatfootcodelab/app/src/main/java/com/example/android/flatfoot/codelab/flatfootcodelab/step10_solution/db/MyDatabase.java
+++ b/samples-flatfoot/flatfootcodelab/app/src/main/java/com/example/android/flatfoot/codelab/flatfootcodelab/step10_solution/db/MyDatabase.java
@@ -13,7 +13,7 @@
 import com.example.android.flatfoot.codelab.flatfootcodelab.step10_solution.entity.MyComment;
 import com.example.android.flatfoot.codelab.flatfootcodelab.step10_solution.entity.MyProduct;
 
-@Database(entities = {MyProduct.class, MyComment.class})
+@Database(entities = {MyProduct.class, MyComment.class}, version = 1)
 @TypeConverters(DateConverter.class)
 public abstract class MyDatabase extends RoomDatabase {
     private static MyDatabase sInstance;
diff --git a/samples-flatfoot/flatfootcodelab/app/src/main/java/com/example/android/flatfoot/codelab/flatfootcodelab/step9_solution/ShowUserViewModel.java b/samples-flatfoot/flatfootcodelab/app/src/main/java/com/example/android/flatfoot/codelab/flatfootcodelab/step9_solution/ShowUserViewModel.java
index 71650df..48257b5 100644
--- a/samples-flatfoot/flatfootcodelab/app/src/main/java/com/example/android/flatfoot/codelab/flatfootcodelab/step9_solution/ShowUserViewModel.java
+++ b/samples-flatfoot/flatfootcodelab/app/src/main/java/com/example/android/flatfoot/codelab/flatfootcodelab/step9_solution/ShowUserViewModel.java
@@ -21,6 +21,7 @@
 import com.android.support.lifecycle.LiveData;
 import com.android.support.lifecycle.Observer;
 import com.android.support.lifecycle.ViewModel;
+
 import com.example.android.flatfoot.codelab.flatfootcodelab.orm_db.AppDatabase;
 import com.example.android.flatfoot.codelab.flatfootcodelab.orm_db.LoanWithUserAndBook;
 import com.example.android.flatfoot.codelab.flatfootcodelab.orm_db.utils.DatabaseInitializer;
@@ -40,6 +41,25 @@
 
     private AppDatabase mDb;
 
+    private final Observer<List<LoanWithUserAndBook>> mObserver =
+            new Observer<List<LoanWithUserAndBook>>() {
+                @Override
+                public void onChanged(
+                        @NonNull final List<LoanWithUserAndBook> loansWithUserAndBook) {
+                    StringBuilder sb = new StringBuilder();
+                    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm",
+                            Locale.US);
+
+                    for (LoanWithUserAndBook loan : loansWithUserAndBook) {
+                        sb.append(String.format("%s\n  (Returned: %s)\n",
+                                loan.bookTitle,
+                                simpleDateFormat.format(loan.endTime)));
+
+                    }
+                    mLoansResult.setValue(sb.toString());
+                }
+            };
+
     public LiveData<String> getLoansResult() {
         return mLoansResult;
     }
@@ -61,24 +81,23 @@
         Calendar calendar = Calendar.getInstance();
         calendar.set(Calendar.DATE, -1);
         Date yesterday = calendar.getTime();
+        removeObserver();
         mLoans = mDb.loanModel().findLoansByNameAfter("Mike", yesterday);
 
         mLoansResult = new LiveData<>();
 
-        mLoans.observe(new Observer<List<LoanWithUserAndBook>>() {
-            @Override
-            public void onChanged(@NonNull final List<LoanWithUserAndBook> loansWithUserAndBook) {
-                StringBuilder sb = new StringBuilder();
-                SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US);
+        mLoans.observeForever(mObserver);
+    }
 
-                for (LoanWithUserAndBook loan : loansWithUserAndBook) {
-                    sb.append(String.format("%s\n  (Returned: %s)\n",
-                            loan.bookTitle,
-                            simpleDateFormat.format(loan.endTime)));
+    @Override
+    protected void onCleared() {
+        super.onCleared();
+        removeObserver();
+    }
 
-                }
-                mLoansResult.setValue(sb.toString());
-            }
-        });
+    private void removeObserver() {
+        if (mLoans != null) {
+            mLoans.removeObserver(mObserver);
+        }
     }
 }