Skip to content

Commit

Permalink
chore: Add new module for migration
Browse files Browse the repository at this point in the history
  • Loading branch information
joc-a committed Jun 10, 2024
1 parent 5d788f6 commit d620f76
Show file tree
Hide file tree
Showing 12 changed files with 342 additions and 251 deletions.
13 changes: 1 addition & 12 deletions exposed-core/api/exposed-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,6 @@ public final class org/jetbrains/exposed/exceptions/DuplicateColumnException : j
public fun <init> (Ljava/lang/String;Ljava/lang/String;)V
}

public final class org/jetbrains/exposed/exceptions/ExposedMigrationException : java/lang/RuntimeException {
public fun <init> (Ljava/lang/Exception;Ljava/lang/String;)V
}

public final class org/jetbrains/exposed/exceptions/ExposedSQLException : java/sql/SQLException {
public fun <init> (Ljava/lang/Throwable;Ljava/util/List;Lorg/jetbrains/exposed/sql/Transaction;)V
public final fun causedByQueries ()Ljava/util/List;
Expand Down Expand Up @@ -613,10 +609,6 @@ public final class org/jetbrains/exposed/sql/Database {
public final fun getVendor ()Ljava/lang/String;
public final fun getVersion ()Ljava/math/BigDecimal;
public final fun isVersionCovers (Ljava/math/BigDecimal;)Z
public final fun migrate ([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/Table;Ljavax/sql/DataSource;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V
public static synthetic fun migrate$default (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 (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
public final fun setUseNestedTransactions (Z)V
public fun toString ()Ljava/lang/String;
}
Expand Down Expand Up @@ -2022,12 +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;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 (Lorg/jetbrains/exposed/sql/SchemaUtils;[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 (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;
Expand Down
4 changes: 0 additions & 4 deletions exposed-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,4 @@ dependencies {
api(kotlin("reflect"))
api(libs.kotlinx.coroutines)
api(libs.slf4j)
api(libs.flyway)
api(libs.flyway.mysql)
api(libs.flyway.oracle)
api(libs.flyway.sqlserver)
}
159 changes: 0 additions & 159 deletions exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Database.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
package org.jetbrains.exposed.sql

import org.flywaydb.core.Flyway
import org.flywaydb.core.api.FlywayException
import org.flywaydb.core.api.output.MigrateResult
import org.jetbrains.annotations.TestOnly
import org.jetbrains.exposed.exceptions.ExposedMigrationException
import org.jetbrains.exposed.sql.statements.api.ExposedConnection
import org.jetbrains.exposed.sql.statements.api.ExposedDatabaseMetadata
import org.jetbrains.exposed.sql.transactions.ThreadLocalTransactionManager
import org.jetbrains.exposed.sql.transactions.TransactionManager
import org.jetbrains.exposed.sql.vendors.*
import java.io.File
import java.math.BigDecimal
import java.sql.Connection
import java.sql.DriverManager
Expand Down Expand Up @@ -109,160 +104,6 @@ class Database private constructor(
*/
internal var dataSourceReadOnly: Boolean = false

/**
* @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.
*
* 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.
*/
@ExperimentalDatabaseMigrationApi
@Suppress("LongParameterList", "TooGenericExceptionCaught")
fun 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()

with(TransactionManager.current()) {
db.dialect.resetCaches()

try {
val migrationScript = File("$migrationScriptDirectory/$migrationTitle.sql")
if (!migrationScript.exists()) {
SchemaUtils.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()
}
}

/**
* @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.
*
* Applies a database migration from [oldVersion] to [newVersion].
* For PostgreSQLNG, "jdbc:pgsql" in the database URL is replaced with "jdbc:postgresql" because the former is not
* supported by Flyway.
*/
@ExperimentalDatabaseMigrationApi
@Suppress("LongParameterList", "TooGenericExceptionCaught")
fun 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()

with(TransactionManager.current()) {
db.dialect.resetCaches()

try {
val migrationScript = File("$migrationScriptDirectory/$migrationTitle.sql")
if (!migrationScript.exists()) {
SchemaUtils.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()
}
}

companion object {
internal val dialects = ConcurrentHashMap<String, () -> DatabaseDialect>()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,3 @@ internal fun Transaction.throwUnsupportedException(message: String): Nothing = t
message,
db.dialect
)

/** An exception thrown when a database migration fails. */
class ExposedMigrationException(exception: Exception, message: String) : RuntimeException(message, exception)
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
internal inline fun <R> logTimeSpent(message: String, withLogs: Boolean, block: () -> R): R {
inline fun <R> logTimeSpent(message: String, withLogs: Boolean, block: () -> R): R {
return if (withLogs) {
val start = System.currentTimeMillis()
val answer = block()
Expand Down Expand Up @@ -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<String> {
val (tablesToCreate, tablesToAlter) = tables.partition { !it.exists() }
Expand Down Expand Up @@ -832,66 +831,6 @@ object SchemaUtils {
return toDrop.toList()
}

/**
* @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.
*
* This function simply generates the migration script without applying the migration. The purpose of it is to show
* the user what the migration script will look like before applying the migration.
* This function uses the Flyway naming convention when generating the migration script.
* If a migration script with the same name already exists, its content will be overwritten.
*/
@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)
}

/**
* @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
Expand All @@ -903,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<String> {
val (tablesToCreate, tablesToAlter) = tables.partition { !it.exists() }
val createStatements = logTimeSpent("Preparing create tables statements", withLogs) {
Expand Down
16 changes: 16 additions & 0 deletions exposed-migration/api/exposed-migration.api
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
public final class ExposedMigrationException : java/lang/RuntimeException {
public fun <init> (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
}

31 changes: 31 additions & 0 deletions exposed-migration/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/** An exception thrown when a database migration fails. */
class ExposedMigrationException(exception: Exception, message: String) : RuntimeException(message, exception)
Loading

0 comments on commit d620f76

Please sign in to comment.