blob: a0a327dc05957c5d962507e98188f5fc94097802 [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.util
import androidx.room.migration.bundle.DatabaseBundle
import androidx.room.processor.ProcessorErrors
import androidx.room.vo.AutoMigrationResult
/**
* This exception should be thrown to abandon processing an @AutoMigration.
*/
class DiffException(val errorMessage: String) : RuntimeException(errorMessage)
/**
* Contains the added, changed and removed columns detected.
*/
data class SchemaDiffResult(
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>
)
/**
* Receives the two bundles, diffs and returns a @SchemaDiffResult.
*
* Throws an @AutoMigrationException with a detailed error message when an AutoMigration cannot
* be generated.
*/
class SchemaDiffer(
val fromSchemaBundle: DatabaseBundle,
val toSchemaBundle: DatabaseBundle
) {
/**
* Compares the two versions of the database based on the schemas provided, and detects
* schema changes.
*
* @return the AutoMigrationResult containing the schema changes detected
*/
fun diffSchemas(): SchemaDiffResult {
val addedTables = mutableListOf<AutoMigrationResult.AddedTable>()
val removedTables = mutableListOf<AutoMigrationResult.RemovedTable>()
val addedColumns = mutableListOf<AutoMigrationResult.AddedColumn>()
val changedColumns = mutableListOf<AutoMigrationResult.ChangedColumn>()
val removedColumns = mutableListOf<AutoMigrationResult.RemovedColumn>()
// Check going from the original version of the schema to the new version for changed and
// removed columns/tables
fromSchemaBundle.entitiesByTableName.forEach { v1Table ->
val v2Table = toSchemaBundle.entitiesByTableName[v1Table.key]
if (v2Table == null) {
removedTables.add(AutoMigrationResult.RemovedTable(v1Table.value))
} else {
val v1Columns = v1Table.value.fieldsByColumnName
val v2Columns = v2Table.fieldsByColumnName
v1Columns.entries.forEach { v1Column ->
val match = v2Columns[v1Column.key]
if (match != null && !match.isSchemaEqual(v1Column.value)) {
changedColumns.add(
AutoMigrationResult.ChangedColumn(
v1Table.key,
v1Column.value,
match
)
)
} else if (match == null) {
removedColumns.add(
AutoMigrationResult.RemovedColumn(
v1Table.key,
v1Column.value
)
)
}
}
}
}
// Check going from the new version of the schema to the original version for added
// tables/columns. Skip the columns with the same name as the previous loop would have
// processed them already.
toSchemaBundle.entitiesByTableName.forEach { v2Table ->
val v1Table = fromSchemaBundle.entitiesByTableName[v2Table.key]
if (v1Table == null) {
addedTables.add(AutoMigrationResult.AddedTable(v2Table.value))
} else {
val v2Columns = v2Table.value.fieldsByColumnName
val v1Columns = v1Table.fieldsByColumnName
v2Columns.entries.forEach { v2Column ->
val match = v1Columns[v2Column.key]
if (match == null) {
if (v2Column.value.isNonNull && v2Column.value.defaultValue == null) {
diffError(
ProcessorErrors.newNotNullColumnMustHaveDefaultValue(v2Column.key)
)
}
addedColumns.add(
AutoMigrationResult.AddedColumn(
v2Table.key,
v2Column.value
)
)
}
}
}
}
if (changedColumns.isNotEmpty()) {
changedColumns.forEach { changedColumn ->
diffError(
ProcessorErrors.columnWithChangedSchemaFound(
changedColumn.originalFieldBundle.columnName
)
)
}
}
if (removedColumns.isNotEmpty()) {
removedColumns.forEach { removedColumn ->
diffError(
ProcessorErrors.removedOrRenamedColumnFound(
removedColumn.fieldBundle.columnName
)
)
}
}
if (removedTables.isNotEmpty()) {
removedTables.forEach { removedTable ->
diffError(
ProcessorErrors.removedOrRenamedTableFound(
removedTable.entityBundle.tableName
)
)
}
}
return SchemaDiffResult(
addedColumn = addedColumns,
changedColumn = changedColumns,
removedColumn = removedColumns,
addedTable = addedTables,
removedTable = removedTables
)
}
private fun diffError(errorMsg: String) {
throw DiffException(errorMsg)
}
}