Handling new table addition schema changes.
In the current implementation, the AutoMigration code will be able to detect the addition of a new table to a schema. It will also be able to detect if a table has been deleted (or renamed, can't difference between these two in this implementation). In this case, the processor will output an error message that includes the name of the table in reference. In the follow-up CLs to this CL, we will begin handling complex schema changes.
Test: Added testing for new column addition in valid and invalid cases for the AutoMigrationProcessorTest.kt, AutoMigrationWriterTest.kt, SchemaDifferTest.kt
Bug: 180119333
Change-Id: Ifcfaf8c118a340bb3a0a5e81a0fedc8b797c2273
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/AutoMigrationProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/AutoMigrationProcessor.kt
index adeeeb5..b9d992c 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/AutoMigrationProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/AutoMigrationProcessor.kt
@@ -104,7 +104,8 @@
element = element,
from = fromSchemaBundle.version,
to = toSchemaBundle.version,
- addedColumns = schemaDiff.added
+ addedColumns = schemaDiff.addedColumn,
+ addedTables = schemaDiff.addedTable
)
}
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 7773547..05a3b6a 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
@@ -850,6 +850,11 @@
"renamed. This change is not currently supported by AutoMigration."
}
+ fun removedOrRenamedTableFound(tableName: String): String {
+ return "Table '$tableName' has been either removed or " +
+ "renamed. This change is not currently supported by AutoMigration."
+ }
+
val AUTO_MIGRATION_FOUND_BUT_EXPORT_SCHEMA_OFF = "Cannot create auto-migrations when export " +
"schema is OFF."
}
\ No newline at end of file
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 80435f6..a0a327d 100644
--- a/room/compiler/src/main/kotlin/androidx/room/util/SchemaDiffer.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/util/SchemaDiffer.kt
@@ -17,7 +17,6 @@
package androidx.room.util
import androidx.room.migration.bundle.DatabaseBundle
-import androidx.room.migration.bundle.EntityBundle
import androidx.room.processor.ProcessorErrors
import androidx.room.vo.AutoMigrationResult
@@ -30,9 +29,11 @@
* Contains the added, changed and removed columns detected.
*/
data class SchemaDiffResult(
- val added: List<AutoMigrationResult.AddedColumn>,
- val changed: List<AutoMigrationResult.ChangedColumn>,
- val removed: List<AutoMigrationResult.RemovedColumn>
+ val addedColumn: MutableList<AutoMigrationResult.AddedColumn>,
+ val changedColumn: List<AutoMigrationResult.ChangedColumn>,
+ val removedColumn: List<AutoMigrationResult.RemovedColumn>,
+ val addedTable: List<AutoMigrationResult.AddedTable>,
+ val removedTable: List<AutoMigrationResult.RemovedTable>
)
/**
@@ -53,8 +54,8 @@
* @return the AutoMigrationResult containing the schema changes detected
*/
fun diffSchemas(): SchemaDiffResult {
- val addedTables = mutableListOf<EntityBundle>()
- val removedTables = mutableListOf<EntityBundle>()
+ val addedTables = mutableListOf<AutoMigrationResult.AddedTable>()
+ val removedTables = mutableListOf<AutoMigrationResult.RemovedTable>()
val addedColumns = mutableListOf<AutoMigrationResult.AddedColumn>()
val changedColumns = mutableListOf<AutoMigrationResult.ChangedColumn>()
@@ -65,7 +66,7 @@
fromSchemaBundle.entitiesByTableName.forEach { v1Table ->
val v2Table = toSchemaBundle.entitiesByTableName[v1Table.key]
if (v2Table == null) {
- removedTables.add(v1Table.value)
+ removedTables.add(AutoMigrationResult.RemovedTable(v1Table.value))
} else {
val v1Columns = v1Table.value.fieldsByColumnName
val v2Columns = v2Table.fieldsByColumnName
@@ -96,7 +97,7 @@
toSchemaBundle.entitiesByTableName.forEach { v2Table ->
val v1Table = fromSchemaBundle.entitiesByTableName[v2Table.key]
if (v1Table == null) {
- addedTables.add(v2Table.value)
+ addedTables.add(AutoMigrationResult.AddedTable(v2Table.value))
} else {
val v2Columns = v2Table.value.fieldsByColumnName
val v1Columns = v1Table.fieldsByColumnName
@@ -104,7 +105,7 @@
val match = v1Columns[v2Column.key]
if (match == null) {
if (v2Column.value.isNonNull && v2Column.value.defaultValue == null) {
- throw DiffException(
+ diffError(
ProcessorErrors.newNotNullColumnMustHaveDefaultValue(v2Column.key)
)
}
@@ -121,15 +122,17 @@
if (changedColumns.isNotEmpty()) {
changedColumns.forEach { changedColumn ->
- throw DiffException(
+ diffError(
ProcessorErrors.columnWithChangedSchemaFound(
changedColumn.originalFieldBundle.columnName
)
)
}
- } else if (removedColumns.isNotEmpty()) {
+ }
+
+ if (removedColumns.isNotEmpty()) {
removedColumns.forEach { removedColumn ->
- throw DiffException(
+ diffError(
ProcessorErrors.removedOrRenamedColumnFound(
removedColumn.fieldBundle.columnName
)
@@ -137,10 +140,26 @@
}
}
+ if (removedTables.isNotEmpty()) {
+ removedTables.forEach { removedTable ->
+ diffError(
+ ProcessorErrors.removedOrRenamedTableFound(
+ removedTable.entityBundle.tableName
+ )
+ )
+ }
+ }
+
return SchemaDiffResult(
- added = addedColumns,
- changed = changedColumns,
- removed = removedColumns
+ addedColumn = addedColumns,
+ changedColumn = changedColumns,
+ removedColumn = removedColumns,
+ addedTable = addedTables,
+ removedTable = removedTables
)
}
+
+ private fun diffError(errorMsg: String) {
+ throw DiffException(errorMsg)
+ }
}
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/AutoMigrationResult.kt b/room/compiler/src/main/kotlin/androidx/room/vo/AutoMigrationResult.kt
index 6931854..91d4bd6 100644
--- a/room/compiler/src/main/kotlin/androidx/room/vo/AutoMigrationResult.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/AutoMigrationResult.kt
@@ -17,6 +17,7 @@
package androidx.room.vo
import androidx.room.compiler.processing.XTypeElement
+import androidx.room.migration.bundle.EntityBundle
import androidx.room.migration.bundle.FieldBundle
import com.squareup.javapoet.ClassName
@@ -24,7 +25,8 @@
val element: XTypeElement,
val from: Int?,
val to: Int?,
- val addedColumns: List<AddedColumn>
+ val addedColumns: List<AddedColumn>,
+ val addedTables: List<AddedTable>
) {
val implTypeName: ClassName by lazy {
@@ -60,4 +62,18 @@
* renamed.
*/
data class RemovedColumn(val tableName: String, val fieldBundle: FieldBundle)
+
+ /**
+ * Stores the table that was added to a database in a newer version.
+ */
+ data class AddedTable(val entityBundle: EntityBundle)
+
+ /**
+ * Stores the table that was present in the old version of a database but is not present in a
+ * new version of the same database, either because it was removed or renamed.
+ *
+ * In the current implementation, we cannot differ between whether the table was removed or
+ * renamed.
+ */
+ data class RemovedTable(val entityBundle: EntityBundle)
}
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 3324771..c6a7b9c 100644
--- a/room/compiler/src/main/kotlin/androidx/room/writer/AutoMigrationWriter.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/writer/AutoMigrationWriter.kt
@@ -76,6 +76,9 @@
* @param migrateFunctionBuilder Builder for the migrate() function to be generated
*/
private fun addAutoMigrationResultToMigrate(migrateFunctionBuilder: MethodSpec.Builder) {
+ if (autoMigrationResult.addedTables.isNotEmpty()) {
+ addNewTableStatements(migrateFunctionBuilder)
+ }
if (autoMigrationResult.addedColumns.isNotEmpty()) {
addNewColumnStatements(migrateFunctionBuilder)
}
@@ -91,11 +94,11 @@
autoMigrationResult.addedColumns.forEach {
val addNewColumnSql = buildString {
append(
- "ALTER TABLE '${it.tableName}' ADD COLUMN `${it.fieldBundle.columnName}` " +
+ "ALTER TABLE `${it.tableName}` ADD COLUMN `${it.fieldBundle.columnName}` " +
"${it.fieldBundle.affinity} "
)
if (it.fieldBundle.isNonNull) {
- append("NOT NULL DEFAULT '${it.fieldBundle.defaultValue}'")
+ append("NOT NULL DEFAULT `${it.fieldBundle.defaultValue}`")
} else {
append("DEFAULT NULL")
}
@@ -105,6 +108,20 @@
}
/**
+ * Adds the appropriate SQL statements for adding new tables to a database, into the
+ * generated migrate() function.
+ *
+ * @param migrateFunctionBuilder Builder for the migrate() function to be generated
+ */
+ private fun addNewTableStatements(migrateFunctionBuilder: MethodSpec.Builder) {
+ autoMigrationResult.addedTables.forEach { addedTable ->
+ migrateFunctionBuilder.addStatement(
+ "database.execSQL($S)", addedTable.entityBundle.createTable()
+ )
+ }
+ }
+
+ /**
* Builds the constructor of the generated AutoMigration.
*
* @return The constructor of the generated AutoMigration
diff --git a/room/compiler/src/test/data/autoMigrationWriter/output/ValidAutoMigrationWithDefault.java b/room/compiler/src/test/data/autoMigrationWriter/output/ValidAutoMigrationWithDefault.java
index 8a5dd5b1..b0882e0 100644
--- a/room/compiler/src/test/data/autoMigrationWriter/output/ValidAutoMigrationWithDefault.java
+++ b/room/compiler/src/test/data/autoMigrationWriter/output/ValidAutoMigrationWithDefault.java
@@ -17,7 +17,7 @@
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
- database.execSQL("ALTER TABLE 'Song' ADD COLUMN `artistId` INTEGER NOT NULL DEFAULT '0'");
+ database.execSQL("ALTER TABLE `Song` ADD COLUMN `artistId` INTEGER NOT NULL DEFAULT `0`");
onPostMigrate(database);
}
}
diff --git a/room/compiler/src/test/data/autoMigrationWriter/output/ValidAutoMigrationWithNewTableAdded.java b/room/compiler/src/test/data/autoMigrationWriter/output/ValidAutoMigrationWithNewTableAdded.java
new file mode 100644
index 0000000..3589a08
--- /dev/null
+++ b/room/compiler/src/test/data/autoMigrationWriter/output/ValidAutoMigrationWithNewTableAdded.java
@@ -0,0 +1,25 @@
+package foo.bar;
+
+import androidx.annotation.NonNull;
+import androidx.room.migration.AutoMigrationCallback;
+import androidx.room.migration.Migration;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+import java.lang.Override;
+import java.lang.SuppressWarnings;
+import javax.annotation.processing.Generated;
+
+@Generated("androidx.room.RoomProcessor")
+@SuppressWarnings({"unchecked", "deprecation"})
+class ValidAutoMigrationWithNewTableAdded_Impl extends Migration implements AutoMigrationCallback {
+ public ValidAutoMigrationWithNewTableAdded_Impl() {
+ super(1, 2);
+ }
+
+ @Override
+ public void migrate(@NonNull SupportSQLiteDatabase database) {
+ database.execSQL("CREATE TABLE IF NOT EXISTS `Artist` (`artistId` INTEGER NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`artistId`))");
+ database.execSQL("CREATE TABLE IF NOT EXISTS `Album` (`albumId` INTEGER NOT NULL, PRIMARY KEY(`albumId`))");
+ database.execSQL("ALTER TABLE `Song` ADD COLUMN `songId` INTEGER DEFAULT NULL");
+ onPostMigrate(database);
+ }
+}
diff --git a/room/compiler/src/test/data/autoMigrationWriter/output/ValidAutoMigrationWithoutDefault.java b/room/compiler/src/test/data/autoMigrationWriter/output/ValidAutoMigrationWithoutDefault.java
index 5747c04..93eb47d 100644
--- a/room/compiler/src/test/data/autoMigrationWriter/output/ValidAutoMigrationWithoutDefault.java
+++ b/room/compiler/src/test/data/autoMigrationWriter/output/ValidAutoMigrationWithoutDefault.java
@@ -17,7 +17,7 @@
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
- database.execSQL("ALTER TABLE 'Song' ADD COLUMN `artistId` INTEGER DEFAULT NULL");
+ database.execSQL("ALTER TABLE `Song` ADD COLUMN `artistId` INTEGER DEFAULT NULL");
onPostMigrate(database);
}
}
diff --git a/room/compiler/src/test/kotlin/androidx/room/util/SchemaDifferTest.kt b/room/compiler/src/test/kotlin/androidx/room/util/SchemaDifferTest.kt
index 6be4eea..0dd90dc 100644
--- a/room/compiler/src/test/kotlin/androidx/room/util/SchemaDifferTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/util/SchemaDifferTest.kt
@@ -34,7 +34,7 @@
fromSchemaBundle = from.database,
toSchemaBundle = toColumnAddedWithColumnInfoDefaultValue.database
).diffSchemas()
- assertThat(schemaDiffResult.added[0].fieldBundle.columnName).isEqualTo("artistId")
+ assertThat(schemaDiffResult.addedColumn[0].fieldBundle.columnName).isEqualTo("artistId")
}
@Test
@@ -53,6 +53,16 @@
}
@Test
+ fun testTableAddedWithColumnInfoDefaultValue() {
+ val schemaDiffResult = SchemaDiffer(
+ fromSchemaBundle = from.database,
+ toSchemaBundle = toTableAddedWithColumnInfoDefaultValue.database
+ ).diffSchemas()
+ assertThat(schemaDiffResult.addedTable[0].entityBundle.tableName).isEqualTo("Artist")
+ assertThat(schemaDiffResult.addedTable[1].entityBundle.tableName).isEqualTo("Album")
+ }
+
+ @Test
fun testColumnRenamed() {
try {
SchemaDiffer(
@@ -204,7 +214,7 @@
* The length column is removed from the first version. No other changes made.
*
*/
- val toColumnRemoved = SchemaBundle(
+ private val toColumnRemoved = SchemaBundle(
2,
DatabaseBundle(
2,
@@ -248,7 +258,7 @@
* Room will put null for that default value in the exported schema. In this case we
* can't migrate.
*/
- val toColumnAddedWithNoDefaultValue = SchemaBundle(
+ private val toColumnAddedWithNoDefaultValue = SchemaBundle(
2,
DatabaseBundle(
2,
@@ -307,7 +317,7 @@
*/
// TODO: We currently do not support column renames as we can't detect rename or deletion
// yet.
- val toColumnRenamed = SchemaBundle(
+ private val toColumnRenamed = SchemaBundle(
2,
DatabaseBundle(
2,
@@ -405,4 +415,147 @@
mutableListOf()
)
)
+
+ private val toTableAddedWithColumnInfoDefaultValue = SchemaBundle(
+ 1,
+ DatabaseBundle(
+ 1,
+ "",
+ mutableListOf(
+ EntityBundle(
+ "Song",
+ "CREATE TABLE IF NOT EXISTS `Song` (`id` INTEGER NOT NULL, " +
+ "`title` TEXT NOT NULL, `length` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ listOf(
+ FieldBundle(
+ "id",
+ "id",
+ "INTEGER",
+ true,
+ "1"
+ ),
+ FieldBundle(
+ "title",
+ "title",
+ "TEXT",
+ true,
+ ""
+ ),
+ FieldBundle(
+ "length",
+ "length",
+ "INTEGER",
+ true,
+ "1"
+ )
+ ),
+ PrimaryKeyBundle(
+ false,
+ mutableListOf("id")
+ ),
+ mutableListOf(),
+ mutableListOf()
+ ),
+ EntityBundle(
+ "Artist",
+ "CREATE TABLE IF NOT EXISTS `Artist` (`artistId` INTEGER NOT NULL, `name` " +
+ "TEXT NOT NULL, PRIMARY KEY(`artistId`))",
+ listOf(
+ FieldBundle(
+ "artistId",
+ "artistId",
+ "INTEGER",
+ true,
+ "1"
+ )
+ ),
+ PrimaryKeyBundle(true, listOf("artistId")),
+ listOf(),
+ listOf()
+ ),
+ EntityBundle(
+ "Album",
+ "CREATE TABLE IF NOT EXISTS `Album` (`albumId` INTEGER NOT NULL, `name` TEXT " +
+ "NOT NULL, PRIMARY KEY(`albumId`))",
+ listOf(
+ FieldBundle(
+ "albumId",
+ "albumId",
+ "INTEGER",
+ true,
+ "1"
+ )
+ ),
+ PrimaryKeyBundle(true, listOf("albumId")),
+ listOf(),
+ listOf()
+ )
+ ),
+ mutableListOf(),
+ mutableListOf()
+ )
+ )
+
+ val toTableAddedWithNoDefaultValue = SchemaBundle(
+ 1,
+ DatabaseBundle(
+ 1,
+ "",
+ mutableListOf(
+ EntityBundle(
+ "Song",
+ "CREATE TABLE IF NOT EXISTS `Song` (`id` INTEGER NOT NULL, " +
+ "`title` TEXT NOT NULL, `length` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ listOf(
+ FieldBundle(
+ "id",
+ "id",
+ "INTEGER",
+ true,
+ "1"
+ ),
+ FieldBundle(
+ "title",
+ "title",
+ "TEXT",
+ true,
+ ""
+ ),
+ FieldBundle(
+ "length",
+ "length",
+ "INTEGER",
+ true,
+ "1"
+ )
+ ),
+ PrimaryKeyBundle(
+ false,
+ mutableListOf("id")
+ ),
+ mutableListOf(),
+ mutableListOf()
+ ),
+ EntityBundle(
+ "Album",
+ "CREATE TABLE IF NOT EXISTS `Album` (`id` INTEGER NOT NULL, `name` TEXT NOT " +
+ "NULL, PRIMARY KEY(`id`))",
+ listOf(
+ FieldBundle(
+ "albumId",
+ "albumId",
+ "INTEGER",
+ true,
+ null
+ )
+ ),
+ PrimaryKeyBundle(true, listOf("id")),
+ listOf(),
+ listOf()
+ )
+ ),
+ mutableListOf(),
+ mutableListOf()
+ )
+ )
}
\ No newline at end of file
diff --git a/room/compiler/src/test/kotlin/androidx/room/writer/AutoMigrationWriterTest.kt b/room/compiler/src/test/kotlin/androidx/room/writer/AutoMigrationWriterTest.kt
index 4176a0b..5379314 100644
--- a/room/compiler/src/test/kotlin/androidx/room/writer/AutoMigrationWriterTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/writer/AutoMigrationWriterTest.kt
@@ -19,7 +19,9 @@
import androidx.room.compiler.processing.XElement
import androidx.room.compiler.processing.util.Source
import androidx.room.compiler.processing.util.runProcessorTest
+import androidx.room.migration.bundle.EntityBundle
import androidx.room.migration.bundle.FieldBundle
+import androidx.room.migration.bundle.PrimaryKeyBundle
import androidx.room.vo.AutoMigrationResult
import loadTestSource
import org.junit.Test
@@ -61,7 +63,8 @@
"0"
)
)
- )
+ ),
+ addedTables = listOf()
)
AutoMigrationWriter(mock(XElement::class.java), autoMigrationResultWithNewAddedColumn)
.write(invocation.processingEnv)
@@ -110,7 +113,8 @@
""
)
)
- )
+ ),
+ addedTables = listOf()
)
AutoMigrationWriter(mock(XElement::class.java), autoMigrationResultWithNewAddedColumn)
.write(invocation.processingEnv)
@@ -125,4 +129,95 @@
}
}
}
+
+ @Test
+ fun validAutoMigrationWithNewTableAdded() {
+ val source = Source.java(
+ "foo.bar.ValidAutoMigrationWithoutDefault",
+ """
+ package foo.bar;
+ import androidx.room.migration.AutoMigrationCallback;
+ import androidx.room.AutoMigration;
+ import androidx.sqlite.db.SupportSQLiteDatabase;
+ @AutoMigration(from=1, to=2)
+ interface ValidAutoMigrationWithNewTableAdded extends AutoMigrationCallback {
+ @Override
+ void onPostMigrate(SupportSQLiteDatabase db);
+ }
+ """.trimIndent()
+ )
+
+ runProcessorTest(listOf(source)) { invocation ->
+ val autoMigrationResultWithNewTableAdded = AutoMigrationResult(
+ element = invocation.processingEnv.requireTypeElement(
+ "foo.bar.ValidAutoMigrationWithNewTableAdded"
+ ),
+ from = 1,
+ to = 2,
+ addedColumns = listOf(
+ AutoMigrationResult.AddedColumn(
+ "Song",
+ FieldBundle(
+ "songId",
+ "songId",
+ "INTEGER",
+ false,
+ ""
+ )
+ )
+ ),
+ addedTables = listOf(
+ AutoMigrationResult.AddedTable(
+ EntityBundle(
+ "Artist",
+ "CREATE TABLE IF NOT EXISTS `Artist` (`artistId` INTEGER NOT NULL, " +
+ "`name` TEXT NOT NULL, PRIMARY KEY(`artistId`))",
+ listOf(
+ FieldBundle(
+ "artistId",
+ "artistId",
+ "INTEGER",
+ true,
+ "1"
+ )
+ ),
+ PrimaryKeyBundle(true, listOf("artistId")),
+ listOf(),
+ listOf()
+ ),
+ ),
+ AutoMigrationResult.AddedTable(
+ EntityBundle(
+ "Album",
+ "CREATE TABLE IF NOT EXISTS `Album` (`albumId` INTEGER NOT NULL, " +
+ "PRIMARY KEY(`albumId`))",
+ listOf(
+ FieldBundle(
+ "albumId",
+ "albumId",
+ "INTEGER",
+ true,
+ "1"
+ )
+ ),
+ PrimaryKeyBundle(true, listOf("albumId")),
+ listOf(),
+ listOf()
+ )
+ )
+ )
+ )
+ AutoMigrationWriter(mock(XElement::class.java), autoMigrationResultWithNewTableAdded)
+ .write(invocation.processingEnv)
+
+ invocation.assertCompilationResult {
+ generatedSource(
+ loadTestSource(
+ "autoMigrationWriter/output/ValidAutoMigrationWithNewTableAdded.java",
+ "foo.bar.ValidAutoMigrationWithNewTableAdded_Impl"
+ )
+ )
+ }
+ }
+ }
}