blob: 8f4b05ce25bea233711abec9281b3195b0dfc6a5 [file] [log] [blame]
/*
* Copyright 2021 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 androidx.room.writer
import androidx.annotation.NonNull
import androidx.room.compiler.processing.XElement
import androidx.room.compiler.processing.addOriginatingElement
import androidx.room.ext.L
import androidx.room.ext.RoomTypeNames
import androidx.room.ext.S
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.AutoMigration
import com.squareup.javapoet.FieldSpec
import com.squareup.javapoet.MethodSpec
import com.squareup.javapoet.ParameterSpec
import com.squareup.javapoet.TypeName
import com.squareup.javapoet.TypeSpec
import javax.lang.model.element.Modifier
/**
* Writes the implementation of migrations that were annotated with @AutoMigration.
*/
class AutoMigrationWriter(
private val dbElement: XElement,
val autoMigration: AutoMigration
) : ClassWriter(autoMigration.implTypeName) {
private val addedColumns = autoMigration.schemaDiff.addedColumns
private val addedTables = autoMigration.schemaDiff.addedTables
private val renamedTables = autoMigration.schemaDiff.renamedTables
private val complexChangedTables = autoMigration.schemaDiff.complexChangedTables
private val deletedTables = autoMigration.schemaDiff.deletedTables
override fun createTypeSpecBuilder(): TypeSpec.Builder {
val builder = TypeSpec.classBuilder(autoMigration.implTypeName)
builder.apply {
addOriginatingElement(dbElement)
superclass(RoomTypeNames.MIGRATION)
if (autoMigration.specClassName != null) {
val callbackField =
FieldSpec.builder(
RoomTypeNames.AUTO_MIGRATION_SPEC,
"callback",
Modifier.PRIVATE,
Modifier.FINAL
).apply {
if (!autoMigration.isSpecProvided) {
initializer("new $T()", autoMigration.specClassName)
}
}
builder.addField(callbackField.build())
}
addMethod(createConstructor())
addMethod(createMigrateMethod())
}
return builder
}
/**
* Builds the constructor of the generated AutoMigration.
*
* @return The constructor of the generated AutoMigration
*/
private fun createConstructor(): MethodSpec {
return MethodSpec.constructorBuilder().apply {
addModifiers(Modifier.PUBLIC)
addStatement(
"super($L, $L)",
autoMigration.from,
autoMigration.to
)
if (autoMigration.isSpecProvided) {
addParameter(
ParameterSpec.builder(
RoomTypeNames.AUTO_MIGRATION_SPEC,
"callback"
).addAnnotation(NonNull::class.java).build()
)
addStatement("this.callback = callback")
}
}.build()
}
private fun createMigrateMethod(): MethodSpec? {
val migrateFunctionBuilder: MethodSpec.Builder = MethodSpec.methodBuilder("migrate")
.apply {
addParameter(
ParameterSpec.builder(
SupportDbTypeNames.DB,
"database"
).addAnnotation(NonNull::class.java).build()
)
addAnnotation(Override::class.java)
addModifiers(Modifier.PUBLIC)
returns(TypeName.VOID)
addMigrationStatements(this)
if (autoMigration.specClassName != null) {
addStatement("callback.onPostMigrate(database)")
}
}
return migrateFunctionBuilder.build()
}
/**
* Takes the changes provided in the {@link AutoMigrationResult} which are differences detected
* between the two versions of the same database, and converts them to the appropriate
* sequence of SQL statements that migrate the database from one version to the other.
*
* @param migrateBuilder Builder for the migrate() function to be generated
*/
private fun addMigrationStatements(migrateBuilder: MethodSpec.Builder) {
addDropViewStatements(migrateBuilder)
addSimpleChangeStatements(migrateBuilder)
addComplexChangeStatements(migrateBuilder)
addRecreateViewStatements(migrateBuilder)
}
/**
* Adds SQL statements to drop all views of the database in the 'from' version.
*
* @param migrateBuilder Builder for the migrate() function to be generated
*/
private fun addDropViewStatements(migrateBuilder: MethodSpec.Builder) {
autoMigration.schemaDiff.fromViews.forEach { view ->
addDatabaseExecuteSqlStatement(migrateBuilder, "DROP VIEW ${view.viewName}")
}
}
/**
* Adds SQL statements to create all views of the database in the 'to' version.
*
* @param migrateBuilder Builder for the migrate() function to be generated
*/
private fun addRecreateViewStatements(migrateBuilder: MethodSpec.Builder) {
autoMigration.schemaDiff.toViews.forEach { view ->
addDatabaseExecuteSqlStatement(migrateBuilder, view.createView())
}
}
/**
* Adds SQL statements performing schema altering commands that are not directly supported by
* SQLite (e.g. foreign key changes). These changes are referred to as "complex" changes.
*
* @param migrateBuilder Builder for the migrate() function to be generated
*/
private fun addComplexChangeStatements(migrateBuilder: MethodSpec.Builder) {
// 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,
oldEntityBundle,
newEntityBundle,
renamedColumnsMap
) ->
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
* are referred to as "simple" changes.
*
* @param migrateBuilder Builder for the migrate() function to be generated
*/
private fun addSimpleChangeStatements(migrateBuilder: MethodSpec.Builder) {
addDeleteTableStatements(migrateBuilder)
addRenameTableStatements(migrateBuilder)
addNewColumnStatements(migrateBuilder)
addNewTableStatements(migrateBuilder)
}
/**
* Adds the SQL statements for creating a new table in the desired revised format of table.
*
* @param newTable Schema of the new table to be created
* @param migrateBuilder Builder for the migrate() function to be generated
*/
private fun addStatementsToCreateNewTable(
newTable: EntityBundle,
migrateBuilder: MethodSpec.Builder
) {
addDatabaseExecuteSqlStatement(
migrateBuilder,
newTable.createNewTable()
)
}
/**
* Adds the SQL statements for transferring the contents of the old table to the new version.
*
* @param oldTableName Name of the table in the old version of the database
* @param tableNameWithNewPrefix Name of the table with the '_new_' prefix added
* @param oldEntityBundle Entity bundle of the table in the old version of the database
* @param newEntityBundle Entity bundle of the table in the new version of the database
* @param renamedColumnsMap Map of the renamed columns of the table (new name -> old name)
* @param migrateBuilder Builder for the migrate() function to be generated
*/
private fun addStatementsToContentTransfer(
oldTableName: String,
tableNameWithNewPrefix: String,
oldEntityBundle: EntityBundle,
newEntityBundle: EntityBundle,
renamedColumnsMap: MutableMap<String, String>,
migrateBuilder: MethodSpec.Builder
) {
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)
}
addDatabaseExecuteSqlStatement(
migrateBuilder,
buildString {
append(
"INSERT INTO `$tableNameWithNewPrefix` " +
"(${newColumnSequence.joinToString(",")})" +
" SELECT ${oldColumnSequence.joinToString(",")} FROM " +
"`$oldTableName`",
)
}
)
}
/**
* Adds the SQL statements for dropping the table at the old version and renaming the
* temporary table to the name of the original table.
*
* @param oldTableName Name of the table in the old version of the database
* @param newTableName Name of the table in the new version of the database
* @param tableNameWithNewPrefix Name of the table with the '_new_' prefix added
* @param migrateBuilder Builder for the migrate() function to be generated
*/
private fun addStatementsToDropTableAndRenameTempTable(
oldTableName: String,
newTableName: String,
tableNameWithNewPrefix: String,
migrateBuilder: MethodSpec.Builder
) {
addDatabaseExecuteSqlStatement(
migrateBuilder,
"DROP TABLE `$oldTableName`"
)
addDatabaseExecuteSqlStatement(
migrateBuilder,
"ALTER TABLE `$tableNameWithNewPrefix` RENAME TO `$newTableName`"
)
}
/**
* Adds the SQL statements for recreating indexes.
*
* @param table The table the indexes of which will be recreated
* @param migrateBuilder Builder for the migrate() function to be generated
*/
private fun addStatementsToRecreateIndexes(
table: EntityBundle,
migrateBuilder: MethodSpec.Builder
) {
table.indices.forEach { index ->
addDatabaseExecuteSqlStatement(
migrateBuilder,
index.getCreateSql(table.tableName)
)
}
}
/**
* Adds the SQL statement for checking the foreign key constraints.
*
* @param tableName Name of the table
* @param migrateBuilder Builder for the migrate() function to be generated
*/
private fun addStatementsToCheckForeignKeyConstraint(
tableName: String,
migrateBuilder: MethodSpec.Builder
) {
addDatabaseExecuteSqlStatement(
migrateBuilder,
"PRAGMA foreign_key_check(`$tableName`)"
)
}
/**
* Adds the SQL statements for removing a table.
*
* @param migrateBuilder Builder for the migrate() function to be generated
*/
private fun addDeleteTableStatements(migrateBuilder: MethodSpec.Builder) {
deletedTables.forEach { tableName ->
val deleteTableSql = buildString {
append(
"DROP TABLE `$tableName`"
)
}
addDatabaseExecuteSqlStatement(
migrateBuilder,
deleteTableSql
)
}
}
/**
* Adds the SQL statements for renaming a table.
*
* @param migrateBuilder Builder for the migrate() function to be generated
*/
private fun addRenameTableStatements(migrateBuilder: MethodSpec.Builder) {
renamedTables.forEach { (oldName, newName) ->
val renameTableSql = buildString {
append(
"ALTER TABLE `$oldName` RENAME TO `$newName`"
)
}
addDatabaseExecuteSqlStatement(
migrateBuilder,
renameTableSql
)
}
}
/**
* Adds the SQL statements for adding new columns to a table.
*
* @param migrateBuilder Builder for the migrate() function to be generated
*/
private fun addNewColumnStatements(migrateBuilder: MethodSpec.Builder) {
addedColumns.forEach {
val addNewColumnSql = buildString {
append(
"ALTER TABLE `${it.value.tableName}` ADD COLUMN `${it.key}` " +
"${it.value.fieldBundle.affinity} "
)
if (it.value.fieldBundle.isNonNull) {
append("NOT NULL DEFAULT ${it.value.fieldBundle.defaultValue}")
} else {
append("DEFAULT NULL")
}
}
addDatabaseExecuteSqlStatement(
migrateBuilder,
addNewColumnSql
)
}
}
/**
* Adds the SQL statements for adding new tables to a database.
*
* @param migrateBuilder Builder for the migrate() function to be generated
*/
private fun addNewTableStatements(migrateBuilder: MethodSpec.Builder) {
addedTables.forEach { addedTable ->
addDatabaseExecuteSqlStatement(
migrateBuilder,
addedTable.entityBundle.createTable()
)
addStatementsToRecreateIndexes(addedTable.entityBundle, migrateBuilder)
}
}
/**
* Adds the given SQL statements into the generated migrate() function to be executed by the
* database.
*
* @param migrateBuilder Builder for the migrate() function to be generated
* @param sql The SQL statement to be executed by the database
*/
private fun addDatabaseExecuteSqlStatement(
migrateBuilder: MethodSpec.Builder,
sql: String
) {
migrateBuilder.addStatement(
"database.execSQL($S)",
sql
)
}
}