Skip to content

Commit 0e6acbd

Browse files
authored
feat: EXPOSED-552 Include DROP statements for unmapped columns for migration (#2249)
1 parent 27baa04 commit 0e6acbd

File tree

8 files changed

+98
-1
lines changed

8 files changed

+98
-1
lines changed

exposed-core/api/exposed-core.api

+2
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,7 @@ public final class org/jetbrains/exposed/sql/Database {
664664
public final fun getDialect ()Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect;
665665
public final fun getIdentifierManager ()Lorg/jetbrains/exposed/sql/statements/api/IdentifierManagerApi;
666666
public final fun getSupportsAlterTableWithAddColumn ()Z
667+
public final fun getSupportsAlterTableWithDropColumn ()Z
667668
public final fun getSupportsMultipleResultSets ()Z
668669
public final fun getUrl ()Ljava/lang/String;
669670
public final fun getUseNestedTransactions ()Z
@@ -3508,6 +3509,7 @@ public abstract class org/jetbrains/exposed/sql/statements/api/ExposedDatabaseMe
35083509
public abstract fun getIdentifierManager ()Lorg/jetbrains/exposed/sql/statements/api/IdentifierManagerApi;
35093510
public abstract fun getSchemaNames ()Ljava/util/List;
35103511
public abstract fun getSupportsAlterTableWithAddColumn ()Z
3512+
public abstract fun getSupportsAlterTableWithDropColumn ()Z
35113513
public abstract fun getSupportsMultipleResultSets ()Z
35123514
public abstract fun getSupportsSelectForUpdate ()Z
35133515
public abstract fun getTableNames ()Ljava/util/Map;

exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Database.kt

+5
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ class Database private constructor(
6969
LazyThreadSafetyMode.NONE
7070
) { metadata { supportsAlterTableWithAddColumn } }
7171

72+
/** Whether the database supports ALTER TABLE with a drop column clause. */
73+
val supportsAlterTableWithDropColumn by lazy(
74+
LazyThreadSafetyMode.NONE
75+
) { metadata { supportsAlterTableWithDropColumn } }
76+
7277
/** Whether the database supports getting multiple result sets from a single execute. */
7378
val supportsMultipleResultSets by lazy(LazyThreadSafetyMode.NONE) { metadata { supportsMultipleResultSets } }
7479

exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/api/ExposedDatabaseMetadata.kt

+3
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ abstract class ExposedDatabaseMetadata(val database: String) {
3030
/** Whether the database supports `ALTER TABLE` with an add column clause. */
3131
abstract val supportsAlterTableWithAddColumn: Boolean
3232

33+
/** Whether the database supports `ALTER TABLE` with a drop column clause. */
34+
abstract val supportsAlterTableWithDropColumn: Boolean
35+
3336
/** Whether the database supports getting multiple result sets from a single execute. */
3437
abstract val supportsMultipleResultSets: Boolean
3538

exposed-jdbc/api/exposed-jdbc.api

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public final class org/jetbrains/exposed/sql/statements/jdbc/JdbcDatabaseMetadat
4545
public final fun getMetadata ()Ljava/sql/DatabaseMetaData;
4646
public fun getSchemaNames ()Ljava/util/List;
4747
public fun getSupportsAlterTableWithAddColumn ()Z
48+
public fun getSupportsAlterTableWithDropColumn ()Z
4849
public fun getSupportsMultipleResultSets ()Z
4950
public fun getSupportsSelectForUpdate ()Z
5051
public fun getTableNames ()Ljava/util/Map;

exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcDatabaseMetadataImpl.kt

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ class JdbcDatabaseMetadataImpl(database: String, val metadata: DatabaseMetaData)
5050
override val defaultIsolationLevel: Int by lazyMetadata { defaultTransactionIsolation }
5151

5252
override val supportsAlterTableWithAddColumn by lazyMetadata { supportsAlterTableWithAddColumn() }
53+
override val supportsAlterTableWithDropColumn by lazyMetadata { supportsAlterTableWithDropColumn() }
5354
override val supportsMultipleResultSets by lazyMetadata { supportsMultipleResultSets() }
5455
override val supportsSelectForUpdate: Boolean by lazyMetadata { supportsSelectForUpdate() }
5556

exposed-migration/api/exposed-migration.api

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
public final class MigrationUtils {
22
public static final field INSTANCE LMigrationUtils;
3+
public final fun dropUnmappedColumnsStatements ([Lorg/jetbrains/exposed/sql/Table;Z)Ljava/util/List;
4+
public static synthetic fun dropUnmappedColumnsStatements$default (LMigrationUtils;[Lorg/jetbrains/exposed/sql/Table;ZILjava/lang/Object;)Ljava/util/List;
35
public final fun generateMigrationScript ([Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;Ljava/lang/String;Z)Ljava/io/File;
46
public static synthetic fun generateMigrationScript$default (LMigrationUtils;[Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;Ljava/lang/String;ZILjava/lang/Object;)Ljava/io/File;
57
public final fun statementsRequiredForDatabaseMigration ([Lorg/jetbrains/exposed/sql/Table;Z)Ljava/util/List;

exposed-migration/src/main/kotlin/MigrationUtils.kt

+44-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ object MigrationUtils {
7676
checkMissingSequences(tables = tables, withLogs).flatMap { it.createStatement() }
7777
}
7878
val alterStatements = logTimeSpent("Preparing alter table statements", withLogs) {
79-
addMissingColumnsStatements(tables = tablesToAlter.toTypedArray(), withLogs)
79+
addMissingColumnsStatements(tables = tablesToAlter.toTypedArray(), withLogs) +
80+
dropUnmappedColumnsStatements(tables = tablesToAlter.toTypedArray(), withLogs)
8081
}
8182

8283
val modifyTablesStatements = logTimeSpent("Checking mapping consistence", withLogs) {
@@ -90,6 +91,48 @@ object MigrationUtils {
9091
return allStatements
9192
}
9293

94+
/**
95+
* Returns the SQL statements that drop any columns that exist in the database but are not defined in [tables].
96+
*
97+
* By default, a description for each intermediate step, as well as its execution time, is logged at the INFO level.
98+
* This can be disabled by setting [withLogs] to `false`.
99+
*
100+
* **Note:** Some dialects, like SQLite, do not support `ALTER TABLE DROP COLUMN` syntax completely.
101+
* Please check the documentation.
102+
*/
103+
fun dropUnmappedColumnsStatements(vararg tables: Table, withLogs: Boolean = true): List<String> {
104+
if (tables.isEmpty()) return emptyList()
105+
106+
val statements = mutableListOf<String>()
107+
108+
val dbSupportsAlterTableWithDropColumn = TransactionManager.current().db.supportsAlterTableWithDropColumn
109+
110+
if (dbSupportsAlterTableWithDropColumn) {
111+
val existingTablesColumns = logTimeSpent("Extracting table columns", withLogs) {
112+
currentDialect.tableColumns(*tables)
113+
}
114+
115+
val tr = TransactionManager.current()
116+
117+
tables.forEach { table ->
118+
val existingColumns = existingTablesColumns[table].orEmpty().toSet()
119+
val tableColumns = table.columns.toSet()
120+
val mappedColumns = existingColumns.mapNotNull { columnMetadata ->
121+
val mappedCol = tableColumns.find { column -> columnMetadata.name.equals(column.nameUnquoted(), ignoreCase = true) }
122+
if (mappedCol != null) columnMetadata else null
123+
}.toSet()
124+
val unmappedColumns = existingColumns.subtract(mappedColumns)
125+
unmappedColumns.forEach {
126+
statements.add(
127+
"ALTER TABLE ${tr.identity(table)} DROP COLUMN ${tr.db.identifierManager.quoteIdentifierWhenWrongCaseOrNecessary(it.name)}"
128+
)
129+
}
130+
}
131+
}
132+
133+
return statements
134+
}
135+
93136
/**
94137
* Log Exposed table mappings <-> real database mapping problems and returns DDL Statements to fix them, including
95138
* DROP/DELETE statements (unlike [checkMappingConsistence])

exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/DatabaseMigrationTests.kt

+40
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import org.jetbrains.exposed.sql.tests.DatabaseTestsBase
1212
import org.jetbrains.exposed.sql.tests.TestDB
1313
import org.jetbrains.exposed.sql.tests.currentDialectTest
1414
import org.jetbrains.exposed.sql.tests.inProperCase
15+
import org.jetbrains.exposed.sql.tests.shared.assertEqualCollections
1516
import org.jetbrains.exposed.sql.tests.shared.assertEqualLists
1617
import org.jetbrains.exposed.sql.tests.shared.assertEquals
1718
import org.jetbrains.exposed.sql.tests.shared.assertTrue
@@ -132,6 +133,45 @@ class DatabaseMigrationTests : DatabaseTestsBase() {
132133
}
133134
}
134135

136+
@Test
137+
fun testDropUnmappedColumnsStatementsIdentical() {
138+
val t1 = object : Table("foo") {
139+
val col1 = integer("col1")
140+
val col2 = integer("CoL2")
141+
val col3 = integer("\"CoL3\"")
142+
}
143+
144+
val t2 = object : Table("foo") {
145+
val col1 = integer("col1")
146+
val col2 = integer("CoL2")
147+
val col3 = integer("\"CoL3\"")
148+
}
149+
150+
withTables(t1) {
151+
val statements = MigrationUtils.dropUnmappedColumnsStatements(t2, withLogs = false)
152+
assertEqualCollections(statements, emptyList())
153+
}
154+
}
155+
156+
@Test
157+
fun testDropUnmappedColumns() {
158+
val t1 = object : Table("foo") {
159+
val id = integer("id")
160+
val name = text("name")
161+
}
162+
163+
val t2 = object : Table("foo") {
164+
val id = integer("id")
165+
}
166+
167+
withTables(excludeSettings = listOf(TestDB.SQLITE, TestDB.ORACLE), t1) {
168+
assertEqualCollections(MigrationUtils.statementsRequiredForDatabaseMigration(t1, withLogs = false), emptyList())
169+
170+
val statements = MigrationUtils.statementsRequiredForDatabaseMigration(t2, withLogs = false)
171+
assertEquals(1, statements.size)
172+
}
173+
}
174+
135175
@Test
136176
fun testAddNewPrimaryKeyOnExistingColumn() {
137177
val tableName = "tester"

0 commit comments

Comments
 (0)