From 2ec13b3fb07e6b6c07f8155a9f1977ccfc89e97a Mon Sep 17 00:00:00 2001 From: Jocelyne Date: Mon, 26 Aug 2024 20:47:32 +0200 Subject: [PATCH] feat: EXPOSED-498 Detect auto-increment status change on a column --- exposed-core/api/exposed-core.api | 11 + .../org/jetbrains/exposed/sql/SchemaUtils.kt | 13 +- .../kotlin/org/jetbrains/exposed/sql/Table.kt | 19 + .../exposed/sql/vendors/DatabaseDialect.kt | 4 +- .../org/jetbrains/exposed/sql/vendors/H2.kt | 4 +- .../exposed/sql/vendors/PostgreSQL.kt | 67 +- .../exposed/sql/vendors/SQLServerDialect.kt | 16 +- .../src/main/kotlin/MigrationUtils.kt | 70 +- .../ddl/CreateMissingTablesAndColumnsTests.kt | 2 +- .../shared/ddl/DatabaseMigrationTests.kt | 616 +++++++++++++++++- .../sql/tests/shared/dml/InsertTests.kt | 4 +- .../sql/tests/shared/entities/ViaTest.kt | 9 +- 12 files changed, 789 insertions(+), 46 deletions(-) diff --git a/exposed-core/api/exposed-core.api b/exposed-core/api/exposed-core.api index f7cc9bb0fd..2a1fdd2854 100644 --- a/exposed-core/api/exposed-core.api +++ b/exposed-core/api/exposed-core.api @@ -2487,6 +2487,7 @@ public class org/jetbrains/exposed/sql/Table : org/jetbrains/exposed/sql/ColumnS public final fun getIndices ()Ljava/util/List; public fun getPrimaryKey ()Lorg/jetbrains/exposed/sql/Table$PrimaryKey; public final fun getSchemaName ()Ljava/lang/String; + public final fun getSequences ()Ljava/util/List; public fun getTableName ()Ljava/lang/String; public fun hashCode ()I public final fun index (Ljava/lang/String;Z[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V @@ -3812,6 +3813,7 @@ public final class org/jetbrains/exposed/sql/vendors/DatabaseDialect$DefaultImpl public final class org/jetbrains/exposed/sql/vendors/DatabaseDialectKt { public static final fun getCurrentDialect ()Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect; + public static final fun inProperCase (Ljava/lang/String;)Ljava/lang/String; } public abstract class org/jetbrains/exposed/sql/vendors/ForUpdateOption { @@ -3985,6 +3987,7 @@ public class org/jetbrains/exposed/sql/vendors/H2Dialect : org/jetbrains/exposed public final fun getDelegatedDialectNameProvider ()Lorg/jetbrains/exposed/sql/vendors/VendorDialect$DialectNameProvider; public fun getFunctionProvider ()Lorg/jetbrains/exposed/sql/vendors/FunctionProvider; public final fun getH2Mode ()Lorg/jetbrains/exposed/sql/vendors/H2Dialect$H2CompatibilityMode; + public final fun getMajorVersion ()Lorg/jetbrains/exposed/sql/vendors/H2Dialect$H2MajorVersion; public fun getName ()Ljava/lang/String; public fun getNeedsSequenceToAutoInc ()Z public final fun getOriginalDataTypeProvider ()Lorg/jetbrains/exposed/sql/vendors/DataTypeProvider; @@ -4023,6 +4026,14 @@ public final class org/jetbrains/exposed/sql/vendors/H2Dialect$H2CompatibilityMo public static fun values ()[Lorg/jetbrains/exposed/sql/vendors/H2Dialect$H2CompatibilityMode; } +public final class org/jetbrains/exposed/sql/vendors/H2Dialect$H2MajorVersion : java/lang/Enum { + public static final field One Lorg/jetbrains/exposed/sql/vendors/H2Dialect$H2MajorVersion; + public static final field Two Lorg/jetbrains/exposed/sql/vendors/H2Dialect$H2MajorVersion; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/vendors/H2Dialect$H2MajorVersion; + public static fun values ()[Lorg/jetbrains/exposed/sql/vendors/H2Dialect$H2MajorVersion; +} + public final class org/jetbrains/exposed/sql/vendors/H2Kt { public static final fun getH2Mode (Lorg/jetbrains/exposed/sql/vendors/DatabaseDialect;)Lorg/jetbrains/exposed/sql/vendors/H2Dialect$H2CompatibilityMode; } 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 edfc6da40c..a86fbd280e 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 @@ -326,8 +326,8 @@ object SchemaUtils { columnType.nullable } val incorrectNullability = existingCol.nullable != colNullable - // Exposed doesn't support changing sequences on columns - val incorrectAutoInc = existingCol.autoIncrement != columnType.isAutoInc && col.autoIncColumnType?.autoincSeq == null + + val incorrectAutoInc = isIncorrectAutoInc(existingCol, col) val incorrectDefaults = isIncorrectDefault(dataTypeProvider, existingCol, col) @@ -358,6 +358,15 @@ object SchemaUtils { 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`. diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt index e84a25cdb7..827c070d89 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt @@ -489,6 +489,25 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { /** Returns all foreign key constraints declared on the table. */ val foreignKeys: List get() = columns.mapNotNull { it.foreignKey } + _foreignKeys + /** + * Returns all sequences declared on the table, along with any auto-generated sequences that are not explicitly + * declared by the user but associated with the table. + */ + val sequences: List + get() = columns.filter { it.columnType.isAutoInc }.mapNotNull { column -> + column.autoIncColumnType?.sequence + ?: column.takeIf { currentDialect is PostgreSQLDialect }?.let { + val q = if (tableName.contains('.')) "\"" else "" + val fallbackSeqName = "$q${tableName.replace("\"", "")}_${it.name}_seq$q" + Sequence( + fallbackSeqName, + startWith = 1, + minValue = 1, + maxValue = Long.MAX_VALUE + ) + } + } + private val checkConstraints = mutableListOf>>() private val generatedCheckPrefix = "chk_${tableName}_unsigned_" diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/DatabaseDialect.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/DatabaseDialect.kt index 10873f9eac..c1b23b3b5f 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/DatabaseDialect.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/DatabaseDialect.kt @@ -36,7 +36,7 @@ interface DatabaseDialect { /** Returns `true` if the dialect supports returning multiple generated keys as a result of an insert operation, `false` otherwise. */ val supportsMultipleGeneratedKeys: Boolean - /** Returns`true` if the dialect supports returning generated keys obtained from a sequence. */ + /** Returns `true` if the dialect supports returning generated keys obtained from a sequence. */ val supportsSequenceAsGeneratedKeys: Boolean get() = supportsCreateSequence /** Returns `true` if the dialect supports only returning generated keys that are identity columns. */ @@ -202,5 +202,5 @@ internal val currentDialectIfAvailable: DatabaseDialect? null } -internal fun String.inProperCase(): String = +fun String.inProperCase(): String = TransactionManager.currentOrNull()?.db?.identifierManager?.inProperCase(this@inProperCase) ?: this diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/H2.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/H2.kt index 12f1767da5..0c6ba9b0b1 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/H2.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/H2.kt @@ -153,7 +153,7 @@ open class H2Dialect : VendorDialect(dialectName, H2DataTypeProvider, H2Function override fun toString(): String = "H2Dialect[$dialectName, $h2Mode]" - internal enum class H2MajorVersion { + enum class H2MajorVersion { One, Two } @@ -161,7 +161,7 @@ open class H2Dialect : VendorDialect(dialectName, H2DataTypeProvider, H2Function exactH2Version(TransactionManager.current()) } - internal val majorVersion: H2MajorVersion by lazy { + val majorVersion: H2MajorVersion by lazy { when { version.startsWith("1.") -> H2MajorVersion.One version.startsWith("2.") -> H2MajorVersion.Two diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/PostgreSQL.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/PostgreSQL.kt index 0750affc9a..de657dea52 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/PostgreSQL.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/PostgreSQL.kt @@ -359,31 +359,58 @@ open class PostgreSQLDialect(override val name: String = dialectName) : VendorDi override fun isAllowedAsColumnDefault(e: Expression<*>): Boolean = true - override fun modifyColumn(column: Column<*>, columnDiff: ColumnDiff): List = listOf( - buildString { - val tr = TransactionManager.current() - append("ALTER TABLE ${tr.identity(column.table)} ") - val colName = tr.identity(column) - append("ALTER COLUMN $colName TYPE ${column.columnType.sqlType()}") - - if (columnDiff.nullability) { - append(", ALTER COLUMN $colName ") - if (column.columnType.nullable) { - append("DROP ") + override fun modifyColumn(column: Column<*>, columnDiff: ColumnDiff): List { + val list = mutableListOf( + buildString { + val tr = TransactionManager.current() + append("ALTER TABLE ${tr.identity(column.table)} ") + val colName = tr.identity(column) + + if (columnDiff.autoInc && column.autoIncColumnType != null) { + val sequence = column.autoIncColumnType?.sequence + if (sequence != null) { + append("ALTER COLUMN $colName TYPE ${column.columnType.sqlType()}") + append(", ALTER COLUMN $colName DROP DEFAULT") + } else { + val q = if (column.table.tableName.contains('.')) "\"" else "" + val fallbackSeqName = "$q${column.table.tableName.replace("\"", "")}_${column.name}_seq$q" + append("ALTER COLUMN $colName SET DEFAULT nextval('$fallbackSeqName')") + } } else { - append("SET ") + append("ALTER COLUMN $colName TYPE ${column.columnType.sqlType()}") } - append("NOT NULL") - } - if (columnDiff.defaults) { - column.dbDefaultValue?.let { - append(", ALTER COLUMN $colName SET DEFAULT ${PostgreSQLDataTypeProvider.processForDefaultValue(it)}") - } ?: run { - append(", ALTER COLUMN $colName DROP DEFAULT") + + if (columnDiff.nullability) { + append(", ALTER COLUMN $colName ") + if (column.columnType.nullable) { + append("DROP ") + } else { + append("SET ") + } + append("NOT NULL") + } + if (columnDiff.defaults) { + column.dbDefaultValue?.let { + append(", ALTER COLUMN $colName SET DEFAULT ${PostgreSQLDataTypeProvider.processForDefaultValue(it)}") + } ?: run { + append(", ALTER COLUMN $colName DROP DEFAULT") + } } } + ) + if (columnDiff.autoInc && column.autoIncColumnType != null && column.autoIncColumnType?.sequence == null) { + list.add( + buildString { + val tr = TransactionManager.current() + val colName = tr.identity(column) + val q = if (column.table.tableName.contains('.')) "\"" else "" + val fallbackSeqName = "$q${column.table.tableName.replace("\"", "")}_${column.name}_seq$q" + append("ALTER SEQUENCE $fallbackSeqName OWNED BY $q${column.table.tableName.replace("\"", "")}.${column.name}$q") + } + ) } - ) + return list + } override fun createDatabase(name: String): String = "CREATE DATABASE ${name.inProperCase()}" diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLServerDialect.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLServerDialect.kt index 88ebb97f8a..a37ba1cc69 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLServerDialect.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLServerDialect.kt @@ -314,9 +314,16 @@ open class SQLServerDialect : VendorDialect(dialectName, SQLServerDataTypeProvid val statements = mutableListOf() + val autoIncColumnType = column.autoIncColumnType + val replaceWithNewColumn = columnDiff.autoInc && autoIncColumnType != null && autoIncColumnType.sequence == null + statements.add( buildString { - append(alterTablePart + "ALTER COLUMN ${transaction.identity(column)} ${column.columnType.sqlType()}") + if (replaceWithNewColumn) { + append(alterTablePart + "ADD NEW_${transaction.identity(column)} ${column.columnType.sqlType()}") + } else { + append(alterTablePart + "ALTER COLUMN ${transaction.identity(column)} ${column.columnType.sqlType()}") + } if (columnDiff.nullability) { val defaultValue = column.dbDefaultValue @@ -354,6 +361,13 @@ open class SQLServerDialect : VendorDialect(dialectName, SQLServerDataTypeProvid ) } + if (replaceWithNewColumn) { + with(statements) { + add(alterTablePart + "DROP COLUMN ${transaction.identity(column)}") + add("EXEC sp_rename '${transaction.identity(column.table)}.NEW_${transaction.identity(column)}', '${transaction.identity(column)}', 'COLUMN'") + } + } + return statements } diff --git a/exposed-migration/src/main/kotlin/MigrationUtils.kt b/exposed-migration/src/main/kotlin/MigrationUtils.kt index 5b7681d126..8f997d8731 100644 --- a/exposed-migration/src/main/kotlin/MigrationUtils.kt +++ b/exposed-migration/src/main/kotlin/MigrationUtils.kt @@ -6,12 +6,15 @@ 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.vendors.H2Dialect import org.jetbrains.exposed.sql.vendors.MysqlDialect import org.jetbrains.exposed.sql.vendors.SQLiteDialect import org.jetbrains.exposed.sql.vendors.currentDialect +import org.jetbrains.exposed.sql.vendors.inProperCase import java.io.File object MigrationUtils { @@ -69,6 +72,9 @@ object MigrationUtils { val createStatements = logTimeSpent("Preparing create tables statements", withLogs) { createStatements(tables = tablesToCreate.toTypedArray()) } + 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) } @@ -80,7 +86,7 @@ object MigrationUtils { ).filter { it !in (createStatements + alterStatements) } } - val allStatements = createStatements + alterStatements + modifyTablesStatements + val allStatements = createStatements + createSequencesStatements + alterStatements + modifyTablesStatements return allStatements } @@ -92,7 +98,8 @@ object MigrationUtils { 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() } + checkExcessiveIndices(tables = tables, withLogs).flatMap { it.dropStatement() } + + checkUnmappedSequences(tables = tables, withLogs).flatMap { it.dropStatement() } } /** @@ -216,6 +223,65 @@ object MigrationUtils { return toDrop.toList() } + /** + * Checks all [tables] for any that have sequences 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 sequences. + * + * @return List of sequences that are missing and can be created. + */ + private fun checkMissingSequences(vararg tables: Table, withLogs: Boolean): List { + if (!currentDialect.supportsCreateSequence) { + return emptyList() + } + + fun Collection.log(mainMessage: String) { + if (withLogs && isNotEmpty()) { + exposedLogger.warn(joinToString(prefix = "$mainMessage\n\t\t", separator = "\n\t\t")) + } + } + + val existingSequencesNames: Set = currentDialect.sequences().toSet() + + val missingSequences = mutableSetOf() + + val mappedSequences: Set = tables.flatMap { table -> table.sequences }.toSet() + + missingSequences.addAll(mappedSequences.filterNot { it.identifier.inProperCase() in existingSequencesNames }) + + missingSequences.log("Sequences missed from database (will be created):") + return missingSequences.toList() + } + + /** + * Checks all [tables] for any that have sequences 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 sequences. + * + * @return List of sequences that are unmapped and can be dropped. + */ + private fun checkUnmappedSequences(vararg tables: Table, withLogs: Boolean): List { + if (!currentDialect.supportsCreateSequence || (currentDialect as? H2Dialect)?.majorVersion == H2Dialect.H2MajorVersion.One) { + return emptyList() + } + + fun Collection.log(mainMessage: String) { + if (withLogs && isNotEmpty()) { + exposedLogger.warn(joinToString(prefix = "$mainMessage\n\t\t", separator = "\n\t\t")) + } + } + + val existingSequencesNames: Set = currentDialect.sequences().toSet() + + val unmappedSequences = mutableSetOf() + + val mappedSequencesNames: Set = tables.flatMap { table -> table.sequences.map { it.identifier.inProperCase() } }.toSet() + + unmappedSequences.addAll(existingSequencesNames.subtract(mappedSequencesNames).map { Sequence(it) }) + + unmappedSequences.log("Sequences exist in database and not mapped in code:") + + return unmappedSequences.toList() + } + private inline fun logTimeSpent(message: String, withLogs: Boolean, block: () -> R): R { return if (withLogs) { val start = System.currentTimeMillis() diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateMissingTablesAndColumnsTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateMissingTablesAndColumnsTests.kt index bba2942922..1ac3e97f26 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateMissingTablesAndColumnsTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateMissingTablesAndColumnsTests.kt @@ -586,7 +586,7 @@ class CreateMissingTablesAndColumnsTests : DatabaseTestsBase() { @Test fun explicitFkNameIsExplicit() { - withTables(ExplicitTable, NonExplicitTable) { + withTables(PlayerTable, ExplicitTable, NonExplicitTable) { assertEquals("Explicit_FK_NAME", ExplicitTable.playerId.foreignKey!!.customFkName) assertEquals(null, NonExplicitTable.playerId.foreignKey!!.customFkName) } diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/DatabaseMigrationTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/DatabaseMigrationTests.kt index d1ed416716..69d7f6cf68 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/DatabaseMigrationTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/DatabaseMigrationTests.kt @@ -1,7 +1,12 @@ package org.jetbrains.exposed.sql.tests.shared.ddl +import MigrationUtils +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.dao.id.IdTable +import org.jetbrains.exposed.sql.Column import org.jetbrains.exposed.sql.ExperimentalDatabaseMigrationApi import org.jetbrains.exposed.sql.SchemaUtils +import org.jetbrains.exposed.sql.Sequence import org.jetbrains.exposed.sql.Table import org.jetbrains.exposed.sql.exists import org.jetbrains.exposed.sql.tests.DatabaseTestsBase @@ -12,7 +17,9 @@ import org.jetbrains.exposed.sql.tests.shared.assertEqualLists import org.jetbrains.exposed.sql.tests.shared.assertEquals import org.jetbrains.exposed.sql.tests.shared.assertTrue import org.jetbrains.exposed.sql.tests.shared.expectException +import org.jetbrains.exposed.sql.vendors.MysqlDialect import org.jetbrains.exposed.sql.vendors.PrimaryKeyMetadata +import org.junit.Before import org.junit.Test import java.io.File import kotlin.properties.Delegates @@ -21,6 +28,19 @@ import kotlin.test.assertNull @OptIn(ExperimentalDatabaseMigrationApi::class) class DatabaseMigrationTests : DatabaseTestsBase() { + @Before + fun dropAllSequences() { + withDb { + val allSequences = currentDialectTest.sequences().map { Sequence(it) } + allSequences.forEach { sequence -> + val dropStatements = sequence.dropStatement() + dropStatements.forEach { statement -> + exec(statement) + } + } + } + } + @Test fun testMigrationScriptDirectoryAndContent() { val tableName = "tester" @@ -40,11 +60,11 @@ class DatabaseMigrationTests : DatabaseTestsBase() { try { SchemaUtils.create(noPKTable) - val script = MigrationUtils.generateMigrationScript(singlePKTable, scriptDirectory = scriptDirectory, scriptName = scriptName) + val script = MigrationUtils.generateMigrationScript(singlePKTable, scriptDirectory = scriptDirectory, scriptName = scriptName, withLogs = false) assertTrue(script.exists()) assertEquals("src/test/resources/$scriptName.sql", script.path) - val expectedStatements: List = MigrationUtils.statementsRequiredForDatabaseMigration(singlePKTable) + val expectedStatements: List = MigrationUtils.statementsRequiredForDatabaseMigration(singlePKTable, withLogs = false) assertEquals(1, expectedStatements.size) val fileStatements: List = script.bufferedReader().readLines().map { it.trimEnd(';') } @@ -80,15 +100,15 @@ class DatabaseMigrationTests : DatabaseTestsBase() { // Create initial script val initialScript = File("$directory/$name.sql") initialScript.createNewFile() - val statements = MigrationUtils.statementsRequiredForDatabaseMigration(noPKTable) + val statements = MigrationUtils.statementsRequiredForDatabaseMigration(noPKTable, withLogs = false) statements.forEach { initialScript.appendText(it) } // Generate script with the same name of initial script - val newScript = MigrationUtils.generateMigrationScript(singlePKTable, scriptDirectory = directory, scriptName = name) + val newScript = MigrationUtils.generateMigrationScript(singlePKTable, scriptDirectory = directory, scriptName = name, withLogs = false) - val expectedStatements: List = MigrationUtils.statementsRequiredForDatabaseMigration(singlePKTable) + val expectedStatements: List = MigrationUtils.statementsRequiredForDatabaseMigration(singlePKTable, withLogs = false) assertEquals(1, expectedStatements.size) val fileStatements: List = newScript.bufferedReader().readLines().map { it.trimEnd(';') } @@ -106,7 +126,7 @@ class DatabaseMigrationTests : DatabaseTestsBase() { fun testNoTablesPassedWhenGeneratingMigrationScript() { withDb { expectException { - MigrationUtils.generateMigrationScript(scriptDirectory = "src/test/resources", scriptName = "V2__Test") + MigrationUtils.generateMigrationScript(scriptDirectory = "src/test/resources", scriptName = "V2__Test", withLogs = false) } } } @@ -131,7 +151,7 @@ class DatabaseMigrationTests : DatabaseTestsBase() { assertNull(primaryKey) val expected = "ALTER TABLE ${tableName.inProperCase()} ADD PRIMARY KEY (${noPKTable.bar.nameInDatabaseCase()})" - val statements = MigrationUtils.statementsRequiredForDatabaseMigration(singlePKTable) + val statements = MigrationUtils.statementsRequiredForDatabaseMigration(singlePKTable, withLogs = false) assertEquals(expected, statements.single()) } finally { SchemaUtils.drop(noPKTable) @@ -157,7 +177,7 @@ class DatabaseMigrationTests : DatabaseTestsBase() { } SchemaUtils.create(table) - val actual = MigrationUtils.statementsRequiredForDatabaseMigration(table) + val actual = MigrationUtils.statementsRequiredForDatabaseMigration(table, withLogs = false) assertEqualLists(emptyList(), actual) } finally { SchemaUtils.drop(table) @@ -177,7 +197,7 @@ class DatabaseMigrationTests : DatabaseTestsBase() { SchemaUtils.create(quotedTable) assertTrue(quotedTable.exists()) - val statements = MigrationUtils.statementsRequiredForDatabaseMigration(quotedTable) + val statements = MigrationUtils.statementsRequiredForDatabaseMigration(quotedTable, withLogs = false) assertTrue(statements.isEmpty()) } finally { SchemaUtils.drop(quotedTable) @@ -210,7 +230,7 @@ class DatabaseMigrationTests : DatabaseTestsBase() { SchemaUtils.create(testTableWithTwoIndices) assertTrue(testTableWithTwoIndices.exists()) - val statements = MigrationUtils.statementsRequiredForDatabaseMigration(testTableWithOneIndex) + val statements = MigrationUtils.statementsRequiredForDatabaseMigration(testTableWithOneIndex, withLogs = false) assertEquals(1, statements.size) } finally { SchemaUtils.drop(testTableWithTwoIndices) @@ -240,11 +260,585 @@ class DatabaseMigrationTests : DatabaseTestsBase() { SchemaUtils.create(testTableWithIndex) assertTrue(testTableWithIndex.exists()) - val statements = MigrationUtils.statementsRequiredForDatabaseMigration(testTableWithoutIndex) + val statements = MigrationUtils.statementsRequiredForDatabaseMigration(testTableWithoutIndex, withLogs = false) assertEquals(1, statements.size) } finally { SchemaUtils.drop(testTableWithIndex) } } } + + @Test + fun testAddAutoIncrementToExistingColumn() { + val tableWithoutAutoIncrement = object : IdTable("test_table") { + override val id: Column> = long("id").entityId() + } + val tableWithAutoIncrement = object : IdTable("test_table") { + override val id: Column> = long("id").autoIncrement().entityId() + } + + withTables(excludeSettings = listOf(TestDB.SQLITE), tableWithoutAutoIncrement) { testDb -> + assertEquals(0, MigrationUtils.statementsRequiredForDatabaseMigration(tableWithoutAutoIncrement, withLogs = false).size) + + val statements = MigrationUtils.statementsRequiredForDatabaseMigration(tableWithAutoIncrement, withLogs = false) + when (testDb) { + TestDB.POSTGRESQL, TestDB.POSTGRESQLNG -> { + assertEquals(3, statements.size) + assertEquals("CREATE SEQUENCE IF NOT EXISTS test_table_id_seq START WITH 1 MINVALUE 1 MAXVALUE 9223372036854775807", statements[0]) + assertEquals("ALTER TABLE test_table ALTER COLUMN id SET DEFAULT nextval('test_table_id_seq')", statements[1]) + assertEquals("ALTER SEQUENCE test_table_id_seq OWNED BY test_table.id", statements[2]) + } + TestDB.SQLSERVER -> { + assertEquals(3, statements.size) + assertEquals("ALTER TABLE test_table ADD NEW_id BIGINT IDENTITY(1,1)", statements[0]) + assertEquals("ALTER TABLE test_table DROP COLUMN id", statements[1]) + assertEquals("EXEC sp_rename 'test_table.NEW_id', 'id', 'COLUMN'", statements[2]) + } + TestDB.ORACLE, TestDB.H2_V2_ORACLE -> { + assertEquals(1, statements.size) + assertEquals("CREATE SEQUENCE test_table_id_seq START WITH 1 MINVALUE 1 MAXVALUE 9223372036854775807", statements[0]) + } + else -> { + assertEquals(1, statements.size) + val alterColumnWord = if (currentDialectTest is MysqlDialect) "MODIFY" else "ALTER" + assertTrue(statements[0].startsWith("ALTER TABLE test_table $alterColumnWord COLUMN id ", ignoreCase = true)) + } + } + } + } + + @Test + fun testAddAutoIncrementWithSequenceNameToExistingColumn() { + val sequenceName = "custom_sequence" + val tableWithoutAutoIncrement = object : IdTable("test_table") { + override val id: Column> = long("id").entityId() + } + val tableWithAutoIncrementSequenceName = object : IdTable("test_table") { + override val id: Column> = long("id").autoIncrement(sequenceName).entityId() + } + + withTables(excludeSettings = listOf(TestDB.SQLITE), tableWithoutAutoIncrement) { + assertEquals(0, MigrationUtils.statementsRequiredForDatabaseMigration(tableWithoutAutoIncrement, withLogs = false).size) + + val statements = MigrationUtils.statementsRequiredForDatabaseMigration(tableWithAutoIncrementSequenceName, withLogs = false) + assertEquals(1, statements.size) + if (currentDialectTest.supportsCreateSequence) { + assertEquals( + "CREATE SEQUENCE${" IF NOT EXISTS".takeIf { currentDialectTest.supportsIfNotExists } ?: ""} " + + "$sequenceName START WITH 1 MINVALUE 1 MAXVALUE 9223372036854775807", + statements[0] + ) + } else { + val alterColumnWord = if (currentDialectTest is MysqlDialect) "MODIFY" else "ALTER" + assertTrue(statements[0].equals("ALTER TABLE TEST_TABLE $alterColumnWord COLUMN ID BIGINT AUTO_INCREMENT NOT NULL", ignoreCase = true)) + } + } + } + + @Test + fun testAddAutoIncrementWithCustomSequenceToExistingColumn() { + val sequence = Sequence( + name = "my_sequence", + startWith = 4, + incrementBy = 2, + minValue = 1, + maxValue = 100, + cycle = true, + cache = 20 + ) + val tableWithoutAutoIncrement = object : IdTable("test_table") { + override val id: Column> = long("id").entityId() + } + val tableWithAutoIncrementCustomSequence = object : IdTable("test_table") { + override val id: Column> = long("id").autoIncrement(sequence).entityId() + } + + withDb(excludeSettings = listOf(TestDB.SQLITE)) { + if (currentDialectTest.supportsCreateSequence) { + try { + SchemaUtils.create(tableWithoutAutoIncrement) + + assertEquals(0, MigrationUtils.statementsRequiredForDatabaseMigration(tableWithoutAutoIncrement, withLogs = false).size) + + val statements = MigrationUtils.statementsRequiredForDatabaseMigration(tableWithAutoIncrementCustomSequence, withLogs = false) + assertEquals(1, statements.size) + assertEquals( + "CREATE SEQUENCE${" IF NOT EXISTS".takeIf { currentDialectTest.supportsIfNotExists } ?: ""} " + + "${sequence.name} START WITH 4 INCREMENT BY 2 MINVALUE 1 MAXVALUE 100 CYCLE CACHE 20", + statements[0] + ) + } finally { + SchemaUtils.drop(tableWithoutAutoIncrement) + } + } + } + } + + @Test + fun testDropAutoIncrementOnExistingColumn() { + val tableWithAutoIncrement = object : IdTable("test_table") { + override val id: Column> = long("id").autoIncrement().entityId() + + override val primaryKey = PrimaryKey(id) + } + val tableWithoutAutoIncrement = object : IdTable("test_table") { + override val id: Column> = long("id").entityId() + + override val primaryKey = PrimaryKey(id) + } + + withTables(excludeSettings = listOf(TestDB.SQLITE), tableWithAutoIncrement) { testDb -> + assertEquals(0, MigrationUtils.statementsRequiredForDatabaseMigration(tableWithAutoIncrement, withLogs = false).size) + + val statements = MigrationUtils.statementsRequiredForDatabaseMigration(tableWithoutAutoIncrement, withLogs = false) + when (testDb) { + TestDB.POSTGRESQL, TestDB.POSTGRESQLNG -> { + assertEquals(2, statements.size) + assertEquals("ALTER TABLE test_table ALTER COLUMN id TYPE BIGINT", statements[0]) + assertEquals("DROP SEQUENCE IF EXISTS test_table_id_seq", statements[1]) + } + TestDB.ORACLE, TestDB.H2_V2_ORACLE -> { + assertEquals(1, statements.size) + assertTrue(statements[0].equals("DROP SEQUENCE TEST_TABLE_ID_SEQ", ignoreCase = true)) + } + else -> { + assertEquals(1, statements.size) + val alterColumnWord = if (currentDialectTest is MysqlDialect) "MODIFY" else "ALTER" + assertTrue(statements[0].equals("ALTER TABLE test_table $alterColumnWord COLUMN id BIGINT", ignoreCase = true)) + } + } + } + } + + @Test + fun testAddSequenceNameToExistingAutoIncrementColumn() { + val sequenceName = "custom_sequence" + val tableWithAutoIncrement = object : IdTable("test_table") { + override val id: Column> = long("id").autoIncrement().entityId() + + override val primaryKey = PrimaryKey(id) + } + val tableWithAutoIncrementSequenceName = object : IdTable("test_table") { + override val id: Column> = long("id").autoIncrement(sequenceName).entityId() + + override val primaryKey = PrimaryKey(id) + } + + withDb(excludeSettings = listOf(TestDB.SQLITE)) { testDb -> + if (currentDialectTest.supportsCreateSequence) { + try { + SchemaUtils.create(tableWithAutoIncrement) + + assertEquals(0, MigrationUtils.statementsRequiredForDatabaseMigration(tableWithAutoIncrement, withLogs = false).size) + + val statements = MigrationUtils.statementsRequiredForDatabaseMigration(tableWithAutoIncrementSequenceName, withLogs = false) + assertEquals( + "CREATE SEQUENCE${" IF NOT EXISTS".takeIf { currentDialectTest.supportsIfNotExists } ?: ""} " + + "$sequenceName START WITH 1 MINVALUE 1 MAXVALUE 9223372036854775807", + statements[0] + ) + when (testDb) { + TestDB.POSTGRESQL, TestDB.POSTGRESQLNG -> { + assertEquals(3, statements.size) + assertEquals("ALTER TABLE test_table ALTER COLUMN id TYPE BIGINT, ALTER COLUMN id DROP DEFAULT", statements[1]) + assertEquals("DROP SEQUENCE IF EXISTS test_table_id_seq", statements[2]) + } + TestDB.ORACLE, TestDB.H2_V2_ORACLE -> { + assertTrue(statements[1].equals("DROP SEQUENCE TEST_TABLE_ID_SEQ", ignoreCase = true)) + } + else -> { + val alterColumnWord = if (currentDialectTest is MysqlDialect) "MODIFY" else "ALTER" + assertTrue(statements[1].startsWith("ALTER TABLE test_table $alterColumnWord COLUMN id BIGINT", ignoreCase = true)) + } + } + } finally { + SchemaUtils.drop(tableWithAutoIncrement) + } + } + } + } + + @Test + fun testAddCustomSequenceToExistingAutoIncrementColumn() { + val sequence = Sequence( + name = "my_sequence", + startWith = 4, + incrementBy = 2, + minValue = 1, + maxValue = 100, + cycle = true, + cache = 20 + ) + val tableWithAutoIncrement = object : IdTable("test_table") { + override val id: Column> = long("id").autoIncrement().entityId() + } + val tableWithAutoIncrementCustomSequence = object : IdTable("test_table") { + override val id: Column> = long("id").autoIncrement(sequence).entityId() + } + + withDb(excludeSettings = listOf(TestDB.SQLITE)) { testDb -> + if (currentDialectTest.supportsCreateSequence) { + try { + SchemaUtils.create(tableWithAutoIncrement) + + assertEquals(0, MigrationUtils.statementsRequiredForDatabaseMigration(tableWithAutoIncrement, withLogs = false).size) + + val statements = MigrationUtils.statementsRequiredForDatabaseMigration(tableWithAutoIncrementCustomSequence, withLogs = false) + assertEquals( + "CREATE SEQUENCE${" IF NOT EXISTS".takeIf { currentDialectTest.supportsIfNotExists } ?: ""} " + + "${sequence.name} START WITH 4 INCREMENT BY 2 MINVALUE 1 MAXVALUE 100 CYCLE CACHE 20", + statements[0] + ) + when (testDb) { + TestDB.POSTGRESQL, TestDB.POSTGRESQLNG -> { + assertEquals(3, statements.size) + assertEquals("ALTER TABLE test_table ALTER COLUMN id TYPE BIGINT, ALTER COLUMN id DROP DEFAULT", statements[1]) + assertEquals("DROP SEQUENCE IF EXISTS test_table_id_seq", statements[2]) + } + TestDB.ORACLE, TestDB.H2_V2_ORACLE -> { + assertEquals(2, statements.size) + assertTrue(statements[1].equals("DROP SEQUENCE test_table_id_seq", ignoreCase = true)) + } + else -> { + assertEquals(2, statements.size) + val alterColumnWord = if (currentDialectTest is MysqlDialect) "MODIFY" else "ALTER" + assertTrue(statements[1].startsWith("ALTER TABLE test_table $alterColumnWord COLUMN id BIGINT", ignoreCase = true)) + } + } + } finally { + SchemaUtils.drop(tableWithAutoIncrement) + } + } + } + } + + @Test + fun testDropAutoIncrementWithSequenceNameOnExistingColumn() { + val sequenceName = "custom_sequence" + val tableWithAutoIncrementSequenceName = object : IdTable("test_table") { + override val id: Column> = long("id").autoIncrement(sequenceName).entityId() + } + val tableWithoutAutoIncrement = object : IdTable("test_table") { + override val id: Column> = long("id").entityId() + } + + withDb(excludeSettings = listOf(TestDB.SQLITE)) { testDb -> + if (currentDialectTest.supportsCreateSequence) { + try { + SchemaUtils.create(tableWithAutoIncrementSequenceName) + + assertEquals(0, MigrationUtils.statementsRequiredForDatabaseMigration(tableWithAutoIncrementSequenceName, withLogs = false).size) + + val statements = MigrationUtils.statementsRequiredForDatabaseMigration(tableWithoutAutoIncrement, withLogs = false) + when (testDb) { + TestDB.H2_V1 -> { + assertEquals(0, statements.size) + } + else -> { + assertEquals(1, statements.size) + assertTrue( + statements[0].equals( + "DROP SEQUENCE${" IF EXISTS".takeIf { currentDialectTest.supportsIfNotExists } ?: ""} $sequenceName", + ignoreCase = true + ) + ) + } + } + } finally { + SchemaUtils.drop(tableWithAutoIncrementSequenceName) + } + } + } + } + + @Test + fun testDropSequenceNameOnExistingAutoIncrementColumn() { + val sequenceName = "custom_sequence" + val tableWithAutoIncrementSequenceName = object : IdTable("test_table") { + override val id: Column> = long("id").autoIncrement(sequenceName).entityId() + } + val tableWithAutoIncrement = object : IdTable("test_table") { + override val id: Column> = long("id").autoIncrement().entityId() + } + + withDb(excludeSettings = listOf(TestDB.SQLITE)) { testDb -> + if (currentDialectTest.supportsCreateSequence) { + try { + SchemaUtils.create(tableWithAutoIncrementSequenceName) + + assertEquals(0, MigrationUtils.statementsRequiredForDatabaseMigration(tableWithAutoIncrementSequenceName).size) + + val statements = MigrationUtils.statementsRequiredForDatabaseMigration(tableWithAutoIncrement, withLogs = false) + when (testDb) { + TestDB.POSTGRESQL, TestDB.POSTGRESQLNG -> { + assertEquals(4, statements.size) + assertEquals("CREATE SEQUENCE IF NOT EXISTS test_table_id_seq START WITH 1 MINVALUE 1 MAXVALUE 9223372036854775807", statements[0]) + assertEquals("ALTER TABLE test_table ALTER COLUMN id SET DEFAULT nextval('test_table_id_seq')", statements[1]) + assertEquals("ALTER SEQUENCE test_table_id_seq OWNED BY test_table.id", statements[2]) + assertEquals("DROP SEQUENCE IF EXISTS $sequenceName", statements[3]) + } + TestDB.SQLSERVER -> { + assertEquals(4, statements.size) + assertEquals("ALTER TABLE test_table ADD NEW_id BIGINT IDENTITY(1,1)", statements[0]) + assertEquals("ALTER TABLE test_table DROP COLUMN id", statements[1]) + assertEquals("EXEC sp_rename 'test_table.NEW_id', 'id', 'COLUMN'", statements[2]) + assertEquals("DROP SEQUENCE $sequenceName", statements[3]) + } + TestDB.ORACLE, TestDB.H2_V2_ORACLE -> { + assertEquals(2, statements.size) + assertEquals("CREATE SEQUENCE test_table_id_seq START WITH 1 MINVALUE 1 MAXVALUE 9223372036854775807", statements[0]) + assertTrue(statements[1].equals("DROP SEQUENCE $sequenceName", ignoreCase = true)) + } + TestDB.H2_V1 -> { + assertEquals(1, statements.size) + assertEquals("ALTER TABLE TEST_TABLE ALTER COLUMN ID BIGINT AUTO_INCREMENT NOT NULL", statements[0]) + } + else -> { + assertEquals(2, statements.size) + assertTrue(statements[0].startsWith("ALTER TABLE TEST_TABLE ALTER COLUMN ID", ignoreCase = true)) + assertTrue( + statements[1].equals( + "DROP SEQUENCE${" IF EXISTS".takeIf { currentDialectTest.supportsIfNotExists } ?: ""} $sequenceName", + ignoreCase = true + ) + ) + } + } + } finally { + SchemaUtils.drop(tableWithAutoIncrementSequenceName) + } + } + } + } + + @Test + fun testAddCustomSequenceToExistingAutoIncrementColumnWithSequenceName() { + val sequenceName = "custom_sequence" + val sequence = Sequence( + name = "my_sequence", + startWith = 4, + incrementBy = 2, + minValue = 1, + maxValue = 100, + cycle = true, + cache = 20 + ) + val tableWithAutoIncrementSequenceName = object : IdTable("test_table") { + override val id: Column> = long("id").autoIncrement(sequenceName).entityId() + } + val tableWithAutoIncrementCustomSequence = object : IdTable("test_table") { + override val id: Column> = long("id").autoIncrement(sequence).entityId() + } + + withDb(excludeSettings = listOf(TestDB.SQLITE)) { testDb -> + if (currentDialectTest.supportsCreateSequence) { + try { + SchemaUtils.create(tableWithAutoIncrementSequenceName) + + assertEquals(0, MigrationUtils.statementsRequiredForDatabaseMigration(tableWithAutoIncrementSequenceName, withLogs = false).size) + + val statements = MigrationUtils.statementsRequiredForDatabaseMigration(tableWithAutoIncrementCustomSequence, withLogs = false) + when (testDb) { + TestDB.H2_V1 -> { + assertEquals(1, statements.size) + assertEquals( + "CREATE SEQUENCE${" IF NOT EXISTS".takeIf { currentDialectTest.supportsIfNotExists } ?: ""} " + + "${sequence.name} START WITH 4 INCREMENT BY 2 MINVALUE 1 MAXVALUE 100 CYCLE CACHE 20", + statements[0] + ) + } + else -> { + assertEquals(2, statements.size) + assertEquals( + "CREATE SEQUENCE${" IF NOT EXISTS".takeIf { currentDialectTest.supportsIfNotExists } ?: ""} " + + "${sequence.name} START WITH 4 INCREMENT BY 2 MINVALUE 1 MAXVALUE 100 CYCLE CACHE 20", + statements[0] + ) + assertTrue( + statements[1].equals( + "DROP SEQUENCE${" IF EXISTS".takeIf { currentDialectTest.supportsIfNotExists } ?: ""} $sequenceName", + ignoreCase = true + ) + ) + } + } + } finally { + SchemaUtils.drop(tableWithAutoIncrementSequenceName) + } + } + } + } + + @Test + fun testDropAutoIncrementWithCustomSequenceOnExistingColumn() { + val sequence = Sequence( + name = "my_sequence", + startWith = 4, + incrementBy = 2, + minValue = 1, + maxValue = 100, + cycle = true, + cache = 20 + ) + val tableWithAutoIncrementCustomSequence = object : IdTable("test_table") { + override val id: Column> = long("id").autoIncrement(sequence).entityId() + } + val tableWithoutAutoIncrement = object : IdTable("test_table") { + override val id: Column> = long("id").entityId() + } + + withDb(excludeSettings = listOf(TestDB.SQLITE)) { testDb -> + if (currentDialectTest.supportsCreateSequence) { + try { + SchemaUtils.create(tableWithAutoIncrementCustomSequence) + + assertEquals(0, MigrationUtils.statementsRequiredForDatabaseMigration(tableWithAutoIncrementCustomSequence, withLogs = false).size) + + val statements = MigrationUtils.statementsRequiredForDatabaseMigration(tableWithoutAutoIncrement, withLogs = false) + when (testDb) { + TestDB.H2_V1 -> { + assertEquals(0, statements.size) + } + else -> { + assertEquals(1, statements.size) + assertTrue( + statements[0].equals( + "DROP SEQUENCE${" IF EXISTS".takeIf { currentDialectTest.supportsIfNotExists } ?: ""} ${sequence.name}", + ignoreCase = true + ) + ) + } + } + } finally { + SchemaUtils.drop(tableWithAutoIncrementCustomSequence) + } + } + } + } + + @Test + fun testDropCustomSequenceOnExistingAutoIncrementColumn() { + val sequence = Sequence( + name = "my_sequence", + startWith = 4, + incrementBy = 2, + minValue = 1, + maxValue = 100, + cycle = true, + cache = 20 + ) + val tableWithAutoIncrementCustomSequence = object : IdTable("test_table") { + override val id: Column> = long("id").autoIncrement(sequence).entityId() + } + val tableWithAutoIncrement = object : IdTable("test_table") { + override val id: Column> = long("id").autoIncrement().entityId() + } + + withDb(excludeSettings = listOf(TestDB.SQLITE)) { testDb -> + if (currentDialectTest.supportsCreateSequence) { + try { + SchemaUtils.create(tableWithAutoIncrementCustomSequence) + + assertEquals(0, MigrationUtils.statementsRequiredForDatabaseMigration(tableWithAutoIncrementCustomSequence).size) + + val statements = MigrationUtils.statementsRequiredForDatabaseMigration(tableWithAutoIncrement, withLogs = false) + when (testDb) { + TestDB.POSTGRESQL, TestDB.POSTGRESQLNG -> { + assertEquals(4, statements.size) + assertEquals("CREATE SEQUENCE IF NOT EXISTS test_table_id_seq START WITH 1 MINVALUE 1 MAXVALUE 9223372036854775807", statements[0]) + assertEquals("ALTER TABLE test_table ALTER COLUMN id SET DEFAULT nextval('test_table_id_seq')", statements[1]) + assertEquals("ALTER SEQUENCE test_table_id_seq OWNED BY test_table.id", statements[2]) + assertEquals("DROP SEQUENCE IF EXISTS ${sequence.name}", statements[3]) + } + TestDB.SQLSERVER -> { + assertEquals(4, statements.size) + assertEquals("ALTER TABLE test_table ADD NEW_id BIGINT IDENTITY(1,1)", statements[0]) + assertEquals("ALTER TABLE test_table DROP COLUMN id", statements[1]) + assertEquals("EXEC sp_rename 'test_table.NEW_id', 'id', 'COLUMN'", statements[2]) + assertEquals("DROP SEQUENCE ${sequence.name}", statements[3]) + } + TestDB.ORACLE, TestDB.H2_V2_ORACLE -> { + assertEquals(2, statements.size) + assertEquals("CREATE SEQUENCE test_table_id_seq START WITH 1 MINVALUE 1 MAXVALUE 9223372036854775807", statements[0]) + assertTrue(statements[1].equals("DROP SEQUENCE ${sequence.name}", ignoreCase = true)) + } + TestDB.H2_V1 -> { + assertEquals(1, statements.size) + assertEquals("ALTER TABLE TEST_TABLE ALTER COLUMN ID BIGINT AUTO_INCREMENT NOT NULL", statements[0]) + } + else -> { + assertEquals(2, statements.size) + assertTrue(statements[0].startsWith("ALTER TABLE TEST_TABLE ALTER COLUMN ID", ignoreCase = true)) + assertTrue( + statements[1].equals( + "DROP SEQUENCE${" IF EXISTS".takeIf { currentDialectTest.supportsIfNotExists } ?: ""} ${sequence.name}", + ignoreCase = true + ) + ) + } + } + } finally { + SchemaUtils.drop(tableWithAutoIncrementCustomSequence) + } + } + } + } + + @Test + fun testAddSequenceNameToExistingAutoIncrementColumnWithCustomSequence() { + val sequenceName = "custom_sequence" + val sequence = Sequence( + name = "my_sequence", + startWith = 4, + incrementBy = 2, + minValue = 1, + maxValue = 100, + cycle = true, + cache = 20 + ) + val tableWithAutoIncrementCustomSequence = object : IdTable("test_table") { + override val id: Column> = long("id").autoIncrement(sequence).entityId() + } + val tableWithAutoIncrementSequenceName = object : IdTable("test_table") { + override val id: Column> = long("id").autoIncrement(sequenceName).entityId() + } + + withDb(excludeSettings = listOf(TestDB.SQLITE)) { testDb -> + if (currentDialectTest.supportsCreateSequence) { + try { + SchemaUtils.create(tableWithAutoIncrementCustomSequence) + + assertEquals(0, MigrationUtils.statementsRequiredForDatabaseMigration(tableWithAutoIncrementCustomSequence).size) + + val statements = MigrationUtils.statementsRequiredForDatabaseMigration(tableWithAutoIncrementSequenceName, withLogs = false) + when (testDb) { + TestDB.H2_V1 -> { + assertEquals(1, statements.size) + assertEquals( + "CREATE SEQUENCE${" IF NOT EXISTS".takeIf { currentDialectTest.supportsIfNotExists } ?: ""} " + + "$sequenceName START WITH 1 MINVALUE 1 MAXVALUE 9223372036854775807", + statements[0] + ) + } + else -> { + assertEquals(2, statements.size) + assertEquals( + "CREATE SEQUENCE${" IF NOT EXISTS".takeIf { currentDialectTest.supportsIfNotExists } ?: ""} " + + "$sequenceName START WITH 1 MINVALUE 1 MAXVALUE 9223372036854775807", + statements[0] + ) + assertTrue( + statements[1].equals( + "DROP SEQUENCE${" IF EXISTS".takeIf { currentDialectTest.supportsIfNotExists } ?: ""} ${sequence.name}", + ignoreCase = true + ) + ) + } + } + } finally { + SchemaUtils.drop(tableWithAutoIncrementCustomSequence) + } + } + } + } } diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/InsertTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/InsertTests.kt index fa1f2ddbb0..aa1a4f541e 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/InsertTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/InsertTests.kt @@ -520,7 +520,7 @@ class InsertTests : DatabaseTestsBase() { } } finally { withDb(db) { - SchemaUtils.drop() + SchemaUtils.drop(testTable) } } } @@ -558,7 +558,7 @@ class InsertTests : DatabaseTestsBase() { } } finally { withDb(db) { - SchemaUtils.drop() + SchemaUtils.drop(testTable) } } } diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/entities/ViaTest.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/entities/ViaTest.kt index 90a611c2e1..b4672e3df4 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/entities/ViaTest.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/entities/ViaTest.kt @@ -189,7 +189,7 @@ class ViaTests : DatabaseTestsBase() { @Test fun testHierarchicalReferences() { - withTables(NodeToNodes) { + withTables(NodesTable, NodeToNodes) { val root = Node.new { name = "root" } val child1 = Node.new { name = "child1" @@ -219,7 +219,7 @@ class ViaTests : DatabaseTestsBase() { @Test fun testWarmUpOnHierarchicalEntities() { - withTables(NodeToNodes) { + withTables(NodesTable, NodeToNodes) { val child1 = Node.new { name = "child1" } val child2 = Node.new { name = "child1" } val root1 = Node.new { @@ -273,7 +273,7 @@ class ViaTests : DatabaseTestsBase() { @Test fun testOrderBy() { - withTables(NodeToNodes) { + withTables(NodesTable, NodeToNodes) { val root = NodeOrdered.new { name = "root" } listOf("#3", "#0", "#2", "#4", "#1").forEach { NodeOrdered.new { @@ -291,6 +291,7 @@ class ViaTests : DatabaseTestsBase() { object Projects : IntIdTable("projects") { val name = varchar("name", 50) } + class Project(id: EntityID) : IntEntity(id) { companion object : IntEntityClass(Projects) @@ -310,6 +311,7 @@ class ViaTests : DatabaseTestsBase() { addIdColumn(task) } } + class ProjectTask(id: EntityID) : CompositeEntity(id) { companion object : CompositeEntityClass(ProjectTasks) @@ -319,6 +321,7 @@ class ViaTests : DatabaseTestsBase() { object Tasks : IntIdTable("tasks") { val title = varchar("title", 64) } + class Task(id: EntityID) : IntEntity(id) { companion object : IntEntityClass(Tasks)