diff --git a/exposed-core/api/exposed-core.api b/exposed-core/api/exposed-core.api index 6659110f58..2c4c6fae62 100644 --- a/exposed-core/api/exposed-core.api +++ b/exposed-core/api/exposed-core.api @@ -256,6 +256,13 @@ public final class org/jetbrains/exposed/sql/Avg : org/jetbrains/exposed/sql/Fun public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V } +public abstract class org/jetbrains/exposed/sql/BaseSchemaUtils { + public fun ()V + protected final fun addMissingColumnsStatements ([Lorg/jetbrains/exposed/sql/Table;Ljava/util/Map;Z)Ljava/util/List; + public static synthetic fun addMissingColumnsStatements$default (Lorg/jetbrains/exposed/sql/BaseSchemaUtils;[Lorg/jetbrains/exposed/sql/Table;Ljava/util/Map;ZILjava/lang/Object;)Ljava/util/List; + protected final fun logTimeSpent (Ljava/lang/String;ZLkotlin/jvm/functions/Function0;)Ljava/lang/Object; +} + public class org/jetbrains/exposed/sql/BasicBinaryColumnType : org/jetbrains/exposed/sql/ColumnType { public fun ()V public synthetic fun nonNullValueToString (Ljava/lang/Object;)Ljava/lang/String; @@ -2120,7 +2127,7 @@ public final class org/jetbrains/exposed/sql/Schema { public fun toString ()Ljava/lang/String; } -public final class org/jetbrains/exposed/sql/SchemaUtils { +public final class org/jetbrains/exposed/sql/SchemaUtils : org/jetbrains/exposed/sql/BaseSchemaUtils { public static final field INSTANCE Lorg/jetbrains/exposed/sql/SchemaUtils; public final fun addMissingColumnsStatements ([Lorg/jetbrains/exposed/sql/Table;Z)Ljava/util/List; public static synthetic fun addMissingColumnsStatements$default (Lorg/jetbrains/exposed/sql/SchemaUtils;[Lorg/jetbrains/exposed/sql/Table;ZILjava/lang/Object;)Ljava/util/List; diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/BaseSchemaUtils.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/BaseSchemaUtils.kt new file mode 100644 index 0000000000..9e3f2b2811 --- /dev/null +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/BaseSchemaUtils.kt @@ -0,0 +1,285 @@ +package org.jetbrains.exposed.sql + +import org.jetbrains.exposed.sql.SchemaUtils.createFKey +import org.jetbrains.exposed.sql.SchemaUtils.createIndex +import org.jetbrains.exposed.sql.SqlExpressionBuilder.asLiteral +import org.jetbrains.exposed.sql.transactions.TransactionManager +import org.jetbrains.exposed.sql.vendors.* +import java.math.BigDecimal + +abstract class BaseSchemaUtils { + protected inline fun logTimeSpent(message: String, withLogs: Boolean, block: () -> R): R { + return if (withLogs) { + val start = System.currentTimeMillis() + val answer = block() + exposedLogger.info(message + " took " + (System.currentTimeMillis() - start) + "ms") + answer + } else { + block() + } + } + + protected fun addMissingColumnsStatements(vararg tables: Table, existingTablesColumns: Map>, withLogs: Boolean = true): List { + val statements = ArrayList() + + val existingPrimaryKeys = logTimeSpent("Extracting primary keys", withLogs) { + currentDialect.existingPrimaryKeys(*tables) + } + + val dbSupportsAlterTableWithAddColumn = TransactionManager.current().db.supportsAlterTableWithAddColumn + + tables.forEach { table -> + // create columns + val thisTableExistingColumns = existingTablesColumns[table].orEmpty() + val existingTableColumns = table.columns.mapNotNull { column -> + val existingColumn = thisTableExistingColumns.find { column.nameUnquoted().equals(it.name, true) } + if (existingColumn != null) column to existingColumn else null + }.toMap() + val missingTableColumns = table.columns.filter { it !in existingTableColumns } + + missingTableColumns.flatMapTo(statements) { it.ddl } + + if (dbSupportsAlterTableWithAddColumn) { + // create indexes with new columns + table.indices.filter { index -> + index.columns.any { + missingTableColumns.contains(it) + } + }.forEach { statements.addAll(createIndex(it)) } + + // sync existing columns + val dataTypeProvider = currentDialect.dataTypeProvider + val redoColumns = existingTableColumns.mapValues { (col, existingCol) -> + val columnType = col.columnType + val colNullable = if (col.dbDefaultValue?.let { currentDialect.isAllowedAsColumnDefault(it) } == false) { + true // Treat a disallowed default value as null because that is what Exposed does with it + } else { + columnType.nullable + } + val incorrectNullability = existingCol.nullable != colNullable + + val incorrectAutoInc = isIncorrectAutoInc(existingCol, col) + + val incorrectDefaults = isIncorrectDefault(dataTypeProvider, existingCol, col) + + val incorrectCaseSensitiveName = existingCol.name.inProperCase() != col.nameUnquoted().inProperCase() + + val incorrectSizeOrScale = isIncorrectSizeOrScale(existingCol, columnType) + + ColumnDiff(incorrectNullability, incorrectAutoInc, incorrectDefaults, incorrectCaseSensitiveName, incorrectSizeOrScale) + }.filterValues { it.hasDifferences() } + + redoColumns.flatMapTo(statements) { (col, changedState) -> col.modifyStatements(changedState) } + + // add missing primary key + val missingPK = table.primaryKey?.takeIf { pk -> pk.columns.none { it in missingTableColumns } } + if (missingPK != null && existingPrimaryKeys[table] == null) { + val missingPKName = missingPK.name.takeIf { table.isCustomPKNameDefined() } + statements.add( + currentDialect.addPrimaryKey(table, missingPKName, pkColumns = missingPK.columns) + ) + } + } + } + + if (dbSupportsAlterTableWithAddColumn) { + statements.addAll(addMissingColumnConstraints(*tables, withLogs = withLogs)) + } + + return statements + } + + private fun isIncorrectAutoInc(columnMetadata: ColumnMetadata, column: Column<*>): Boolean = when { + !columnMetadata.autoIncrement && column.columnType.isAutoInc && column.autoIncColumnType?.sequence == null -> + true + columnMetadata.autoIncrement && column.columnType.isAutoInc && column.autoIncColumnType?.sequence != null -> + true + columnMetadata.autoIncrement && !column.columnType.isAutoInc -> true + else -> false + } + + /** + * For DDL purposes we do not segregate the cases when the default value was not specified, and when it + * was explicitly set to `null`. + */ + private fun isIncorrectDefault(dataTypeProvider: DataTypeProvider, columnMeta: ColumnMetadata, column: Column<*>): Boolean { + val isExistingColumnDefaultNull = columnMeta.defaultDbValue == null + val isDefinedColumnDefaultNull = column.dbDefaultValue?.takeIf { currentDialect.isAllowedAsColumnDefault(it) } == null || + (column.dbDefaultValue is LiteralOp<*> && (column.dbDefaultValue as? LiteralOp<*>)?.value == null) + + return when { + // Both values are null-like, no DDL update is needed + isExistingColumnDefaultNull && isDefinedColumnDefaultNull -> false + // Only one of the values is null-like, DDL update is needed + isExistingColumnDefaultNull != isDefinedColumnDefaultNull -> true + + else -> { + val columnDefaultValue = column.dbDefaultValue?.let { + dataTypeProvider.dbDefaultToString(column, it) + } + columnMeta.defaultDbValue != columnDefaultValue + } + } + } + + private fun isIncorrectSizeOrScale(columnMeta: ColumnMetadata, columnType: IColumnType<*>): Boolean { + // ColumnMetadata.scale can only be non-null if ColumnMetadata.size is non-null + if (columnMeta.size == null) return false + + return when (columnType) { + is DecimalColumnType -> columnType.precision != columnMeta.size || columnType.scale != columnMeta.scale + is CharColumnType -> columnType.colLength != columnMeta.size + is VarCharColumnType -> columnType.colLength != columnMeta.size + is BinaryColumnType -> columnType.length != columnMeta.size + else -> false + } + } + + private fun addMissingColumnConstraints(vararg tables: Table, withLogs: Boolean): List { + val existingColumnConstraint = logTimeSpent("Extracting column constraints", withLogs) { + currentDialect.columnConstraints(*tables) + } + + val foreignKeyConstraints = tables.flatMap { table -> + table.foreignKeys.map { it to existingColumnConstraint[table to it.from]?.firstOrNull() } + } + + val statements = ArrayList() + + for ((foreignKey, existingConstraint) in foreignKeyConstraints) { + if (existingConstraint == null) { + statements.addAll(createFKey(foreignKey)) + continue + } + + val noForeignKey = existingConstraint.targetTable != foreignKey.targetTable + val deleteRuleMismatch = foreignKey.deleteRule != existingConstraint.deleteRule + val updateRuleMismatch = foreignKey.updateRule != existingConstraint.updateRule + + if (noForeignKey || deleteRuleMismatch || updateRuleMismatch) { + statements.addAll(existingConstraint.dropStatement()) + statements.addAll(createFKey(foreignKey)) + } + } + + return statements + } + + @Suppress("NestedBlockDepth", "ComplexMethod", "LongMethod") + private fun DataTypeProvider.dbDefaultToString(column: Column<*>, exp: Expression<*>): String { + return when (exp) { + is LiteralOp<*> -> { + val dialect = currentDialect + when (val value = exp.value) { + is Boolean -> when (dialect) { + is MysqlDialect -> if (value) "1" else "0" + is PostgreSQLDialect -> value.toString() + else -> booleanToStatementString(value) + } + + is String -> when { + dialect is PostgreSQLDialect -> when (column.columnType) { + is VarCharColumnType -> "'$value'::character varying" + is TextColumnType -> "'$value'::text" + else -> processForDefaultValue(exp) + } + + dialect is OracleDialect || dialect.h2Mode == H2Dialect.H2CompatibilityMode.Oracle -> when { + column.columnType is VarCharColumnType && value == "" -> "NULL" + column.columnType is TextColumnType && value == "" -> "NULL" + else -> value + } + + else -> value + } + + is Enum<*> -> when (exp.columnType) { + is EnumerationNameColumnType<*> -> when (dialect) { + is PostgreSQLDialect -> "'${value.name}'::character varying" + else -> value.name + } + + else -> processForDefaultValue(exp) + } + + is BigDecimal -> when (dialect) { + is MysqlDialect -> value.setScale((exp.columnType as DecimalColumnType).scale).toString() + else -> processForDefaultValue(exp) + } + + else -> { + when { + column.columnType is JsonColumnMarker -> { + val processed = processForDefaultValue(exp) + when (dialect) { + is PostgreSQLDialect -> { + if (column.columnType.usesBinaryFormat) { + processed.replace(Regex("(\"|})(:|,)(\\[|\\{|\")"), "$1$2 $3") + } else { + processed + } + } + + is MariaDBDialect -> processed.trim('\'') + is MysqlDialect -> "_utf8mb4\\'${processed.trim('(', ')', '\'')}\\'" + else -> when { + processed.startsWith('\'') && processed.endsWith('\'') -> processed.trim('\'') + else -> processed + } + } + } + + column.columnType is ArrayColumnType<*, *> && dialect is PostgreSQLDialect -> { + (value as List<*>) + .takeIf { it.isNotEmpty() } + ?.run { + val delegateColumnType = column.columnType.delegate as IColumnType + val delegateColumn = (column as Column).withColumnType(delegateColumnType) + val processed = map { + if (delegateColumn.columnType is StringColumnType) { + "'$it'::text" + } else { + dbDefaultToString(delegateColumn, delegateColumn.asLiteral(it)) + } + } + "ARRAY$processed" + } ?: processForDefaultValue(exp) + } + + column.columnType is IDateColumnType -> { + val processed = processForDefaultValue(exp) + if (processed.startsWith('\'') && processed.endsWith('\'')) { + processed.trim('\'') + } else { + processed + } + } + + else -> processForDefaultValue(exp) + } + } + } + } + + is Function<*> -> { + var processed = processForDefaultValue(exp) + if (exp.columnType is IDateColumnType) { + if (processed.startsWith("CURRENT_TIMESTAMP") || processed == "GETDATE()") { + when (currentDialect) { + is SQLServerDialect -> processed = "getdate" + is MariaDBDialect -> processed = processed.lowercase() + } + } + if (processed.trim('(').startsWith("CURRENT_DATE")) { + when (currentDialect) { + is MysqlDialect -> processed = "curdate()" + } + } + } + processed + } + + else -> processForDefaultValue(exp) + } + } +} diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SchemaUtils.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SchemaUtils.kt index 378f8e6216..fa24993c09 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SchemaUtils.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SchemaUtils.kt @@ -1,25 +1,15 @@ package org.jetbrains.exposed.sql import org.jetbrains.exposed.exceptions.ExposedSQLException -import org.jetbrains.exposed.sql.SqlExpressionBuilder.asLiteral import org.jetbrains.exposed.sql.transactions.TransactionManager -import org.jetbrains.exposed.sql.vendors.* -import java.math.BigDecimal +import org.jetbrains.exposed.sql.vendors.H2Dialect +import org.jetbrains.exposed.sql.vendors.MysqlDialect +import org.jetbrains.exposed.sql.vendors.SQLiteDialect +import org.jetbrains.exposed.sql.vendors.currentDialect /** Utility functions that assist with creating, altering, and dropping database schema objects. */ @Suppress("TooManyFunctions", "LargeClass") -object SchemaUtils { - private inline fun logTimeSpent(message: String, withLogs: Boolean, block: () -> R): R { - return if (withLogs) { - val start = System.currentTimeMillis() - val answer = block() - exposedLogger.info(message + " took " + (System.currentTimeMillis() - start) + "ms") - answer - } else { - block() - } - } - +object SchemaUtils : BaseSchemaUtils() { private class TableDepthGraph(val tables: Iterable) { val graph = fetchAllTables().let { tables -> if (tables.isEmpty()) { @@ -157,124 +147,6 @@ object SchemaUtils { /** Returns the SQL statements that create the provided [index]. */ fun createIndex(index: Index): List = index.createStatement() - @Suppress("NestedBlockDepth", "ComplexMethod", "LongMethod") - private fun DataTypeProvider.dbDefaultToString(column: Column<*>, exp: Expression<*>): String { - return when (exp) { - is LiteralOp<*> -> { - val dialect = currentDialect - when (val value = exp.value) { - is Boolean -> when (dialect) { - is MysqlDialect -> if (value) "1" else "0" - is PostgreSQLDialect -> value.toString() - else -> booleanToStatementString(value) - } - - is String -> when { - dialect is PostgreSQLDialect -> when (column.columnType) { - is VarCharColumnType -> "'$value'::character varying" - is TextColumnType -> "'$value'::text" - else -> processForDefaultValue(exp) - } - - dialect is OracleDialect || dialect.h2Mode == H2Dialect.H2CompatibilityMode.Oracle -> when { - column.columnType is VarCharColumnType && value == "" -> "NULL" - column.columnType is TextColumnType && value == "" -> "NULL" - else -> value - } - - else -> value - } - - is Enum<*> -> when (exp.columnType) { - is EnumerationNameColumnType<*> -> when (dialect) { - is PostgreSQLDialect -> "'${value.name}'::character varying" - else -> value.name - } - - else -> processForDefaultValue(exp) - } - - is BigDecimal -> when (dialect) { - is MysqlDialect -> value.setScale((exp.columnType as DecimalColumnType).scale).toString() - else -> processForDefaultValue(exp) - } - - else -> { - when { - column.columnType is JsonColumnMarker -> { - val processed = processForDefaultValue(exp) - when (dialect) { - is PostgreSQLDialect -> { - if (column.columnType.usesBinaryFormat) { - processed.replace(Regex("(\"|})(:|,)(\\[|\\{|\")"), "$1$2 $3") - } else { - processed - } - } - - is MariaDBDialect -> processed.trim('\'') - is MysqlDialect -> "_utf8mb4\\'${processed.trim('(', ')', '\'')}\\'" - else -> when { - processed.startsWith('\'') && processed.endsWith('\'') -> processed.trim('\'') - else -> processed - } - } - } - - column.columnType is ArrayColumnType<*, *> && dialect is PostgreSQLDialect -> { - (value as List<*>) - .takeIf { it.isNotEmpty() } - ?.run { - val delegateColumnType = column.columnType.delegate as IColumnType - val delegateColumn = (column as Column).withColumnType(delegateColumnType) - val processed = map { - if (delegateColumn.columnType is StringColumnType) { - "'$it'::text" - } else { - dbDefaultToString(delegateColumn, delegateColumn.asLiteral(it)) - } - } - "ARRAY$processed" - } ?: processForDefaultValue(exp) - } - - column.columnType is IDateColumnType -> { - val processed = processForDefaultValue(exp) - if (processed.startsWith('\'') && processed.endsWith('\'')) { - processed.trim('\'') - } else { - processed - } - } - - else -> processForDefaultValue(exp) - } - } - } - } - - is Function<*> -> { - var processed = processForDefaultValue(exp) - if (exp.columnType is IDateColumnType) { - if (processed.startsWith("CURRENT_TIMESTAMP") || processed == "GETDATE()") { - when (currentDialect) { - is SQLServerDialect -> processed = "getdate" - is MariaDBDialect -> processed = processed.lowercase() - } - } - if (processed.trim('(').startsWith("CURRENT_DATE")) { - when (currentDialect) { - is MysqlDialect -> processed = "curdate()" - } - } - } - processed - } - - else -> processForDefaultValue(exp) - } - } - /** * Returns the SQL statements that create any columns defined in [tables], which are missing from the existing * tables in the database. @@ -287,154 +159,10 @@ object SchemaUtils { */ fun addMissingColumnsStatements(vararg tables: Table, withLogs: Boolean = true): List { if (tables.isEmpty()) return emptyList() - - val statements = ArrayList() - val existingTablesColumns = logTimeSpent("Extracting table columns", withLogs) { currentDialect.tableColumns(*tables) } - - val existingPrimaryKeys = logTimeSpent("Extracting primary keys", withLogs) { - currentDialect.existingPrimaryKeys(*tables) - } - - val dbSupportsAlterTableWithAddColumn = TransactionManager.current().db.supportsAlterTableWithAddColumn - - for (table in tables) { - // create columns - val thisTableExistingColumns = existingTablesColumns[table].orEmpty() - val existingTableColumns = table.columns.mapNotNull { column -> - val existingColumn = thisTableExistingColumns.find { column.nameUnquoted().equals(it.name, true) } - if (existingColumn != null) column to existingColumn else null - }.toMap() - val missingTableColumns = table.columns.filter { it !in existingTableColumns } - - missingTableColumns.flatMapTo(statements) { it.ddl } - - if (dbSupportsAlterTableWithAddColumn) { - // create indexes with new columns - table.indices.filter { index -> - index.columns.any { - missingTableColumns.contains(it) - } - }.forEach { statements.addAll(createIndex(it)) } - - // sync existing columns - val dataTypeProvider = currentDialect.dataTypeProvider - val redoColumns = existingTableColumns.mapValues { (col, existingCol) -> - val columnType = col.columnType - val colNullable = if (col.dbDefaultValue?.let { currentDialect.isAllowedAsColumnDefault(it) } == false) { - true // Treat a disallowed default value as null because that is what Exposed does with it - } else { - columnType.nullable - } - val incorrectNullability = existingCol.nullable != colNullable - - val incorrectAutoInc = isIncorrectAutoInc(existingCol, col) - - val incorrectDefaults = isIncorrectDefault(dataTypeProvider, existingCol, col) - - val incorrectCaseSensitiveName = existingCol.name.inProperCase() != col.nameUnquoted().inProperCase() - - val incorrectSizeOrScale = isIncorrectSizeOrScale(existingCol, columnType) - - ColumnDiff(incorrectNullability, incorrectAutoInc, incorrectDefaults, incorrectCaseSensitiveName, incorrectSizeOrScale) - }.filterValues { it.hasDifferences() } - - redoColumns.flatMapTo(statements) { (col, changedState) -> col.modifyStatements(changedState) } - - // add missing primary key - val missingPK = table.primaryKey?.takeIf { pk -> pk.columns.none { it in missingTableColumns } } - if (missingPK != null && existingPrimaryKeys[table] == null) { - val missingPKName = missingPK.name.takeIf { table.isCustomPKNameDefined() } - statements.add( - currentDialect.addPrimaryKey(table, missingPKName, pkColumns = missingPK.columns) - ) - } - } - } - - if (dbSupportsAlterTableWithAddColumn) { - statements.addAll(addMissingColumnConstraints(*tables, withLogs = withLogs)) - } - - return statements - } - - private fun isIncorrectAutoInc(columnMetadata: ColumnMetadata, column: Column<*>): Boolean = when { - !columnMetadata.autoIncrement && column.columnType.isAutoInc && column.autoIncColumnType?.sequence == null -> - true - columnMetadata.autoIncrement && column.columnType.isAutoInc && column.autoIncColumnType?.sequence != null -> - true - columnMetadata.autoIncrement && !column.columnType.isAutoInc -> true - else -> false - } - - /** - * For DDL purposes we do not segregate the cases when the default value was not specified, and when it - * was explicitly set to `null`. - */ - private fun isIncorrectDefault(dataTypeProvider: DataTypeProvider, columnMeta: ColumnMetadata, column: Column<*>): Boolean { - val isExistingColumnDefaultNull = columnMeta.defaultDbValue == null - val isDefinedColumnDefaultNull = column.dbDefaultValue?.takeIf { currentDialect.isAllowedAsColumnDefault(it) } == null || - (column.dbDefaultValue is LiteralOp<*> && (column.dbDefaultValue as? LiteralOp<*>)?.value == null) - - return when { - // Both values are null-like, no DDL update is needed - isExistingColumnDefaultNull && isDefinedColumnDefaultNull -> false - // Only one of the values is null-like, DDL update is needed - isExistingColumnDefaultNull != isDefinedColumnDefaultNull -> true - - else -> { - val columnDefaultValue = column.dbDefaultValue?.let { - dataTypeProvider.dbDefaultToString(column, it) - } - columnMeta.defaultDbValue != columnDefaultValue - } - } - } - - private fun isIncorrectSizeOrScale(columnMeta: ColumnMetadata, columnType: IColumnType<*>): Boolean { - // ColumnMetadata.scale can only be non-null if ColumnMetadata.size is non-null - if (columnMeta.size == null) return false - - return when (columnType) { - is DecimalColumnType -> columnType.precision != columnMeta.size || columnType.scale != columnMeta.scale - is CharColumnType -> columnType.colLength != columnMeta.size - is VarCharColumnType -> columnType.colLength != columnMeta.size - is BinaryColumnType -> columnType.length != columnMeta.size - else -> false - } - } - - private fun addMissingColumnConstraints(vararg tables: Table, withLogs: Boolean): List { - val existingColumnConstraint = logTimeSpent("Extracting column constraints", withLogs) { - currentDialect.columnConstraints(*tables) - } - - val foreignKeyConstraints = tables.flatMap { table -> - table.foreignKeys.map { it to existingColumnConstraint[table to it.from]?.firstOrNull() } - } - - val statements = ArrayList() - - for ((foreignKey, existingConstraint) in foreignKeyConstraints) { - if (existingConstraint == null) { - statements.addAll(createFKey(foreignKey)) - continue - } - - val noForeignKey = existingConstraint.targetTable != foreignKey.targetTable - val deleteRuleMismatch = foreignKey.deleteRule != existingConstraint.deleteRule - val updateRuleMismatch = foreignKey.updateRule != existingConstraint.updateRule - - if (noForeignKey || deleteRuleMismatch || updateRuleMismatch) { - statements.addAll(existingConstraint.dropStatement()) - statements.addAll(createFKey(foreignKey)) - } - } - - return statements + return addMissingColumnsStatements(tables = tables, existingTablesColumns = existingTablesColumns, withLogs = withLogs) } private fun Transaction.execStatements(inBatch: Boolean, statements: List) { diff --git a/exposed-migration/api/exposed-migration.api b/exposed-migration/api/exposed-migration.api index 7b8e5deceb..333c1cec63 100644 --- a/exposed-migration/api/exposed-migration.api +++ b/exposed-migration/api/exposed-migration.api @@ -1,4 +1,4 @@ -public final class MigrationUtils { +public final class MigrationUtils : org/jetbrains/exposed/sql/BaseSchemaUtils { public static final field INSTANCE LMigrationUtils; public final fun dropUnmappedColumnsStatements ([Lorg/jetbrains/exposed/sql/Table;Z)Ljava/util/List; public static synthetic fun dropUnmappedColumnsStatements$default (LMigrationUtils;[Lorg/jetbrains/exposed/sql/Table;ZILjava/lang/Object;)Ljava/util/List; diff --git a/exposed-migration/src/main/kotlin/MigrationUtils.kt b/exposed-migration/src/main/kotlin/MigrationUtils.kt index 98402b6a3a..2229ed00e3 100644 --- a/exposed-migration/src/main/kotlin/MigrationUtils.kt +++ b/exposed-migration/src/main/kotlin/MigrationUtils.kt @@ -1,23 +1,18 @@ -import org.jetbrains.exposed.sql.ExperimentalDatabaseMigrationApi -import org.jetbrains.exposed.sql.Index -import org.jetbrains.exposed.sql.SchemaUtils.addMissingColumnsStatements +import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SchemaUtils.checkExcessiveForeignKeyConstraints import org.jetbrains.exposed.sql.SchemaUtils.checkExcessiveIndices import org.jetbrains.exposed.sql.SchemaUtils.checkMappingConsistence import org.jetbrains.exposed.sql.SchemaUtils.createStatements import org.jetbrains.exposed.sql.SchemaUtils.statementsRequiredToActualizeScheme -import org.jetbrains.exposed.sql.Sequence -import org.jetbrains.exposed.sql.Table -import org.jetbrains.exposed.sql.exists -import org.jetbrains.exposed.sql.exposedLogger import org.jetbrains.exposed.sql.transactions.TransactionManager +import org.jetbrains.exposed.sql.vendors.ColumnMetadata import org.jetbrains.exposed.sql.vendors.H2Dialect import org.jetbrains.exposed.sql.vendors.MysqlDialect import org.jetbrains.exposed.sql.vendors.SQLiteDialect import org.jetbrains.exposed.sql.vendors.currentDialect import java.io.File -object MigrationUtils { +object MigrationUtils : BaseSchemaUtils() { /** * This function simply generates the migration script without applying the migration. Its purpose is to show what * the migration script will look like before applying the migration. If a migration script with the same name @@ -75,9 +70,16 @@ object MigrationUtils { val createSequencesStatements = logTimeSpent("Preparing create sequences statements", withLogs) { checkMissingSequences(tables = tables, withLogs).flatMap { it.createStatement() } } - val alterStatements = logTimeSpent("Preparing alter table statements", withLogs) { - addMissingColumnsStatements(tables = tablesToAlter.toTypedArray(), withLogs) + - dropUnmappedColumnsStatements(tables = tablesToAlter.toTypedArray(), withLogs) + val alterStatements = if (tablesToAlter.isEmpty()) { + emptyList() + } else { + logTimeSpent("Preparing alter table statements", withLogs) { + val existingTablesColumns = logTimeSpent("Extracting table columns", withLogs) { + currentDialect.tableColumns(tables = tablesToAlter.toTypedArray()) + } + addMissingColumnsStatements(tables = tablesToAlter.toTypedArray(), existingTablesColumns, withLogs) + + dropUnmappedColumnsStatements(tables = tablesToAlter.toTypedArray(), existingTablesColumns) + } } val modifyTablesStatements = logTimeSpent("Checking mapping consistence", withLogs) { @@ -102,16 +104,21 @@ object MigrationUtils { */ fun dropUnmappedColumnsStatements(vararg tables: Table, withLogs: Boolean = true): List { if (tables.isEmpty()) return emptyList() + val existingTablesColumns = logTimeSpent("Extracting table columns", withLogs) { + currentDialect.tableColumns(*tables) + } + return dropUnmappedColumnsStatements(tables = tables, existingTablesColumns = existingTablesColumns) + } + private fun dropUnmappedColumnsStatements( + vararg tables: Table, + existingTablesColumns: Map> + ): List { val statements = mutableListOf() val dbSupportsAlterTableWithDropColumn = TransactionManager.current().db.supportsAlterTableWithDropColumn if (dbSupportsAlterTableWithDropColumn) { - val existingTablesColumns = logTimeSpent("Extracting table columns", withLogs) { - currentDialect.tableColumns(*tables) - } - val tr = TransactionManager.current() tables.forEach { table -> @@ -324,17 +331,6 @@ object MigrationUtils { return unmappedSequences.toList() } - - private inline fun logTimeSpent(message: String, withLogs: Boolean, block: () -> R): R { - return if (withLogs) { - val start = System.currentTimeMillis() - val answer = block() - exposedLogger.info(message + " took " + (System.currentTimeMillis() - start) + "ms") - answer - } else { - block() - } - } } internal fun String.inProperCase(): String =