Implementing support for handling all schema changes including FTS Table related ones.

This change handles detecting all schema changes involving FTS tables in order to generate an auto migration. During code generation, it is important to note that the normal (non-fts) tables that have complex schema changes are processed before FTS table related changes are processed. This is to handle the case where content tables FTS tables reference may have schema changes such as table renames.

Test: Tests have been added to verify different FTS table related changes are assessed correctly. In addition, a test has been added in AutoMigrationTest.kt to verify that a user defined Migration is chosen over an auto migration, when both are present (the user has defined an auto migration manually as well as added an @AutoMigration annotation in the Database with the same "from" and "to" versions).
Bug: 180395129, 181777611
Change-Id: I250d695becdaaa6240b011c2dc03ec31eaceed17
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt b/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
index 9e437ab..bc40eab 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
@@ -942,8 +942,6 @@
             " a different name."
     }
 
-    val FTS_TABLE_NOT_CURRENTLY_SUPPORTED = "Schemas involving FTS tables are not currently " +
-        "supported."
     val INNER_CLASS_AUTOMIGRATION_SPEC_MUST_BE_STATIC = "An inner class AutoMigrationSpec must be" +
         " static."
 
diff --git a/room/compiler/src/main/kotlin/androidx/room/util/SchemaDiffer.kt b/room/compiler/src/main/kotlin/androidx/room/util/SchemaDiffer.kt
index 390a4ef..7d91803 100644
--- a/room/compiler/src/main/kotlin/androidx/room/util/SchemaDiffer.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/util/SchemaDiffer.kt
@@ -22,7 +22,6 @@
 import androidx.room.migration.bundle.ForeignKeyBundle
 import androidx.room.migration.bundle.FtsEntityBundle
 import androidx.room.migration.bundle.IndexBundle
-import androidx.room.processor.ProcessorErrors.FTS_TABLE_NOT_CURRENTLY_SUPPORTED
 import androidx.room.processor.ProcessorErrors.deletedOrRenamedTableFound
 import androidx.room.processor.ProcessorErrors.tableRenameError
 import androidx.room.processor.ProcessorErrors.conflictingRenameColumnAnnotationsFound
@@ -67,7 +66,6 @@
  * @param renameTableEntries List of repeatable annotations specifying table renames
  * @param deleteTableEntries List of repeatable annotations specifying table deletes
  */
-// TODO: (b/181777611) Handle FTS tables
 class SchemaDiffer(
     private val fromSchemaBundle: DatabaseBundle,
     private val toSchemaBundle: DatabaseBundle,
@@ -78,6 +76,9 @@
     private val deleteTableEntries: List<AutoMigrationResult.DeletedTable>
 ) {
     private val potentiallyDeletedTables = mutableSetOf<String>()
+    // Maps FTS tables in the to version to the name of their content tables in the from version
+    // for easy lookup.
+    private val contentTableToFtsEntities = mutableMapOf<String, MutableList<EntityBundle>>()
 
     private val addedTables = mutableSetOf<AutoMigrationResult.AddedTable>()
     // Any table that has been renamed, but also does not contain any complex changes.
@@ -107,13 +108,18 @@
         // deleted columns/tables
         fromSchemaBundle.entitiesByTableName.values.forEach { fromTable ->
             val toTable = detectTableLevelChanges(fromTable)
-            if (fromTable is FtsEntityBundle) {
-                diffError(FTS_TABLE_NOT_CURRENTLY_SUPPORTED)
-            }
 
             // Check for column related changes. Since we require toTable to not be null, any
             // deleted tables will be skipped here.
             if (toTable != null) {
+                if (fromTable is FtsEntityBundle &&
+                    fromTable.ftsOptions.contentTable.isNotEmpty()
+                ) {
+                    contentTableToFtsEntities.getOrElse(fromTable.ftsOptions.contentTable) {
+                        mutableListOf()
+                    }.add(fromTable)
+                }
+
                 val fromColumns = fromTable.fieldsByColumnName
                 val processedColumnsInNewVersion = fromColumns.values.mapNotNull { fromColumn ->
                     detectColumnLevelChanges(
@@ -141,8 +147,10 @@
                 )
             )
         }
-
         processDeletedColumns()
+
+        processContentTables()
+
         return SchemaDiffResult(
             addedColumns = addedColumns,
             deletedColumns = deletedColumns,
@@ -154,6 +162,27 @@
     }
 
     /**
+     * Checks if any content tables have been renamed, and if so, marks the FTS table referencing
+     * the content table as a complex changed table.
+     */
+    private fun processContentTables() {
+        renameTableEntries.forEach { renamedTable ->
+            contentTableToFtsEntities[renamedTable.originalTableName]?.filter {
+                !complexChangedTables.containsKey(it.tableName)
+            }?.forEach { ftsTable ->
+                complexChangedTables[ftsTable.tableName] =
+                    AutoMigrationResult.ComplexChangedTable(
+                        tableName = ftsTable.tableName,
+                        tableNameWithNewPrefix = ftsTable.newTableName,
+                        oldVersionEntityBundle = ftsTable,
+                        newVersionEntityBundle = ftsTable,
+                        renamedColumnsMap = mutableMapOf()
+                    )
+            }
+        }
+    }
+
+    /**
      * Detects any changes at the table-level, independent of any changes that may be present at
      * the column-level (e.g. column add/rename/delete).
      *
@@ -168,6 +197,7 @@
         // Check if the table was renamed. If so, check for other complex changes that could
         // be found on the table level. Save the end result to the complex changed tables map.
         val renamedTable = isTableRenamed(fromTable.tableName)
+
         if (renamedTable != null) {
             val toTable = toSchemaBundle.entitiesByTableName[renamedTable.newTableName]
             if (toTable != null) {
@@ -175,7 +205,8 @@
                     fromTable,
                     toTable
                 )
-                if (isComplexChangedTable) {
+                val isFtsEntity = fromTable is FtsEntityBundle
+                if (isComplexChangedTable || isFtsEntity) {
                     if (toSchemaBundle.entitiesByTableName.containsKey(toTable.newTableName)) {
                         diffError(tableWithConflictingPrefixFound(toTable.newTableName))
                     }
@@ -233,6 +264,8 @@
         if (!isDeletedTable) {
             potentiallyDeletedTables.add(fromTable.tableName)
         }
+
+        // Table was deleted.
         return null
     }
 
@@ -313,6 +346,8 @@
                 )
             )
         }
+
+        // Column was deleted
         return null
     }
 
@@ -329,6 +364,20 @@
         fromTable: EntityBundle,
         toTable: EntityBundle
     ): Boolean {
+        // If we have an FTS table, check if options have changed
+        if (fromTable is FtsEntityBundle &&
+            toTable is FtsEntityBundle &&
+            !fromTable.ftsOptions.isSchemaEqual(toTable.ftsOptions)
+        ) {
+            return true
+        }
+        // Check if the to table or the from table is an FTS table while the other is not.
+        if (fromTable is FtsEntityBundle && !(toTable is FtsEntityBundle) ||
+            toTable is FtsEntityBundle && !(fromTable is FtsEntityBundle)
+        ) {
+            return true
+        }
+
         if (!isForeignKeyBundlesListEqual(fromTable.foreignKeys, toTable.foreignKeys)) {
             return true
         }
diff --git a/room/compiler/src/main/kotlin/androidx/room/writer/AutoMigrationWriter.kt b/room/compiler/src/main/kotlin/androidx/room/writer/AutoMigrationWriter.kt
index 4a3f95e..d62073e 100644
--- a/room/compiler/src/main/kotlin/androidx/room/writer/AutoMigrationWriter.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/writer/AutoMigrationWriter.kt
@@ -25,8 +25,8 @@
 import androidx.room.ext.SupportDbTypeNames
 import androidx.room.ext.T
 import androidx.room.migration.bundle.EntityBundle
+import androidx.room.migration.bundle.FtsEntityBundle
 import androidx.room.vo.AutoMigrationResult
-import com.squareup.javapoet.ClassName
 import com.squareup.javapoet.FieldSpec
 import com.squareup.javapoet.MethodSpec
 import com.squareup.javapoet.ParameterSpec
@@ -37,7 +37,6 @@
 /**
  * Writes the implementation of migrations that were annotated with @AutoMigration.
  */
-// TODO: (b/181777611) Handle FTS tables
 class AutoMigrationWriter(
     private val dbElement: XElement,
     val autoMigrationResult: AutoMigrationResult
@@ -65,9 +64,9 @@
                         "new $T()", autoMigrationResult.specClassName
                     ).build()
                 )
-                addMethod(createConstructor())
             }
