Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor!: Move statementsRequiredForDatabaseMigration function from SchemaUtils to MigrationUtils #2195

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading