From 9c02fd21ca3da76ea35a01682579e27cc99fd81b Mon Sep 17 00:00:00 2001 From: Fabrice Lecomte Date: Fri, 5 Jul 2019 10:04:14 +0200 Subject: [PATCH] feature #2: save executed migration in DB --- .../fr/postgresjson/definition/Function.kt | 15 +++- .../fr/postgresjson/migration/Function.kt | 37 +++++--- .../fr/postgresjson/migration/Migrations.kt | 86 +++++++++++++------ .../kotlin/fr/postgresjson/migration/Query.kt | 21 +++-- .../fr/postgresjson/serializer/Serializer.kt | 2 + .../resources/sql/migration/deleteHistory.sql | 2 +- .../sql/migration/findAllFunction.sql | 2 +- .../sql/migration/findAllHistory.sql | 4 +- .../sql/migration/insertFunction.sql | 5 +- .../resources/sql/migration/insertHistory.sql | 5 +- .../kotlin/fr/postgresjson/MigrationTest.kt | 25 ++++-- .../kotlin/fr/postgresjson/TestAbstract.kt | 8 +- 12 files changed, 147 insertions(+), 65 deletions(-) diff --git a/src/main/kotlin/fr/postgresjson/definition/Function.kt b/src/main/kotlin/fr/postgresjson/definition/Function.kt index ba17844..4f26d64 100644 --- a/src/main/kotlin/fr/postgresjson/definition/Function.kt +++ b/src/main/kotlin/fr/postgresjson/definition/Function.kt @@ -6,6 +6,7 @@ import java.io.File open class Function ( override val script: String ) : Resource, ParametersInterface { + val returns: String? override val name: String override val parameters: List override var source: File? = null @@ -21,7 +22,7 @@ open class Function ( if (queryMatch !== null) { val functionName = queryMatch.groups.get("name")?.value?.trim() val functionParameters = queryMatch.groups["params"]?.value?.trim() - val returns = queryMatch.groups["return"]?.value?.trim() + this.returns = queryMatch.groups["return"]?.value?.trim() /* Create parameters definition */ val parameters = if (functionParameters !== null) { @@ -45,6 +46,18 @@ open class Function ( abstract class ParseException(message: String, cause: Throwable? = null): Exception(message, cause) class FunctionNotFound(cause: Throwable? = null): ParseException("Function not found in script", cause) + fun getDefinition (): String { + return "$name (" + parameters.joinToString(", ") + ") $returns" + } + + infix fun `has same definition` (other: Function): Boolean { + return other.getDefinition() == this.getDefinition() + } + + infix fun `is same` (other: Function): Boolean { + return other.script == this.script + } + companion object { fun build(source: File): List { return source.readText() diff --git a/src/main/kotlin/fr/postgresjson/migration/Function.kt b/src/main/kotlin/fr/postgresjson/migration/Function.kt index 2aea465..fe09d26 100644 --- a/src/main/kotlin/fr/postgresjson/migration/Function.kt +++ b/src/main/kotlin/fr/postgresjson/migration/Function.kt @@ -1,6 +1,9 @@ package fr.postgresjson.migration import fr.postgresjson.connexion.Connection +import fr.postgresjson.migration.Migration.Action +import fr.postgresjson.migration.Migration.Status +import java.io.File import java.util.* import fr.postgresjson.definition.Function as DefinitionFunction @@ -11,6 +14,7 @@ class Function( override var executedAt: Date? = null ): Migration { val name = up.name + override var doExecute: Action? = null init { if (up.name !== down.name) { @@ -30,39 +34,44 @@ class Function( executedAt ) - override fun up(): Migration.Status { + override fun up(): Status { connection.exec(up.script) - // TODO insert to migration Table - return Migration.Status.OK + + File(this::class.java.getResource("/sql/migration/insertFunction.sql").toURI()).let { + connection.selectOne(it.readText(), listOf(up))?.let { function -> + executedAt = function.executedAt + doExecute = Action.OK + } + } + return Status.OK } - override fun down(): Migration.Status { + override fun down(): Status { connection.exec(down.script) - // TODO insert to migration Table - return Migration.Status.OK + + File(this::class.java.getResource("/sql/migration/deleteFunction.sql").toURI()).let { + connection.exec(it.readText(), listOf(down)) + } + return Status.OK } - override fun test(): Migration.Status { + override fun test(): Status { connection.inTransaction { up() down() it.sendQuery("ROLLBACK"); }.join() - return Migration.Status.OK // TODO + return Status.OK // TODO } - override fun status(): Migration.Status { + override fun status(): Status { val result = connection.inTransaction { up() down() it.sendQuery("ROLLBACK") }.join() - return Migration.Status.OK // TODO - } - - override fun doExecute(): Boolean { - return executedAt === null + return Status.OK // TODO } } \ No newline at end of file diff --git a/src/main/kotlin/fr/postgresjson/migration/Migrations.kt b/src/main/kotlin/fr/postgresjson/migration/Migrations.kt index 69400f3..68b57d7 100644 --- a/src/main/kotlin/fr/postgresjson/migration/Migrations.kt +++ b/src/main/kotlin/fr/postgresjson/migration/Migrations.kt @@ -3,6 +3,8 @@ package fr.postgresjson.migration import com.github.jasync.sql.db.util.size import fr.postgresjson.connexion.Connection import fr.postgresjson.entity.Entity +import fr.postgresjson.migration.Migration.Action +import fr.postgresjson.migration.Migration.Status import java.io.File import java.io.FileNotFoundException import java.util.* @@ -10,8 +12,7 @@ import fr.postgresjson.definition.Function as DefinitionFunction class MigrationEntity( val filename: String, - val definition: String, - val executedAt: Date, + val executedAt: Date?, val up: String, val down: String, val version: Int @@ -19,13 +20,14 @@ class MigrationEntity( interface Migration { var executedAt: Date? + var doExecute: Action? fun up(): Status fun down(): Status fun test(): Status fun status(): Status - fun doExecute(): Boolean enum class Status(i: Int) { OK(2), UP_FAIL(0), DOWN_FAIL(1) } + enum class Action { OK, UP, DOWN} } class Migrations(directory: File, private val connection: Connection) { @@ -37,6 +39,17 @@ class Migrations(directory: File, private val connection: Connection) { initDB() getMigrationFromDB() getMigrationFromDirectory(directory) + queries.forEach { (_, query) -> + if (query.doExecute === null) { + query.doExecute = Action.DOWN + } + } + + functions.forEach { (_, function) -> + if (function.doExecute === null) { + function.doExecute = Action.DOWN + } + } } /** @@ -64,8 +77,8 @@ class Migrations(directory: File, private val connection: Connection) { private fun getMigrationFromDirectory(directory: File) { directory.walk().filter { it.isDirectory - }.forEach { directory -> - directory.walk().filter { + }.forEach { subDirectory -> + subDirectory.walk().filter { it.isFile }.forEach { file -> if (file.name.endsWith(".up.sql")) { @@ -92,10 +105,24 @@ class Migrations(directory: File, private val connection: Connection) { enum class Direction { UP, DOWN } class DownMigrationNotDefined(path: String, cause: FileNotFoundException): Throwable("The file $path whas not found", cause) - fun addFunction(definition: DefinitionFunction): Migrations { + fun addFunction(definition: DefinitionFunction, callback: (Function) -> Unit = {}): Migrations { if (functions[definition.name] === null) { - functions[definition.name] = Function(definition, definition, connection) + // TODO define down migration + functions[definition.name] = Function(definition, definition, connection).apply { + doExecute = Action.UP + } + } else { + functions[definition.name]!!.apply { + if (up `is same` definition) { + doExecute = Action.OK + } else { + doExecute = Action.UP + } + } } + + callback(functions[definition.name]!!) + return this } @@ -104,10 +131,19 @@ class Migrations(directory: File, private val connection: Connection) { return this } - fun addQuery(name: String, up: String, down: String): Migrations { + fun addQuery(name: String, up: String, down: String, callback: (Query) -> Unit = {}): Migrations { if (queries[name] === null) { - queries[name] = Query(name, up, down, connection) + queries[name] = Query(name, up, down, connection).apply { + doExecute = Action.UP + } + } else { + queries[name]!!.apply { + doExecute = Action.OK + } } + + callback(queries[name]!!) + return this } @@ -123,11 +159,11 @@ class Migrations(directory: File, private val connection: Connection) { } } - fun up(): Map { - val list: MutableMap = mutableMapOf() + fun up(): Map { + val list: MutableMap = mutableMapOf() queries.forEach { it.value.let { query -> - if (query.doExecute()) { + if (query.doExecute == Action.UP) { query.up().let { status -> list[query.name] = status } @@ -137,7 +173,7 @@ class Migrations(directory: File, private val connection: Connection) { functions.forEach { it.value.let { function -> - if (function.doExecute()) { + if (function.doExecute == Action.UP) { function.up().let { status -> list[function.name] = status } @@ -148,11 +184,11 @@ class Migrations(directory: File, private val connection: Connection) { return list.toMap() } - fun down(): Map { - val list: MutableMap = mutableMapOf() + fun down(force: Boolean = false): Map { + val list: MutableMap = mutableMapOf() queries.forEach { it.value.let { query -> - if (query.doExecute()) { + if (query.doExecute == Action.DOWN || force) { query.down().let { status -> list[query.name] = status } @@ -162,7 +198,7 @@ class Migrations(directory: File, private val connection: Connection) { functions.forEach { it.value.let { function -> - if (function.doExecute()) { + if (function.doExecute == Action.DOWN || force) { function.down().let { status -> list[function.name] = status } @@ -173,17 +209,17 @@ class Migrations(directory: File, private val connection: Connection) { return list.toMap() } - fun test(): Map, Migration.Status> { - var list: MutableMap, Migration.Status> = mutableMapOf() - connection.connect().let { - it.sendQuery("BEGIN").join() + fun test(): Map, Status> { + val list: MutableMap, Status> = mutableMapOf() + connection.connect().apply { + sendQuery("BEGIN").join() up().map { - list.set(Pair(it.key, Direction.UP), it.value) + list[Pair(it.key, Direction.UP)] = it.value } - down().map { - list.set(Pair(it.key, Direction.DOWN), it.value) + down(true).map { + list[Pair(it.key, Direction.DOWN)] = it.value } - it.sendQuery("ROLLBACK").join() + sendQuery("ROLLBACK").join() } return list.toMap() diff --git a/src/main/kotlin/fr/postgresjson/migration/Query.kt b/src/main/kotlin/fr/postgresjson/migration/Query.kt index 38736aa..0e0d7cf 100644 --- a/src/main/kotlin/fr/postgresjson/migration/Query.kt +++ b/src/main/kotlin/fr/postgresjson/migration/Query.kt @@ -2,6 +2,8 @@ package fr.postgresjson.migration import fr.postgresjson.connexion.Connection import fr.postgresjson.entity.Entity +import fr.postgresjson.migration.Migration.Action +import java.io.File import java.util.* class Query( @@ -11,16 +13,27 @@ class Query( private val connection: Connection, override var executedAt: Date? = null ): Migration, Entity(name) { + override var doExecute: Action? = null + override fun up(): Migration.Status { connection.exec(up).join() - // TODO insert to migration Table + + File(this::class.java.getResource("/sql/migration/insertHistory.sql").toURI()).let { + connection.selectOne(it.readText(), listOf(name, up, down))?.let { query -> + executedAt = query.executedAt + doExecute = Action.OK + } + } return Migration.Status.OK } override fun down(): Migration.Status { connection.exec(down).join() - // TODO insert to migration Table + + File(this::class.java.getResource("/sql/migration/deleteHistory.sql").toURI()).let { + connection.exec(it.readText(), listOf(name)) + } return Migration.Status.OK } @@ -44,8 +57,4 @@ class Query( return Migration.Status.OK // TODO } - - override fun doExecute(): Boolean { - return executedAt === null - } } \ No newline at end of file diff --git a/src/main/kotlin/fr/postgresjson/serializer/Serializer.kt b/src/main/kotlin/fr/postgresjson/serializer/Serializer.kt index df945ca..5f25a50 100644 --- a/src/main/kotlin/fr/postgresjson/serializer/Serializer.kt +++ b/src/main/kotlin/fr/postgresjson/serializer/Serializer.kt @@ -6,6 +6,7 @@ import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.databind.DeserializationContext import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.PropertyNamingStrategy import com.fasterxml.jackson.databind.deser.std.StdDeserializer import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper @@ -27,6 +28,7 @@ class Serializer(val mapper: ObjectMapper = jacksonObjectMapper()) { module.addDeserializer(UuidEntity::class.java, EntityUuidDeserializer(collection)) module.addDeserializer(IdEntity::class.java, EntityIdDeserializer(collection)) mapper.registerModule(module) + mapper.propertyNamingStrategy = PropertyNamingStrategy.SNAKE_CASE } fun serialize(source: EntityI): String { diff --git a/src/main/resources/sql/migration/deleteHistory.sql b/src/main/resources/sql/migration/deleteHistory.sql index 6eca48f..e70b301 100644 --- a/src/main/resources/sql/migration/deleteHistory.sql +++ b/src/main/resources/sql/migration/deleteHistory.sql @@ -1,3 +1,3 @@ DELETE FROM migration.history -WHERE filename = :filename; \ No newline at end of file +WHERE filename = ?; \ No newline at end of file diff --git a/src/main/resources/sql/migration/findAllFunction.sql b/src/main/resources/sql/migration/findAllFunction.sql index 0fa1da8..6eeb4bb 100644 --- a/src/main/resources/sql/migration/findAllFunction.sql +++ b/src/main/resources/sql/migration/findAllFunction.sql @@ -1,2 +1,2 @@ -SELECT json_object_agg(filename, f) +SELECT json_agg(f order by f.version) FROM migration.functions f; \ No newline at end of file diff --git a/src/main/resources/sql/migration/findAllHistory.sql b/src/main/resources/sql/migration/findAllHistory.sql index 0fa1da8..1c833e1 100644 --- a/src/main/resources/sql/migration/findAllHistory.sql +++ b/src/main/resources/sql/migration/findAllHistory.sql @@ -1,2 +1,2 @@ -SELECT json_object_agg(filename, f) -FROM migration.functions f; \ No newline at end of file +SELECT json_agg(h order by h.version) +FROM migration.history h; \ No newline at end of file diff --git a/src/main/resources/sql/migration/insertFunction.sql b/src/main/resources/sql/migration/insertFunction.sql index 5a4199a..1c40057 100644 --- a/src/main/resources/sql/migration/insertFunction.sql +++ b/src/main/resources/sql/migration/insertFunction.sql @@ -1,2 +1,3 @@ -INSERT INTO migration.functions (filename, definition, up, down, version) -VALUES (:filename, :definition, :up, :down, :version); +INSERT INTO migration.functions as f (filename, definition, executed_at, up, down, version) +VALUES (?, ?, now(), ?, ?, ?) +RETURNING to_json(f); \ No newline at end of file diff --git a/src/main/resources/sql/migration/insertHistory.sql b/src/main/resources/sql/migration/insertHistory.sql index 271b8eb..98dd96d 100644 --- a/src/main/resources/sql/migration/insertHistory.sql +++ b/src/main/resources/sql/migration/insertHistory.sql @@ -1,2 +1,3 @@ -INSERT INTO migration.history (filename, up, down, version) -VALUES (:filename, :up, :down, :version); +INSERT INTO migration.history as h (filename, executed_at, up, down, version) +VALUES (?, now(), ?, ?, nextval('migration.version_seq')) +RETURNING to_json(h); diff --git a/src/test/kotlin/fr/postgresjson/MigrationTest.kt b/src/test/kotlin/fr/postgresjson/MigrationTest.kt index 7368d29..af59604 100644 --- a/src/test/kotlin/fr/postgresjson/MigrationTest.kt +++ b/src/test/kotlin/fr/postgresjson/MigrationTest.kt @@ -16,8 +16,12 @@ class MigrationTest(): TestAbstract() { fun upQuery() { val resources = File(this::class.java.getResource("/sql/migrations").toURI()) val m = Migrations(resources, getConnextion()) - m.up() `should contain` Pair("1", Migration.Status.OK) - m.up().size `should be equal to` 1 + m.up().let { + it `should contain` Pair("1", Migration.Status.OK) + it.size `should be equal to` 1 + } + + m.up().size `should be equal to` 0 } @Test @@ -32,15 +36,22 @@ class MigrationTest(): TestAbstract() { fun downQuery() { val resources = File(this::class.java.getResource("/sql/migrations").toURI()) val m = Migrations(resources, getConnextion()) - m.down() `should contain` Pair("1", Migration.Status.OK) - m.down().size `should be equal to` 1 + repeat(3) { + m.down(true).let { + it `should contain` Pair("1", Migration.Status.OK) + it.size `should be equal to` 1 + } + } } @Test fun `test up and down migrations`() { val resources = File(this::class.java.getResource("/sql/real_migrations").toURI()) - val m = Migrations(resources, getConnextion()) - m.test().size `should be equal to` 2 - m.test().size `should be equal to` 2 + Migrations(resources, getConnextion()).apply { + test().size `should be equal to` 2 + } + Migrations(resources, getConnextion()).apply { + test().size `should be equal to` 2 + } } } \ No newline at end of file diff --git a/src/test/kotlin/fr/postgresjson/TestAbstract.kt b/src/test/kotlin/fr/postgresjson/TestAbstract.kt index 2fbca6b..7120a09 100644 --- a/src/test/kotlin/fr/postgresjson/TestAbstract.kt +++ b/src/test/kotlin/fr/postgresjson/TestAbstract.kt @@ -1,8 +1,8 @@ package fr.postgresjson import fr.postgresjson.connexion.Connection -import org.junit.jupiter.api.AfterAll -import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS import java.io.File @@ -13,14 +13,14 @@ abstract class TestAbstract { return Connection(database = "test", username = "test", password = "test") } - @BeforeAll + @BeforeEach fun beforeAll() { val initSQL = File(this::class.java.getResource("/fixtures/init.sql").toURI()) val promise = getConnextion().connect().sendQuery(initSQL.readText()) promise.join() } - @AfterAll + @AfterEach fun afterAll() { val downSQL = File(this::class.java.getResource("/fixtures/down.sql").toURI()) getConnextion().connect().sendQuery(downSQL.readText()).join()