-            addMethod(createMigrateMethod(autoMigrationResult.specClassName))
+            addMethod(createConstructor())
+            addMethod(createMigrateMethod())
         }
         return builder
     }
@@ -84,7 +83,7 @@
         }.build()
     }
 
-    private fun createMigrateMethod(specClassName: ClassName?): MethodSpec? {
+    private fun createMigrateMethod(): MethodSpec? {
         val migrateFunctionBuilder: MethodSpec.Builder = MethodSpec.methodBuilder("migrate")
             .apply {
                 addParameter(
@@ -97,7 +96,7 @@
                 addModifiers(Modifier.PUBLIC)
                 returns(TypeName.VOID)
                 addAutoMigrationResultToMigrate(this)
-                if (specClassName != null) {
+                if (autoMigrationResult.specClassName != null) {
                     addStatement("callback.onPostMigrate(database)")
                 }
             }
@@ -112,8 +111,8 @@
      * @param migrateBuilder Builder for the migrate() function to be generated
      */
     private fun addAutoMigrationResultToMigrate(migrateBuilder: MethodSpec.Builder) {
-        addComplexChangeStatements(migrateBuilder)
         addSimpleChangeStatements(migrateBuilder)
+        addComplexChangeStatements(migrateBuilder)
     }
 
     /**
@@ -123,7 +122,11 @@
      * @param migrateBuilder Builder for the migrate() function to be generated
      */
     private fun addComplexChangeStatements(migrateBuilder: MethodSpec.Builder) {
-        complexChangedTables.values.forEach {
+        // Create a collection that is sorted such that FTS bundles are handled after the normal
+        // tables have been processed
+        complexChangedTables.values.sortedBy {
+            it.newVersionEntityBundle is FtsEntityBundle
+        }.forEach {
             (
                 _,
                 tableNameWithNewPrefix,
@@ -132,28 +135,82 @@
                 renamedColumnsMap
             ) ->
 
-            addStatementsToCreateNewTable(newEntityBundle, migrateBuilder)
-            addStatementsToContentTransfer(
-                oldEntityBundle.tableName,
-                tableNameWithNewPrefix,
-                oldEntityBundle,
-                newEntityBundle,
-                renamedColumnsMap,
-                migrateBuilder
-            )
-            addStatementsToDropTableAndRenameTempTable(
-                oldEntityBundle.tableName,
-                newEntityBundle.tableName,
-                tableNameWithNewPrefix,
-                migrateBuilder
-            )
-            addStatementsToRecreateIndexes(newEntityBundle, migrateBuilder)
-            if (newEntityBundle.foreignKeys.isNotEmpty()) {
-                addStatementsToCheckForeignKeyConstraint(newEntityBundle.tableName, migrateBuilder)
+            if (oldEntityBundle is FtsEntityBundle &&
+                !oldEntityBundle.ftsOptions.contentTable.isNullOrBlank()
+            ) {
+                addStatementsToMigrateFtsTable(
+                    migrateBuilder,
+                    oldEntityBundle,
+                    newEntityBundle,
+                    renamedColumnsMap
+                )
+            } else {
+                addStatementsToCreateNewTable(newEntityBundle, migrateBuilder)
+                addStatementsToContentTransfer(
+                    oldEntityBundle.tableName,
+                    tableNameWithNewPrefix,
+                    oldEntityBundle,
+                    newEntityBundle,
+                    renamedColumnsMap,
+                    migrateBuilder
+                )
+                addStatementsToDropTableAndRenameTempTable(
+                    oldEntityBundle.tableName,
+                    newEntityBundle.tableName,
+                    tableNameWithNewPrefix,
+                    migrateBuilder
+                )
+                addStatementsToRecreateIndexes(newEntityBundle, migrateBuilder)
+                if (newEntityBundle.foreignKeys.isNotEmpty()) {
+                    addStatementsToCheckForeignKeyConstraint(
+                        newEntityBundle.tableName,
+                        migrateBuilder
+                    )
+                }
             }
         }
     }
 
+    private fun addStatementsToMigrateFtsTable(
+        migrateBuilder: MethodSpec.Builder,
+        oldTable: EntityBundle,
+        newTable: EntityBundle,
+        renamedColumnsMap: MutableMap<String, String>
+    ) {
+        addDatabaseExecuteSqlStatement(migrateBuilder, "DROP TABLE `${oldTable.tableName}`")
+        addDatabaseExecuteSqlStatement(migrateBuilder, newTable.createTable())
+
+        // Transfer contents of the FTS table, using the content table if available.
+        val newColumnSequence = oldTable.fieldsByColumnName.keys.filter {
+            oldTable.fieldsByColumnName.keys.contains(it) ||
+                renamedColumnsMap.containsKey(it)
+        }.toMutableList()
+        val oldColumnSequence = mutableListOf<String>()
+        newColumnSequence.forEach { column ->
+            oldColumnSequence.add(renamedColumnsMap[column] ?: column)
+        }
+        if (oldTable is FtsEntityBundle) {
+            oldColumnSequence.add("rowid")
+            newColumnSequence.add("docid")
+        }
+        val contentTable = (newTable as FtsEntityBundle).ftsOptions.contentTable
+        val selectFromTable = if (contentTable.isEmpty()) {
+            oldTable.tableName
+        } else {
+            contentTable
+        }
+        addDatabaseExecuteSqlStatement(
+            migrateBuilder,
+            buildString {
+                append(
+                    "INSERT INTO `${newTable.tableName}` (${newColumnSequence.joinToString(",")})" +
+                        " SELECT ${oldColumnSequence.joinToString(",")} FROM " +
+                        "`$selectFromTable`",
+                )
+            }
+        )
+    }
+
     /**
      * Adds SQL statements performing schema altering commands directly supported by SQLite
      * (adding tables/columns, renaming tables/columns, dropping tables/columns). These changes
@@ -205,7 +262,7 @@
         val newColumnSequence = newEntityBundle.fieldsByColumnName.keys.filter {
             oldEntityBundle.fieldsByColumnName.keys.contains(it) ||
                 renamedColumnsMap.containsKey(it)
-        }
+        }.toMutableList()
         val oldColumnSequence = mutableListOf<String>()
         newColumnSequence.forEach { column ->
             oldColumnSequence.add(renamedColumnsMap[column] ?: column)
@@ -215,8 +272,10 @@
             migrateBuilder,
             buildString {
                 append(
-                    "INSERT INTO `$tableNameWithNewPrefix` (${newColumnSequence.joinToString(",")
-                    }) SELECT ${oldColumnSequence.joinToString(",")} FROM `$oldTableName`",
+                    "INSERT INTO `$tableNameWithNewPrefix` " +
+                        "(${newColumnSequence.joinToString(",")})" +
+                        " SELECT ${oldColumnSequence.joinToString(",")} FROM " +
+                        "`$oldTableName`",
                 )
             }
         )
diff --git a/room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.AutoMigrationDb/1.json b/room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.AutoMigrationDb/1.json
index 8b7d1c0..c041150 100644
--- a/room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.AutoMigrationDb/1.json
+++ b/room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.AutoMigrationDb/1.json
@@ -665,6 +665,128 @@
         },
         "indices": [],
         "foreignKeys": []
+      },
+      {
+        "ftsVersion": "FTS4",
+        "ftsOptions": {
+          "tokenizer": "simple",
+          "tokenizerArgs": [],
+          "contentTable": "Entity13",
+          "languageIdColumnName": "",
+          "matchInfo": "FTS4",
+          "notIndexedColumns": [],
+          "prefixSizes": [],
+          "preferredOrder": "ASC"
+        },
+        "contentSyncTriggers": [
+          "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_Entity21_BEFORE_UPDATE BEFORE UPDATE ON `Entity13` BEGIN DELETE FROM `Entity21` WHERE `docid`=OLD.`rowid`; END",
+          "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_Entity21_BEFORE_DELETE BEFORE DELETE ON `Entity13` BEGIN DELETE FROM `Entity21` WHERE `docid`=OLD.`rowid`; END",
+          "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_Entity21_AFTER_UPDATE AFTER UPDATE ON `Entity13` BEGIN INSERT INTO `Entity21`(`docid`, `name`, `addedInV1`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`addedInV1`); END",
+          "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_Entity21_AFTER_INSERT AFTER INSERT ON `Entity13` BEGIN INSERT INTO `Entity21`(`docid`, `name`, `addedInV1`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`addedInV1`); END"
+        ],
+        "tableName": "Entity21",
+        "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`name` TEXT, `addedInV1` INTEGER NOT NULL DEFAULT 1, content=`Entity13`)",
+        "fields": [
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "addedInV1",
+            "columnName": "addedInV1",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "1"
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "rowid"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "ftsVersion": "FTS3",
+        "ftsOptions": {
+          "tokenizer": "simple",
+          "tokenizerArgs": [],
+          "contentTable": "",
+          "languageIdColumnName": "",
+          "matchInfo": "FTS3",
+          "notIndexedColumns": [],
+          "prefixSizes": [],
+          "preferredOrder": "ASC"
+        },
+        "contentSyncTriggers": [],
+        "tableName": "Entity22",
+        "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS3(`name` TEXT, `addedInV1` INTEGER NOT NULL DEFAULT 1, matchinfo=fts3)",
+        "fields": [
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "addedInV1",
+            "columnName": "addedInV1",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "1"
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "rowid"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "ftsVersion": "FTS3",
+        "ftsOptions": {
+          "tokenizer": "simple",
+          "tokenizerArgs": [],
+          "contentTable": "",
+          "languageIdColumnName": "",
+          "matchInfo": "FTS3",
+          "notIndexedColumns": [],
+          "prefixSizes": [],
+          "preferredOrder": "ASC"
+        },
+        "contentSyncTriggers": [],
+        "tableName": "Entity23",
+        "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS3(`name` TEXT, `addedInV1` INTEGER NOT NULL DEFAULT 1, matchinfo=fts3)",
+        "fields": [
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "addedInV1",
+            "columnName": "addedInV1",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "1"
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "rowid"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [],
+        "foreignKeys": []
       }
     ],
     "views": [],
diff --git a/room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.AutoMigrationDb/2.json b/room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.AutoMigrationDb/2.json
index a925d683..631fe7f 100644
--- a/room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.AutoMigrationDb/2.json
+++ b/room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.AutoMigrationDb/2.json
@@ -2,7 +2,7 @@
   "formatVersion": 1,
   "database": {
     "version": 2,
-    "identityHash": "083a4e91debef71d5d900ca2cf0baccf",
+    "identityHash": "9479b874f83849f2d740bdbb874b449a",
     "entities": [
       {
         "tableName": "Entity1",
@@ -673,12 +673,180 @@
         },
         "indices": [],
         "foreignKeys": []
+      },
+      {
+        "ftsVersion": "FTS4",
+        "ftsOptions": {
+          "tokenizer": "simple",
+          "tokenizerArgs": [],
+          "contentTable": "Entity13_V2",
+          "languageIdColumnName": "",
+          "matchInfo": "FTS4",
+          "notIndexedColumns": [],
+          "prefixSizes": [],
+          "preferredOrder": "ASC"
+        },
+        "contentSyncTriggers": [
+          "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_Entity21_BEFORE_UPDATE BEFORE UPDATE ON `Entity13_V2` BEGIN DELETE FROM `Entity21` WHERE `docid`=OLD.`rowid`; END",
+          "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_Entity21_BEFORE_DELETE BEFORE DELETE ON `Entity13_V2` BEGIN DELETE FROM `Entity21` WHERE `docid`=OLD.`rowid`; END",
+          "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_Entity21_AFTER_UPDATE AFTER UPDATE ON `Entity13_V2` BEGIN INSERT INTO `Entity21`(`docid`, `name`, `addedInV1`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`addedInV1`); END",
+          "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_Entity21_AFTER_INSERT AFTER INSERT ON `Entity13_V2` BEGIN INSERT INTO `Entity21`(`docid`, `name`, `addedInV1`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`addedInV1`); END"
+        ],
+        "tableName": "Entity21",
+        "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`name` TEXT, `addedInV1` INTEGER NOT NULL DEFAULT 1, content=`Entity13_V2`)",
+        "fields": [
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "addedInV1",
+            "columnName": "addedInV1",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "1"
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "rowid"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "ftsVersion": "FTS4",
+        "ftsOptions": {
+          "tokenizer": "simple",
+          "tokenizerArgs": [],
+          "contentTable": "",
+          "languageIdColumnName": "",
+          "matchInfo": "FTS4",
+          "notIndexedColumns": [],
+          "prefixSizes": [],
+          "preferredOrder": "ASC"
+        },
+        "contentSyncTriggers": [],
+        "tableName": "Entity22",
+        "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`name` TEXT, `addedInV1` INTEGER NOT NULL DEFAULT 1)",
+        "fields": [
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "addedInV1",
+            "columnName": "addedInV1",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "1"
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "rowid"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "ftsVersion": "FTS3",
+        "ftsOptions": {
+          "tokenizer": "simple",
+          "tokenizerArgs": [],
+          "contentTable": "",
+          "languageIdColumnName": "",
+          "matchInfo": "FTS4",
+          "notIndexedColumns": [],
+          "prefixSizes": [],
+          "preferredOrder": "ASC"
+        },
+        "contentSyncTriggers": [],
+        "tableName": "Entity23",
+        "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS3(`name` TEXT, `addedInV1` INTEGER NOT NULL DEFAULT 1, `addedInV2` INTEGER NOT NULL DEFAULT 2)",
+        "fields": [
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "addedInV1",
+            "columnName": "addedInV1",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "1"
+          },
+          {
+            "fieldPath": "addedInV2",
+            "columnName": "addedInV2",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "2"
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "rowid"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "ftsVersion": "FTS3",
+        "ftsOptions": {
+          "tokenizer": "simple",
+          "tokenizerArgs": [],
+          "contentTable": "",
+          "languageIdColumnName": "",
+          "matchInfo": "FTS4",
+          "notIndexedColumns": [],
+          "prefixSizes": [],
+          "preferredOrder": "ASC"
+        },
+        "contentSyncTriggers": [],
+        "tableName": "Entity24",
+        "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS3(`name` TEXT, `addedInV1` INTEGER NOT NULL DEFAULT 1)",
+        "fields": [
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "addedInV1",
+            "columnName": "addedInV1",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "1"
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "rowid"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [],
+        "foreignKeys": []
       }
     ],
     "views": [],
     "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, '083a4e91debef71d5d900ca2cf0baccf')"
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9479b874f83849f2d740bdbb874b449a')"
     ]
   }
 }
\ No newline at end of file
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationDb.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationDb.java
index a0063b9..997a9d1 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationDb.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationDb.java
@@ -28,6 +28,9 @@
 import androidx.room.DeleteTable;
 import androidx.room.Entity;
 import androidx.room.ForeignKey;
+import androidx.room.Fts3;
+import androidx.room.Fts4;
+import androidx.room.FtsOptions;
 import androidx.room.Index;
 import androidx.room.PrimaryKey;
 import androidx.room.Query;
@@ -59,7 +62,11 @@
                 AutoMigrationDb.Entity16.class,
                 AutoMigrationDb.Entity17.class,
                 AutoMigrationDb.Entity19_V2.class,
-                AutoMigrationDb.Entity20_V2.class
+                AutoMigrationDb.Entity20_V2.class,
+                AutoMigrationDb.Entity21.class,
+                AutoMigrationDb.Entity22.class,
+                AutoMigrationDb.Entity23.class,
+                AutoMigrationDb.Entity24.class
         },
         autoMigrations = {
                 @AutoMigration(
@@ -336,6 +343,58 @@
         public int addedInV2;
     }
 
+    /**
+     * The content table of this FTS table has been renamed from Entity13 to Entity13_V2.
+     */
+    @Entity
+    @Fts4(contentEntity = Entity13_V2.class)
+    static class Entity21 {
+        public static final String TABLE_NAME = "Entity21";
+        @PrimaryKey
+        public int rowid;
+        public String name;
+        @ColumnInfo(defaultValue = "1")
+        public int addedInV1;
+    }
+
+    /**
+     * Change the options of the table from FTS3 to FTS4.
+     */
+    @Entity
+    @Fts4(matchInfo = FtsOptions.MatchInfo.FTS4)
+    static class Entity22 {
+        public static final String TABLE_NAME = "Entity22";
+        @PrimaryKey
+        public int rowid;
+        public String name;
+        @ColumnInfo(defaultValue = "1")
+        public int addedInV1;
+    }
+
+    @Entity
+    @Fts3
+    static class Entity23 {
+        public static final String TABLE_NAME = "Entity23";
+        @PrimaryKey
+        public int rowid;
+        public String name;
+        @ColumnInfo(defaultValue = "1")
+        public int addedInV1;
+        @ColumnInfo(defaultValue = "2")
+        public int addedInV2;
+    }
+
+    @Entity
+    @Fts3
+    static class Entity24 {
+        public static final String TABLE_NAME = "Entity24";
+        @PrimaryKey
+        public int rowid;
+        public String name;
+        @ColumnInfo(defaultValue = "1")
+        public int addedInV1;
+    }
+
     @Dao
     interface AutoMigrationDao {
         @Query("SELECT * from Entity1 ORDER BY id ASC")
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationTest.java
index 7a45a06..fa6fa9d 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationTest.java
@@ -16,11 +16,16 @@
 
 package androidx.room.integration.testapp.migration;
 
+import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.fail;
 
+import android.database.sqlite.SQLiteException;
+
+import androidx.annotation.NonNull;
 import androidx.room.Room;
+import androidx.room.migration.Migration;
 import androidx.room.testing.MigrationTestHelper;
 import androidx.sqlite.db.SupportSQLiteDatabase;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -54,6 +59,9 @@
         db.close();
     }
 
+    /**
+     * Tests the case where a non existent auto migration is called.
+     */
     @Test
     public void testBadAutoMigrationInput() throws IOException {
         try (SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1)) {
@@ -85,12 +93,37 @@
         helper.runMigrationsAndValidate(
                 TEST_DB,
                 2,
-                true,
-                autoMigrationDbV2.getAutoGeneratedMigration(1, 2)
+                true
         );
         assertThat(autoMigrationDbV2.dao().getAllEntity1s().size(), is(1));
     }
 
+    /**
+     * Verifies that the user defined migration is selected over using an autoMigration.
+     */
+    @Test
+    public void goFromV1ToV2WithUserDefinedMigration() throws IOException {
+        try (SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1)) {
+            db.execSQL("INSERT INTO Entity1 (id, name) VALUES (1, 'row1')");
+        }
+
+        try {
+            AutoMigrationDb autoMigrationDbV2 = Room.databaseBuilder(
+                    InstrumentationRegistry.getInstrumentation().getTargetContext(),
+                    AutoMigrationDb.class, TEST_DB).addMigrations(MIGRATION_1_2).build();
+            autoMigrationDbV2.getOpenHelper().getWritableDatabase(); // trigger open
+            helper.closeWhenFinished(autoMigrationDbV2);
+
+            helper.runMigrationsAndValidate(
+                    TEST_DB,
+                    2,
+                    true
+            );
+        } catch (SQLiteException exception) {
+            assertThat(exception.getMessage(), containsString("no such table: Entity0"));
+        }
+    }
+
     private AutoMigrationDb getLatestDb() {
         AutoMigrationDb db = Room.databaseBuilder(
                 InstrumentationRegistry.getInstrumentation().getTargetContext(),
@@ -99,4 +132,12 @@
         helper.closeWhenFinished(db);
         return db;
     }
+
+    private static final Migration MIGRATION_1_2 = new Migration(1, 2) {
+        @Override
+        public void migrate(@NonNull SupportSQLiteDatabase database) {
+            database.execSQL("ALTER TABLE `Entity0` ADD COLUMN `addedInV2` INTEGER NOT NULL "
+                    + "DEFAULT 2");
+        }
+    };
 }