Skip to content

Commit

Permalink
refactor!: Move statementsRequiredForDatabaseMigration function fro…
Browse files Browse the repository at this point in the history
…m `SchemaUtils` to `MigrationUtils` (#2195)
  • Loading branch information
joc-a authored Aug 12, 2024
1 parent 44d484f commit 7c3df2f
Show file tree
Hide file tree
Showing 9 changed files with 200 additions and 176 deletions.
2 changes: 0 additions & 2 deletions exposed-core/api/exposed-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -2114,8 +2114,6 @@ public final class org/jetbrains/exposed/sql/SchemaUtils {
public final fun setSchema (Lorg/jetbrains/exposed/sql/Schema;Z)V
public static synthetic fun setSchema$default (Lorg/jetbrains/exposed/sql/SchemaUtils;Lorg/jetbrains/exposed/sql/Schema;ZILjava/lang/Object;)V
public final fun sortTablesByReferences (Ljava/lang/Iterable;)Ljava/util/List;
public final fun statementsRequiredForDatabaseMigration ([Lorg/jetbrains/exposed/sql/Table;Z)Ljava/util/List;
public static synthetic fun statementsRequiredForDatabaseMigration$default (Lorg/jetbrains/exposed/sql/SchemaUtils;[Lorg/jetbrains/exposed/sql/Table;ZILjava/lang/Object;)Ljava/util/List;
public final fun statementsRequiredToActualizeScheme ([Lorg/jetbrains/exposed/sql/Table;Z)Ljava/util/List;
public static synthetic fun statementsRequiredToActualizeScheme$default (Lorg/jetbrains/exposed/sql/SchemaUtils;[Lorg/jetbrains/exposed/sql/Table;ZILjava/lang/Object;)Ljava/util/List;
public final fun withDataBaseLock (Lorg/jetbrains/exposed/sql/Transaction;Lkotlin/jvm/functions/Function0;)V
Expand Down
163 changes: 0 additions & 163 deletions exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SchemaUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -613,17 +613,6 @@ object SchemaUtils {
return checkMissingAndUnmappedIndices(tables = tables, withLogs).flatMap { it.createStatement() }
}

/**
* Log Exposed table mappings <-> real database mapping problems and returns DDL Statements to fix them, including
* DROP/DELETE statements (unlike [checkMappingConsistence])
*/
private fun mappingConsistenceRequiredStatements(vararg tables: Table, withLogs: Boolean = true): List<String> {
return checkMissingIndices(tables = tables, withLogs).flatMap { it.createStatement() } +
checkUnmappedIndices(tables = tables, withLogs).flatMap { it.dropStatement() } +
checkExcessiveForeignKeyConstraints(tables = tables, withLogs).flatMap { it.dropStatement() } +
checkExcessiveIndices(tables = tables, withLogs).flatMap { it.dropStatement() }
}

/**
* Checks all [tables] for any that have more than one defined index and logs the findings. If found, this function
* also logs the SQL statements that can be used to drop these indices.
Expand Down Expand Up @@ -779,158 +768,6 @@ object SchemaUtils {
return toCreate.toList()
}

/**
* Checks all [tables] for any that have indices that are missing in the database but are defined in the code. If
* found, this function also logs the SQL statements that can be used to create these indices.
*
* @return List of indices that are missing and can be created.
*/
private fun checkMissingIndices(vararg tables: Table, withLogs: Boolean): List<Index> {
fun Collection<Index>.log(mainMessage: String) {
if (withLogs && isNotEmpty()) {
exposedLogger.warn(joinToString(prefix = "$mainMessage\n\t\t", separator = "\n\t\t"))
}
}

val fKeyConstraints = currentDialect.columnConstraints(*tables).keys
val existingIndices = currentDialect.existingIndices(*tables)

fun List<Index>.filterForeignKeys() = if (currentDialect is MysqlDialect) {
filterNot { it.table to LinkedHashSet(it.columns) in fKeyConstraints }
} else {
this
}

// SQLite: indices whose names start with "sqlite_" are meant for internal use
fun List<Index>.filterInternalIndices() = if (currentDialect is SQLiteDialect) {
filter { !it.indexName.startsWith("sqlite_") }
} else {
this
}

fun Table.existingIndices() = existingIndices[this].orEmpty().filterForeignKeys().filterInternalIndices()

fun Table.mappedIndices() = this.indices.filterForeignKeys().filterInternalIndices()

val missingIndices = HashSet<Index>()
val nameDiffers = HashSet<Index>()

tables.forEach { table ->
val existingTableIndices = table.existingIndices()
val mappedIndices = table.mappedIndices()

for (index in existingTableIndices) {
val mappedIndex = mappedIndices.firstOrNull { it.onlyNameDiffer(index) } ?: continue
if (withLogs) {
exposedLogger.info(
"Index on table '${table.tableName}' differs only in name: in db ${index.indexName} -> in mapping ${mappedIndex.indexName}"
)
}
nameDiffers.add(index)
nameDiffers.add(mappedIndex)
}

missingIndices.addAll(mappedIndices.subtract(existingTableIndices))
}

val toCreate = missingIndices.subtract(nameDiffers)
toCreate.log("Indices missed from database (will be created):")
return toCreate.toList()
}

/**
* Checks all [tables] for any that have indices that exist in the database but are not mapped in the code. If
* found, this function also logs the SQL statements that can be used to drop these indices.
*
* @return List of indices that are unmapped and can be dropped.
*/
private fun checkUnmappedIndices(vararg tables: Table, withLogs: Boolean): List<Index> {
fun Collection<Index>.log(mainMessage: String) {
if (withLogs && isNotEmpty()) {
exposedLogger.warn(joinToString(prefix = "$mainMessage\n\t\t", separator = "\n\t\t"))
}
}

val foreignKeyConstraints = currentDialect.columnConstraints(*tables).keys
val existingIndices = currentDialect.existingIndices(*tables)

fun List<Index>.filterForeignKeys() = if (currentDialect is MysqlDialect) {
filterNot { it.table to LinkedHashSet(it.columns) in foreignKeyConstraints }
} else {
this
}

// SQLite: indices whose names start with "sqlite_" are meant for internal use
fun List<Index>.filterInternalIndices() = if (currentDialect is SQLiteDialect) {
filter { !it.indexName.startsWith("sqlite_") }
} else {
this
}

fun Table.existingIndices() = existingIndices[this].orEmpty().filterForeignKeys().filterInternalIndices()

fun Table.mappedIndices() = this.indices.filterForeignKeys().filterInternalIndices()

val unmappedIndices = HashMap<String, MutableSet<Index>>()
val nameDiffers = HashSet<Index>()

tables.forEach { table ->
val existingTableIndices = table.existingIndices()
val mappedIndices = table.mappedIndices()

for (index in existingTableIndices) {
val mappedIndex = mappedIndices.firstOrNull { it.onlyNameDiffer(index) } ?: continue
nameDiffers.add(index)
nameDiffers.add(mappedIndex)
}

unmappedIndices.getOrPut(table.nameInDatabaseCase()) {
hashSetOf()
}.addAll(existingTableIndices.subtract(mappedIndices))
}

val toDrop = mutableSetOf<Index>()
unmappedIndices.forEach { (name, indices) ->
toDrop.addAll(
indices.subtract(nameDiffers).also {
it.log("Indices exist in database and not mapped in code on class '$name':")
}
)
}
return toDrop.toList()
}

/**
* Returns the SQL statements that need to be executed to make the existing database schema compatible with
* the table objects defined using Exposed. Unlike [statementsRequiredToActualizeScheme], DROP/DELETE statements are
* included.
*
* **Note:** Some dialects, like SQLite, do not support `ALTER TABLE ADD COLUMN` syntax completely,
* which restricts the behavior when adding some missing columns. Please check the documentation.
*
* By default, a description for each intermediate step, as well as its execution time, is logged at the INFO level.
* This can be disabled by setting [withLogs] to `false`.
*/
fun statementsRequiredForDatabaseMigration(vararg tables: Table, withLogs: Boolean = true): List<String> {
val (tablesToCreate, tablesToAlter) = tables.partition { !it.exists() }
val createStatements = logTimeSpent("Preparing create tables statements", withLogs) {
createStatements(tables = tablesToCreate.toTypedArray())
}
val alterStatements = logTimeSpent("Preparing alter table statements", withLogs) {
addMissingColumnsStatements(tables = tablesToAlter.toTypedArray(), withLogs)
}

val modifyTablesStatements = logTimeSpent("Checking mapping consistence", withLogs) {
mappingConsistenceRequiredStatements(
tables = tables,
withLogs
).filter { it !in (createStatements + alterStatements) }
}

val allStatements = createStatements + alterStatements + modifyTablesStatements
return allStatements
}

/**
* Creates table with name "busy" (if not present) and single column to be used as "synchronization" point. Table wont be dropped after execution.
*
Expand Down
1 change: 1 addition & 0 deletions exposed-java-time/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ dependencies {
testImplementation(project(":exposed-dao"))
testImplementation(project(":exposed-tests"))
testImplementation(project(":exposed-json"))
testImplementation(project(":exposed-migration"))
testImplementation(libs.junit)
testImplementation(kotlin("test-junit"))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -601,7 +601,7 @@ class JavaTimeTests : DatabaseTestsBase() {
val date: Column<LocalDate> = date("date").index().defaultExpression(CurrentDate)
}
withTables(testTable) {
val statements = SchemaUtils.statementsRequiredForDatabaseMigration(testTable)
val statements = MigrationUtils.statementsRequiredForDatabaseMigration(testTable)
assertTrue(statements.isEmpty())
}
}
Expand Down
1 change: 1 addition & 0 deletions exposed-kotlin-datetime/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ dependencies {
testImplementation(project(":exposed-dao"))
testImplementation(project(":exposed-tests"))
testImplementation(project(":exposed-json"))
testImplementation(project(":exposed-migration"))
testImplementation(libs.junit)
testImplementation(kotlin("test-junit"))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -618,7 +618,7 @@ class KotlinTimeTests : DatabaseTestsBase() {
val date: Column<LocalDate> = date("date").index().defaultExpression(CurrentDate)
}
withTables(testTable) {
val statements = SchemaUtils.statementsRequiredForDatabaseMigration(testTable)
val statements = MigrationUtils.statementsRequiredForDatabaseMigration(testTable)
assertTrue(statements.isEmpty())
}
}
Expand Down
2 changes: 2 additions & 0 deletions exposed-migration/api/exposed-migration.api
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@ public final class MigrationUtils {
public static final field INSTANCE LMigrationUtils;
public final fun generateMigrationScript ([Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;Ljava/lang/String;Z)Ljava/io/File;
public static synthetic fun generateMigrationScript$default (LMigrationUtils;[Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;Ljava/lang/String;ZILjava/lang/Object;)Ljava/io/File;
public final fun statementsRequiredForDatabaseMigration ([Lorg/jetbrains/exposed/sql/Table;Z)Ljava/util/List;
public static synthetic fun statementsRequiredForDatabaseMigration$default (LMigrationUtils;[Lorg/jetbrains/exposed/sql/Table;ZILjava/lang/Object;)Ljava/util/List;
}

Loading

0 comments on commit 7c3df2f

Please sign in to comment.