diff --git a/exposed-core/api/exposed-core.api b/exposed-core/api/exposed-core.api index b22685d836..ba9c585b43 100644 --- a/exposed-core/api/exposed-core.api +++ b/exposed-core/api/exposed-core.api @@ -2014,10 +2014,9 @@ public final class org/jetbrains/exposed/sql/SchemaUtils { public static synthetic fun dropSchema$default (Lorg/jetbrains/exposed/sql/SchemaUtils;[Lorg/jetbrains/exposed/sql/Schema;ZZILjava/lang/Object;)V public final fun dropSequence ([Lorg/jetbrains/exposed/sql/Sequence;Z)V public static synthetic fun dropSequence$default (Lorg/jetbrains/exposed/sql/SchemaUtils;[Lorg/jetbrains/exposed/sql/Sequence;ZILjava/lang/Object;)V - 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 (Lorg/jetbrains/exposed/sql/SchemaUtils;[Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;Ljava/lang/String;ZILjava/lang/Object;)Ljava/io/File; public final fun listDatabases ()Ljava/util/List; public final fun listTables ()Ljava/util/List; + public final fun logTimeSpent (Ljava/lang/String;ZLkotlin/jvm/functions/Function0;)Ljava/lang/Object; 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; 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 de94fc40f2..b5fb06ac11 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 @@ -4,13 +4,12 @@ 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.io.File import java.math.BigDecimal /** 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 { + inline fun logTimeSpent(message: String, withLogs: Boolean, block: () -> R): R { return if (withLogs) { val start = System.currentTimeMillis() val answer = block() @@ -510,11 +509,11 @@ object SchemaUtils { * Returns the SQL statements that need to be executed to make the existing database schema compatible with * the table objects defined using Exposed. * - * **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`. + * + * **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. */ fun statementsRequiredToActualizeScheme(vararg tables: Table, withLogs: Boolean = true): List { val (tablesToCreate, tablesToAlter) = tables.partition { !it.exists() } @@ -832,44 +831,6 @@ object SchemaUtils { return toDrop.toList() } - /** - * @param tables The tables whose changes will be used to generate the migration script. - * @param scriptName The name to be used for the generated migration script. - * @param scriptDirectory The directory (path from repository root) in which to create the migration script. - * @param withLogs 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`. - * - * @return The generated migration script. - * - * @throws IllegalArgumentException if no argument is passed for the [tables] parameter. - * - * 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 already exists, its content will be overwritten. - */ - @ExperimentalDatabaseMigrationApi - fun generateMigrationScript(vararg tables: Table, scriptDirectory: String, scriptName: String, withLogs: Boolean = true): File { - require(tables.isNotEmpty()) { "Tables argument must not be empty" } - - val allStatements = statementsRequiredForDatabaseMigration(*tables, withLogs = withLogs) - - val migrationScript = File("$scriptDirectory/$scriptName.sql") - migrationScript.createNewFile() - - // Clear existing content - migrationScript.writeText("") - - // Append statements - allStatements.forEach { statement -> - // Add semicolon only if it's not already there - val conditionalSemicolon = if (statement.last() == ';') "" else ";" - - migrationScript.appendText("$statement$conditionalSemicolon\n") - } - - return migrationScript - } - /** * 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 @@ -881,6 +842,7 @@ object SchemaUtils { * 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`. */ + @ExperimentalDatabaseMigrationApi fun statementsRequiredForDatabaseMigration(vararg tables: Table, withLogs: Boolean = true): List { val (tablesToCreate, tablesToAlter) = tables.partition { !it.exists() } val createStatements = logTimeSpent("Preparing create tables statements", withLogs) { diff --git a/exposed-migration/api/exposed-migration.api b/exposed-migration/api/exposed-migration.api new file mode 100644 index 0000000000..814f432836 --- /dev/null +++ b/exposed-migration/api/exposed-migration.api @@ -0,0 +1,16 @@ +public final class ExposedMigrationException : java/lang/RuntimeException { + public fun (Ljava/lang/Exception;Ljava/lang/String;)V +} + +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;Ljava/lang/String;Z)Ljava/io/File; + 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;Ljava/lang/String;ZILjava/lang/Object;)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 migrate (Lorg/jetbrains/exposed/sql/Database;[Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V + public final fun migrate (Lorg/jetbrains/exposed/sql/Database;[Lorg/jetbrains/exposed/sql/Table;Ljavax/sql/DataSource;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V + public static synthetic fun migrate$default (LMigrationUtils;Lorg/jetbrains/exposed/sql/Database;[Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZILjava/lang/Object;)V + public static synthetic fun migrate$default (LMigrationUtils;Lorg/jetbrains/exposed/sql/Database;[Lorg/jetbrains/exposed/sql/Table;Ljavax/sql/DataSource;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZILjava/lang/Object;)V +} + diff --git a/exposed-migration/build.gradle.kts b/exposed-migration/build.gradle.kts new file mode 100644 index 0000000000..b2b9bc7e81 --- /dev/null +++ b/exposed-migration/build.gradle.kts @@ -0,0 +1,31 @@ +plugins { + kotlin("jvm") +} + +repositories { + mavenCentral() +} + +dependencies { + api(project(":exposed-core")) + + api(libs.flyway) + api(libs.flyway.mysql) + api(libs.flyway.oracle) + api(libs.flyway.sqlserver) + + testImplementation(project(":exposed-tests")) + + testImplementation(libs.junit) + testImplementation(kotlin("test-junit")) + + testCompileOnly(libs.pgjdbc.ng) +} + +tasks.test { + useJUnitPlatform() +} + +kotlin { + jvmToolchain(8) +} diff --git a/exposed-migration/src/main/kotlin/ExposedMigrationException.kt b/exposed-migration/src/main/kotlin/ExposedMigrationException.kt new file mode 100644 index 0000000000..67e2368e6a --- /dev/null +++ b/exposed-migration/src/main/kotlin/ExposedMigrationException.kt @@ -0,0 +1,2 @@ +/** An exception thrown when a database migration fails. */ +class ExposedMigrationException(exception: Exception, message: String) : RuntimeException(message, exception) diff --git a/exposed-migration/src/main/kotlin/MigrationUtils.kt b/exposed-migration/src/main/kotlin/MigrationUtils.kt new file mode 100644 index 0000000000..91778ce719 --- /dev/null +++ b/exposed-migration/src/main/kotlin/MigrationUtils.kt @@ -0,0 +1,222 @@ +import org.flywaydb.core.Flyway +import org.flywaydb.core.api.FlywayException +import org.flywaydb.core.api.output.MigrateResult +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.ExperimentalDatabaseMigrationApi +import org.jetbrains.exposed.sql.SchemaUtils +import org.jetbrains.exposed.sql.SchemaUtils.statementsRequiredForDatabaseMigration +import org.jetbrains.exposed.sql.Table +import org.jetbrains.exposed.sql.exposedLogger +import org.jetbrains.exposed.sql.transactions.TransactionManager +import java.io.File +import javax.sql.DataSource + +object MigrationUtils { + /** + * Applies a database migration from [oldVersion] to [newVersion]. If a migration script with the same name already + * exists, the existing one will be used as is and a new one will not be generated. This allows you to generate a + * migration script before the migration and modify it manually if needed. + * + * To generate a migration script without applying a migration, @see [generateMigrationScript]. + * + * @param tables The tables to which the migration will be applied. + * @param user The user of the database. + * @param password The password of the database. + * @param oldVersion The version to migrate from. Pending migrations up to [oldVersion] are applied before applying the migration to [newVersion]. + * @param newVersion The version to migrate to. + * @param migrationTitle The title of the migration. + * @param migrationScriptDirectory The directory in which to create the migration script. + * @param withLogs 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`. + * + * @throws ExposedMigrationException if the migration fails. + */ + @ExperimentalDatabaseMigrationApi + @Suppress("LongParameterList", "TooGenericExceptionCaught") + fun Database.migrate( + vararg tables: Table, + user: String, + password: String, + oldVersion: String, + newVersion: String, + migrationTitle: String, + migrationScriptDirectory: String, + withLogs: Boolean = true + ) { + val flyway = Flyway + .configure() + .baselineOnMigrate(true) + .baselineVersion(oldVersion) + .dataSource(url, user, password) + .locations("filesystem:$migrationScriptDirectory") + .load() + + attemptMigration( + *tables, + flyway = flyway, + oldVersion = oldVersion, + newVersion = newVersion, + migrationTitle = migrationTitle, + migrationScriptDirectory = migrationScriptDirectory, + withLogs = withLogs + ) + } + + /** + * Applies a database migration from [oldVersion] to [newVersion]. If a migration script with the same name already + * exists, the existing one will be used as is and a new one will not be generated. This allows you to generate a + * migration script before the migration and modify it manually if needed. + * + * To generate a migration script without applying a migration, @see [generateMigrationScript]. + * + * @param tables The tables to which the migration will be applied. + * @param dataSource The [DataSource] object to be used as a means of getting a connection. + * @param oldVersion The version to migrate from. Pending migrations up to [oldVersion] are applied before applying the migration to [newVersion]. + * @param newVersion The version to migrate to. + * @param migrationTitle The title of the migration. + * @param migrationScriptDirectory The directory in which to create the migration script. + * @param withLogs 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`. + * + * @throws ExposedMigrationException if the migration fails. + */ + @ExperimentalDatabaseMigrationApi + @Suppress("LongParameterList", "TooGenericExceptionCaught") + fun Database.migrate( + vararg tables: Table, + dataSource: DataSource, + oldVersion: String, + newVersion: String, + migrationTitle: String, + migrationScriptDirectory: String, + withLogs: Boolean = true + ) { + val flyway = Flyway + .configure() + .baselineOnMigrate(true) + .baselineVersion(oldVersion) + .dataSource(dataSource) + .locations("filesystem:$migrationScriptDirectory") + .load() + + attemptMigration( + *tables, + flyway = flyway, + oldVersion = oldVersion, + newVersion = newVersion, + migrationTitle = migrationTitle, + migrationScriptDirectory = migrationScriptDirectory, + withLogs = withLogs + ) + } + + @ExperimentalDatabaseMigrationApi + @Suppress("TooGenericExceptionCaught") + private fun attemptMigration( + vararg tables: Table, + flyway: Flyway, + oldVersion: String, + newVersion: String, + migrationTitle: String, + migrationScriptDirectory: String, + withLogs: Boolean + ) { + with(TransactionManager.current()) { + db.dialect.resetCaches() + + try { + val migrationScript = File("$migrationScriptDirectory/$migrationTitle.sql") + if (!migrationScript.exists()) { + generateMigrationScript( + tables = *tables, + newVersion = newVersion, + title = migrationTitle, + scriptDirectory = migrationScriptDirectory, + withLogs = withLogs + ) + } + } catch (exception: Exception) { + throw ExposedMigrationException( + exception = exception, + message = "Failed to generate migration script for migration from $oldVersion to $newVersion: ${exception.message.orEmpty()}" + ) + } + + try { + SchemaUtils.logTimeSpent("Migrating database from $oldVersion to $newVersion", withLogs = true) { + val migrateResult: MigrateResult = flyway.migrate() + if (withLogs) { + exposedLogger.info("Migration of database ${if (migrateResult.success) "succeeded" else "failed"}.") + } + } + } catch (exception: FlywayException) { + flyway.repair() + throw ExposedMigrationException( + exception = exception, + message = "Migration failed from version $oldVersion to $newVersion: ${exception.message.orEmpty()}" + ) + } + + db.dialect.resetCaches() + } + } + + /** + * This function simply generates the migration script, using the Flyway naming convention, without applying the + * migration. Its purpose is to show the user what the migration script will look like before applying the + * migration. If a migration script with the same name already exists, its content will be overwritten. + * + * @param tables The tables whose changes will be used to generate the migration script. + * @param newVersion The version to migrate to. + * @param title The title of the migration. + * @param scriptDirectory The directory in which to create the migration script. + * @param withLogs 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`. + * + * @return The generated migration script. + * + * @throws IllegalArgumentException if no argument is passed for the [tables] parameter. + */ + @ExperimentalDatabaseMigrationApi + fun generateMigrationScript(vararg tables: Table, newVersion: String, title: String, scriptDirectory: String, withLogs: Boolean = true): File { + return generateMigrationScript(*tables, scriptName = "V${newVersion}__$title", scriptDirectory = scriptDirectory, withLogs = withLogs) + } + + /** + * 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 + * already exists, its content will be overwritten. + * + * @param tables The tables whose changes will be used to generate the migration script. + * @param scriptName The name to be used for the generated migration script. + * @param scriptDirectory The directory (path from repository root) in which to create the migration script. + * @param withLogs 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`. + * + * @return The generated migration script. + * + * @throws IllegalArgumentException if no argument is passed for the [tables] parameter. + */ + @ExperimentalDatabaseMigrationApi + fun generateMigrationScript(vararg tables: Table, scriptDirectory: String, scriptName: String, withLogs: Boolean = true): File { + require(tables.isNotEmpty()) { "Tables argument must not be empty" } + + val allStatements = statementsRequiredForDatabaseMigration(*tables, withLogs = withLogs) + + val migrationScript = File("$scriptDirectory/$scriptName.sql") + migrationScript.createNewFile() + + // Clear existing content + migrationScript.writeText("") + + // Append statements + allStatements.forEach { statement -> + // Add semicolon only if it's not already there + val conditionalSemicolon = if (statement.last() == ';') "" else ";" + + migrationScript.appendText("$statement$conditionalSemicolon\n") + } + + return migrationScript + } +} diff --git a/exposed-tests/build.gradle.kts b/exposed-tests/build.gradle.kts index f5b19fe63c..5b42ba07b5 100644 --- a/exposed-tests/build.gradle.kts +++ b/exposed-tests/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { implementation(project(":exposed-jdbc")) implementation(project(":exposed-dao")) implementation(project(":exposed-kotlin-datetime")) + implementation(project(":exposed-migration")) implementation(libs.slf4j) implementation(libs.log4j.slf4j.impl) 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 c9f0956a1d..b38fd677b5 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,5 +1,9 @@ package org.jetbrains.exposed.sql.tests.shared.ddl +import MigrationUtils +import MigrationUtils.migrate +import com.impossibl.postgres.jdbc.PGDataSource +import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.ExperimentalDatabaseMigrationApi import org.jetbrains.exposed.sql.SchemaUtils import org.jetbrains.exposed.sql.Table @@ -12,8 +16,11 @@ 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.transactions.transaction import org.jetbrains.exposed.sql.vendors.PrimaryKeyMetadata +import org.junit.Assume import org.junit.Test +import org.sqlite.SQLiteDataSource import java.io.File import kotlin.properties.Delegates import kotlin.test.assertNull @@ -40,7 +47,7 @@ class DatabaseMigrationTests : DatabaseTestsBase() { try { SchemaUtils.create(noPKTable) - val script = SchemaUtils.generateMigrationScript(singlePKTable, scriptDirectory = scriptDirectory, scriptName = scriptName) + val script = MigrationUtils.generateMigrationScript(singlePKTable, scriptDirectory = scriptDirectory, scriptName = scriptName) assertTrue(script.exists()) assertEquals("src/test/resources/$scriptName.sql", script.path) @@ -86,7 +93,7 @@ class DatabaseMigrationTests : DatabaseTestsBase() { } // Generate script with the same name of initial script - val newScript = SchemaUtils.generateMigrationScript(singlePKTable, scriptDirectory = directory, scriptName = name) + val newScript = MigrationUtils.generateMigrationScript(singlePKTable, scriptDirectory = directory, scriptName = name) val expectedStatements: List = SchemaUtils.statementsRequiredForDatabaseMigration(singlePKTable) assertEquals(1, expectedStatements.size) @@ -106,7 +113,7 @@ class DatabaseMigrationTests : DatabaseTestsBase() { fun testNoTablesPassedWhenGeneratingMigrationScript() { withDb { expectException { - SchemaUtils.generateMigrationScript(scriptDirectory = "src/test/resources", scriptName = "V2__Test") + MigrationUtils.generateMigrationScript(scriptDirectory = "src/test/resources", scriptName = "V2__Test") } } } @@ -247,4 +254,159 @@ class DatabaseMigrationTests : DatabaseTestsBase() { } } } + + @Test + fun testMigration() { + val testTableWithoutIndex = object : Table("tester") { + val id = integer("id") + val name = varchar("name", length = 42) + + override val primaryKey = PrimaryKey(id) + } + + val testTableWithIndex = object : Table("tester") { + val id = integer("id") + val name = varchar("name", length = 42) + + override val primaryKey = PrimaryKey(id) + val byName = index("test_table_by_name", false, name) + } + + val migrationTitle = "AddIndex" + val migrationScriptDirectory = "src/test/resources" + + withDb(excludeSettings = listOf(TestDB.POSTGRESQLNG, TestDB.SQLITE)) { + if (!isOldMySql()) { + try { + SchemaUtils.create(testTableWithoutIndex) + assertTrue(testTableWithoutIndex.exists()) + + val database = this.db + database.migrate( + testTableWithIndex, + user = it.user, + password = it.pass, + oldVersion = "1", + newVersion = "2.0", + migrationTitle = migrationTitle, + migrationScriptDirectory = migrationScriptDirectory + ) + + assertEquals(1, currentDialectTest.existingIndices(testTableWithoutIndex).size) + } finally { + SchemaUtils.drop(testTableWithoutIndex) + assertTrue(File("$migrationScriptDirectory/V2.0__$migrationTitle.sql").delete()) + } + } + } + } + + @Test + fun testPostgreSQLNGMigration() { + Assume.assumeTrue(TestDB.POSTGRESQLNG in TestDB.enabledDialects()) + + val testTableWithoutIndex = object : Table("tester") { + val id = integer("id") + val name = varchar("name", length = 42) + + override val primaryKey = PrimaryKey(id) + } + + val testTableWithIndex = object : Table("tester") { + val id = integer("id") + val name = varchar("name", length = 42) + + override val primaryKey = PrimaryKey(id) + val byName = index("test_table_by_name", false, name) + } + + val schemaHistoryTable = object : Table("flyway_schema_history") {} + + val migrationTitle = "AddIndex" + val migrationScriptDirectory = "src/test/resources" + + val dataSource = PGDataSource().apply { + url = TestDB.POSTGRESQLNG.connection() + user = TestDB.POSTGRESQLNG.user + password = TestDB.POSTGRESQLNG.pass + } + + val database = Database.connect(dataSource) + + transaction(database) { + try { + SchemaUtils.drop(schemaHistoryTable) + + SchemaUtils.create(testTableWithoutIndex) + assertTrue(testTableWithoutIndex.exists()) + + database.migrate( + testTableWithIndex, + dataSource = dataSource, + oldVersion = "1", + newVersion = "2.0", + migrationTitle = migrationTitle, + migrationScriptDirectory = migrationScriptDirectory + ) + + assertEquals(1, currentDialectTest.existingIndices(testTableWithoutIndex).size) + } finally { + SchemaUtils.drop(testTableWithoutIndex) + SchemaUtils.drop(schemaHistoryTable) + assertTrue(File("$migrationScriptDirectory/V2.0__$migrationTitle.sql").delete()) + } + } + } + + @Test + fun testSQLiteMigration() { + Assume.assumeTrue(TestDB.SQLITE in TestDB.enabledDialects()) + + val testTableWithoutIndex = object : Table("tester") { + val id = integer("id") + val name = varchar("name", length = 42) + + override val primaryKey = PrimaryKey(id) + } + + val testTableWithIndex = object : Table("tester") { + val id = integer("id") + val name = varchar("name", length = 42) + + override val primaryKey = PrimaryKey(id) + val byName = index("test_table_by_name", false, name) + } + + val migrationTitle = "AddIndex" + val migrationScriptDirectory = "src/test/resources" + + val dataSource = SQLiteDataSource().apply { + url = "jdbc:sqlite:file:testDb.db?mode=memory" + } + + val database = Database.connect( + dataSource + ) + + transaction(database) { + try { + SchemaUtils.create(testTableWithoutIndex) + assertTrue(testTableWithoutIndex.exists()) + + database.migrate( + testTableWithIndex, + dataSource = dataSource, + oldVersion = "1", + newVersion = "2.0", + migrationTitle = migrationTitle, + migrationScriptDirectory = migrationScriptDirectory + ) + + assertEquals(1, currentDialectTest.existingIndices(testTableWithoutIndex).size) + } finally { + SchemaUtils.drop(testTableWithoutIndex) + assertTrue(File("$migrationScriptDirectory/V2.0__$migrationTitle.sql").delete()) + } + } + } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 89482f18b1..9972cd91b1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,6 +32,7 @@ javax-money = "1.1" moneta = "1.4.4" hikariCP = "4.0.3" logcaptor = "2.9.2" +flyway = "9.22.3" [libraries] jvm = { group = "org.jetbrains.kotlin.jvm", name = "org.jetbrains.kotlin.jvm.gradle.plugin", version.ref = "kotlin" } @@ -81,6 +82,11 @@ mssql = { group = "com.microsoft.sqlserver", name = "mssql-jdbc", version.ref = logcaptor = { group = "io.github.hakky54", name = "logcaptor", version.ref = "logcaptor" } +flyway = { group = "org.flywaydb", name = "flyway-core", version.ref = "flyway" } +flyway-mysql = { group = "org.flywaydb", name = "flyway-mysql", version.ref = "flyway" } +flyway-oracle = { group = "org.flywaydb", name = "flyway-database-oracle", version.ref = "flyway" } +flyway-sqlserver = { group = "org.flywaydb", name = "flyway-sqlserver", version.ref = "flyway" } + [plugins] jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 8df81ff262..13f9fb4c8d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -12,6 +12,7 @@ include("exposed-bom") include("exposed-kotlin-datetime") include("exposed-crypt") include("exposed-json") +include("exposed-migration") pluginManagement { repositories {