-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
Auto Migrations
- Loading branch information
Showing
14 changed files
with
397 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
plugins { | ||
id("library-convention") | ||
} | ||
|
||
version = libs.versions.wdater.get() | ||
|
||
dependencies { | ||
implementation(libs.exposedCore) | ||
api(projects.wdater) | ||
} |
169 changes: 169 additions & 0 deletions
169
auto-migrations/src/main/kotlin/app/meetacy/database/updater/auto/AutoMigration.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
package app.meetacy.database.updater.auto | ||
|
||
import app.meetacy.database.updater.Migration | ||
import app.meetacy.database.updater.MigrationContext | ||
import app.meetacy.database.updater.log.Logger | ||
import app.meetacy.database.updater.log.SQL | ||
import org.jetbrains.exposed.sql.* | ||
import org.jetbrains.exposed.sql.vendors.ColumnMetadata | ||
|
||
public fun AutoMigration( | ||
vararg tables: Table, | ||
fromVersion: Int, | ||
toVersion: Int = fromVersion + 1 | ||
): AutoMigration = AutoMigration(tables.toList(), fromVersion, toVersion) | ||
|
||
/** | ||
* Auto migrations do not support renaming/deleting | ||
*/ | ||
public class AutoMigration( | ||
private val tables: List<Table>, | ||
override val fromVersion: Int, | ||
override val toVersion: Int = fromVersion + 1 | ||
) : Migration { | ||
|
||
override suspend fun MigrationContext.migrate() { | ||
logger.log("Trying to auto migrate from current state") | ||
migrateTables() | ||
migrateColumns() | ||
} | ||
|
||
private fun MigrationContext.migrateTables() { | ||
logger.log("Migrating tables...") | ||
val logger = logger["tables-migration"] | ||
|
||
val rawNames = transaction.db.dialect.allTablesNames() | ||
logger.log("Tables from server: $rawNames") | ||
val schema = transaction.connection.schema | ||
logger.log("Default schema: $schema") | ||
|
||
val tablesOnServer = rawNames.map { name -> name.removePrefix("$schema.") } | ||
|
||
createMissingTables(logger, tablesOnServer) | ||
} | ||
|
||
private fun MigrationContext.createMissingTables( | ||
baseLogger: Logger, | ||
tablesOnServer: List<String> | ||
) { | ||
baseLogger.log("Creating missing tables...") | ||
val logger = baseLogger["create"] | ||
|
||
val newTables = tables.filter { table -> | ||
tablesOnServer.none { name -> normalizedTableName(table) == name } | ||
} | ||
|
||
if (newTables.isEmpty()) { | ||
logger.log("No new tables were found") | ||
return | ||
} | ||
|
||
logger.log("New tables were found: $newTables, creating them...") | ||
|
||
val create = SchemaUtils.createStatements(*newTables.toTypedArray()) | ||
execInBatch(logger, create) | ||
|
||
logger.log("New tables were created") | ||
} | ||
|
||
private fun MigrationContext.migrateColumns() { | ||
logger.log("Migrating columns...") | ||
val logger = logger["columns-migration"] | ||
val columnsOnServer = transaction.db.dialect.tableColumns(*tables.toTypedArray()) | ||
createMissingColumns(logger, columnsOnServer) | ||
val updatedColumnOnServer = transaction.db.dialect.tableColumns(*tables.toTypedArray()) | ||
modifyAllColumns(logger, updatedColumnOnServer) | ||
logger.log("Migration completed!") | ||
} | ||
|
||
private fun MigrationContext.createMissingColumns( | ||
baseLogger: Logger, | ||
databaseColumns: Map<Table, List<ColumnMetadata>> | ||
) { | ||
baseLogger.log("Creating missing columns...") | ||
val parentLogger = baseLogger["create"] | ||
|
||
tables.forEach { table -> | ||
parentLogger.log("Working on: ${normalizedTableName(table)}") | ||
|
||
val logger = parentLogger[normalizedTableName(table)] | ||
val columnsOnServer = databaseColumns.getValue(table) | ||
|
||
val newColumns = table.columns.filter { column -> | ||
columnsOnServer.none { metadata -> normalizedColumnName(column) == metadata.name } | ||
} | ||
|
||
if (newColumns.isEmpty()) { | ||
logger.log("No new columns were found") | ||
return@forEach | ||
} | ||
|
||
logger.log("New columns were found: ${newColumns.map { normalizedColumnName(it) } }, creating them...") | ||
|
||
val create = newColumns.flatMap { column -> column.createStatement() } | ||
execInBatch(logger, create) | ||
|
||
logger.log("New columns were created") | ||
} | ||
} | ||
|
||
private fun MigrationContext.modifyAllColumns( | ||
baseLogger: Logger, | ||
databaseColumns: Map<Table, List<ColumnMetadata>> | ||
) { | ||
baseLogger.log("Modifying all columns...") | ||
val parentLogger = baseLogger["modify-all"] | ||
|
||
tables.forEach { table -> | ||
parentLogger.log("Working on ${normalizedTableName(table)}") | ||
val logger = parentLogger[normalizedTableName(table)] | ||
|
||
val columnsOnServer = databaseColumns.getValue(table) | ||
|
||
val modify = table.columns.map { column -> | ||
column to columnsOnServer.first { it.name == normalizedColumnName(column) } | ||
}.flatMap { (column, columnOnServer) -> | ||
// exposed doesn't support migration of auto inc | ||
if (column.columnType.isAutoInc) return@flatMap emptyList() | ||
|
||
column.modifyStatements( | ||
ColumnDiff( | ||
nullability = column.columnType.nullable != columnOnServer.nullable, | ||
autoInc = false, | ||
defaults = true, | ||
caseSensitiveName = true | ||
) | ||
) | ||
} | ||
execInBatch(logger, modify) | ||
logger.log("Columns were modified") | ||
} | ||
} | ||
|
||
private fun MigrationContext.execInBatch( | ||
baseLogger: Logger, | ||
statements: List<String> | ||
) { | ||
statements.forEach(baseLogger.SQL::log) | ||
transaction.execInBatch(statements) | ||
} | ||
|
||
private fun MigrationContext.normalizedTableName(table: Table): String { | ||
return table.nameInDatabaseCase().removePrefix("${transaction.connection.schema}.") | ||
} | ||
|
||
private fun MigrationContext.normalizedColumnName(column: Column<*>): String { | ||
return column.nameInDatabaseCase() | ||
} | ||
|
||
override val displayName: String = "AutoMigration" | ||
|
||
private fun MigrationContext.cannotMigrate(): Nothing { | ||
transaction.rollback() | ||
throw CannotAutoMigrateException() | ||
} | ||
|
||
public class CannotAutoMigrateException : RuntimeException( | ||
/* message = */"Ambiguity occurred while migrating, cannot make auto migration for current state" | ||
) | ||
} |
7 changes: 7 additions & 0 deletions
7
auto-migrations/src/main/kotlin/app/meetacy/database/updater/auto/NoOpColumnType.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package app.meetacy.database.updater.auto | ||
|
||
import org.jetbrains.exposed.sql.ColumnType | ||
|
||
internal object NoOpColumnType : ColumnType() { | ||
override fun sqlType(): String = error("No operation") | ||
} |
13 changes: 13 additions & 0 deletions
13
auto-migrations/src/main/kotlin/app/meetacy/database/updater/auto/Table.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package app.meetacy.database.updater.auto | ||
|
||
import org.jetbrains.exposed.sql.Table | ||
|
||
internal fun Table.dropColumnStatement(columnName: String): List<String> { | ||
val column = Table(tableName).registerColumn<Nothing>(columnName, NoOpColumnType) | ||
return column.dropStatement() | ||
} | ||
|
||
internal fun Table.createColumnStatement(columnName: String): List<String> { | ||
val column = Table(tableName).registerColumn<Nothing>(columnName, NoOpColumnType) | ||
return column.createStatement() | ||
} |
11 changes: 11 additions & 0 deletions
11
auto-migrations/src/main/kotlin/app/meetacy/database/updater/auto/TableName.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package app.meetacy.database.updater.auto | ||
|
||
internal data class TableName( | ||
val schema: Schema, | ||
val name: String | ||
) { | ||
sealed interface Schema { | ||
object Default : Schema | ||
data class Custom(val string: String) : Schema | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,7 +2,7 @@ | |
|
||
kotlin = "1.8.10" | ||
exposed = "0.41.1" | ||
wdater = "0.0.1" | ||
wdater = "0.0.2" | ||
|
||
[libraries] | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,3 +17,5 @@ dependencyResolutionManagement { | |
} | ||
|
||
includeBuild("build-logic") | ||
|
||
include("auto-migrations") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.