feature #2: save executed migration in DB

This commit is contained in:
2019-07-05 10:04:14 +02:00
parent 6cc3152215
commit 9c02fd21ca
12 changed files with 147 additions and 65 deletions

View File

@@ -6,6 +6,7 @@ import java.io.File
open class Function ( open class Function (
override val script: String override val script: String
) : Resource, ParametersInterface { ) : Resource, ParametersInterface {
val returns: String?
override val name: String override val name: String
override val parameters: List<Parameter> override val parameters: List<Parameter>
override var source: File? = null override var source: File? = null
@@ -21,7 +22,7 @@ open class Function (
if (queryMatch !== null) { if (queryMatch !== null) {
val functionName = queryMatch.groups.get("name")?.value?.trim() val functionName = queryMatch.groups.get("name")?.value?.trim()
val functionParameters = queryMatch.groups["params"]?.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 */ /* Create parameters definition */
val parameters = if (functionParameters !== null) { val parameters = if (functionParameters !== null) {
@@ -45,6 +46,18 @@ open class Function (
abstract class ParseException(message: String, cause: Throwable? = null): Exception(message, cause) abstract class ParseException(message: String, cause: Throwable? = null): Exception(message, cause)
class FunctionNotFound(cause: Throwable? = null): ParseException("Function not found in script", 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 { companion object {
fun build(source: File): List<Function> { fun build(source: File): List<Function> {
return source.readText() return source.readText()

View File

@@ -1,6 +1,9 @@
package fr.postgresjson.migration package fr.postgresjson.migration
import fr.postgresjson.connexion.Connection 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 java.util.*
import fr.postgresjson.definition.Function as DefinitionFunction import fr.postgresjson.definition.Function as DefinitionFunction
@@ -11,6 +14,7 @@ class Function(
override var executedAt: Date? = null override var executedAt: Date? = null
): Migration { ): Migration {
val name = up.name val name = up.name
override var doExecute: Action? = null
init { init {
if (up.name !== down.name) { if (up.name !== down.name) {
@@ -30,39 +34,44 @@ class Function(
executedAt executedAt
) )
override fun up(): Migration.Status { override fun up(): Status {
connection.exec(up.script) 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<String, MigrationEntity?>(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) 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 { connection.inTransaction {
up() up()
down() down()
it.sendQuery("ROLLBACK"); it.sendQuery("ROLLBACK");
}.join() }.join()
return Migration.Status.OK // TODO return Status.OK // TODO
} }
override fun status(): Migration.Status { override fun status(): Status {
val result = connection.inTransaction { val result = connection.inTransaction {
up() up()
down() down()
it.sendQuery("ROLLBACK") it.sendQuery("ROLLBACK")
}.join() }.join()
return Migration.Status.OK // TODO return Status.OK // TODO
}
override fun doExecute(): Boolean {
return executedAt === null
} }
} }

View File

@@ -3,6 +3,8 @@ package fr.postgresjson.migration
import com.github.jasync.sql.db.util.size import com.github.jasync.sql.db.util.size
import fr.postgresjson.connexion.Connection import fr.postgresjson.connexion.Connection
import fr.postgresjson.entity.Entity import fr.postgresjson.entity.Entity
import fr.postgresjson.migration.Migration.Action
import fr.postgresjson.migration.Migration.Status
import java.io.File import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.util.* import java.util.*
@@ -10,8 +12,7 @@ import fr.postgresjson.definition.Function as DefinitionFunction
class MigrationEntity( class MigrationEntity(
val filename: String, val filename: String,
val definition: String, val executedAt: Date?,
val executedAt: Date,
val up: String, val up: String,
val down: String, val down: String,
val version: Int val version: Int
@@ -19,13 +20,14 @@ class MigrationEntity(
interface Migration { interface Migration {
var executedAt: Date? var executedAt: Date?
var doExecute: Action?
fun up(): Status fun up(): Status
fun down(): Status fun down(): Status
fun test(): Status fun test(): Status
fun status(): Status fun status(): Status
fun doExecute(): Boolean
enum class Status(i: Int) { OK(2), UP_FAIL(0), DOWN_FAIL(1) } 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) { class Migrations(directory: File, private val connection: Connection) {
@@ -37,6 +39,17 @@ class Migrations(directory: File, private val connection: Connection) {
initDB() initDB()
getMigrationFromDB() getMigrationFromDB()
getMigrationFromDirectory(directory) 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) { private fun getMigrationFromDirectory(directory: File) {
directory.walk().filter { directory.walk().filter {
it.isDirectory it.isDirectory
}.forEach { directory -> }.forEach { subDirectory ->
directory.walk().filter { subDirectory.walk().filter {
it.isFile it.isFile
}.forEach { file -> }.forEach { file ->
if (file.name.endsWith(".up.sql")) { if (file.name.endsWith(".up.sql")) {
@@ -92,10 +105,24 @@ class Migrations(directory: File, private val connection: Connection) {
enum class Direction { UP, DOWN } enum class Direction { UP, DOWN }
class DownMigrationNotDefined(path: String, cause: FileNotFoundException): Throwable("The file $path whas not found", cause) 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) { 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 return this
} }
@@ -104,10 +131,19 @@ class Migrations(directory: File, private val connection: Connection) {
return this 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) { 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 return this
} }
@@ -123,11 +159,11 @@ class Migrations(directory: File, private val connection: Connection) {
} }
} }
fun up(): Map<String, Migration.Status> { fun up(): Map<String, Status> {
val list: MutableMap<String, Migration.Status> = mutableMapOf() val list: MutableMap<String, Status> = mutableMapOf()
queries.forEach { queries.forEach {
it.value.let { query -> it.value.let { query ->
if (query.doExecute()) { if (query.doExecute == Action.UP) {
query.up().let { status -> query.up().let { status ->
list[query.name] = status list[query.name] = status
} }
@@ -137,7 +173,7 @@ class Migrations(directory: File, private val connection: Connection) {
functions.forEach { functions.forEach {
it.value.let { function -> it.value.let { function ->
if (function.doExecute()) { if (function.doExecute == Action.UP) {
function.up().let { status -> function.up().let { status ->
list[function.name] = status list[function.name] = status
} }
@@ -148,11 +184,11 @@ class Migrations(directory: File, private val connection: Connection) {
return list.toMap() return list.toMap()
} }
fun down(): Map<String, Migration.Status> { fun down(force: Boolean = false): Map<String, Status> {
val list: MutableMap<String, Migration.Status> = mutableMapOf() val list: MutableMap<String, Status> = mutableMapOf()
queries.forEach { queries.forEach {
it.value.let { query -> it.value.let { query ->
if (query.doExecute()) { if (query.doExecute == Action.DOWN || force) {
query.down().let { status -> query.down().let { status ->
list[query.name] = status list[query.name] = status
} }
@@ -162,7 +198,7 @@ class Migrations(directory: File, private val connection: Connection) {
functions.forEach { functions.forEach {
it.value.let { function -> it.value.let { function ->
if (function.doExecute()) { if (function.doExecute == Action.DOWN || force) {
function.down().let { status -> function.down().let { status ->
list[function.name] = status list[function.name] = status
} }
@@ -173,17 +209,17 @@ class Migrations(directory: File, private val connection: Connection) {
return list.toMap() return list.toMap()
} }
fun test(): Map<Pair<String, Direction>, Migration.Status> { fun test(): Map<Pair<String, Direction>, Status> {
var list: MutableMap<Pair<String, Direction>, Migration.Status> = mutableMapOf() val list: MutableMap<Pair<String, Direction>, Status> = mutableMapOf()
connection.connect().let { connection.connect().apply {
it.sendQuery("BEGIN").join() sendQuery("BEGIN").join()
up().map { up().map {
list.set(Pair(it.key, Direction.UP), it.value) list[Pair(it.key, Direction.UP)] = it.value
} }
down().map { down(true).map {
list.set(Pair(it.key, Direction.DOWN), it.value) list[Pair(it.key, Direction.DOWN)] = it.value
} }
it.sendQuery("ROLLBACK").join() sendQuery("ROLLBACK").join()
} }
return list.toMap() return list.toMap()

View File

@@ -2,6 +2,8 @@ package fr.postgresjson.migration
import fr.postgresjson.connexion.Connection import fr.postgresjson.connexion.Connection
import fr.postgresjson.entity.Entity import fr.postgresjson.entity.Entity
import fr.postgresjson.migration.Migration.Action
import java.io.File
import java.util.* import java.util.*
class Query( class Query(
@@ -11,16 +13,27 @@ class Query(
private val connection: Connection, private val connection: Connection,
override var executedAt: Date? = null override var executedAt: Date? = null
): Migration, Entity<String?>(name) { ): Migration, Entity<String?>(name) {
override var doExecute: Action? = null
override fun up(): Migration.Status { override fun up(): Migration.Status {
connection.exec(up).join() connection.exec(up).join()
// TODO insert to migration Table
File(this::class.java.getResource("/sql/migration/insertHistory.sql").toURI()).let {
connection.selectOne<String, MigrationEntity?>(it.readText(), listOf(name, up, down))?.let { query ->
executedAt = query.executedAt
doExecute = Action.OK
}
}
return Migration.Status.OK return Migration.Status.OK
} }
override fun down(): Migration.Status { override fun down(): Migration.Status {
connection.exec(down).join() 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 return Migration.Status.OK
} }
@@ -44,8 +57,4 @@ class Query(
return Migration.Status.OK // TODO return Migration.Status.OK // TODO
} }
override fun doExecute(): Boolean {
return executedAt === null
}
} }

View File

@@ -6,6 +6,7 @@ import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.DeserializationContext import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper 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.deser.std.StdDeserializer
import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper 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(UuidEntity::class.java, EntityUuidDeserializer(collection))
module.addDeserializer(IdEntity::class.java, EntityIdDeserializer(collection)) module.addDeserializer(IdEntity::class.java, EntityIdDeserializer(collection))
mapper.registerModule(module) mapper.registerModule(module)
mapper.propertyNamingStrategy = PropertyNamingStrategy.SNAKE_CASE
} }
fun <T> serialize(source: EntityI<T>): String { fun <T> serialize(source: EntityI<T>): String {

View File

@@ -1,3 +1,3 @@
DELETE DELETE
FROM migration.history FROM migration.history
WHERE filename = :filename; WHERE filename = ?;

View File

@@ -1,2 +1,2 @@
SELECT json_object_agg(filename, f) SELECT json_agg(f order by f.version)
FROM migration.functions f; FROM migration.functions f;

View File

@@ -1,2 +1,2 @@
SELECT json_object_agg(filename, f) SELECT json_agg(h order by h.version)
FROM migration.functions f; FROM migration.history h;

View File

@@ -1,2 +1,3 @@
INSERT INTO migration.functions (filename, definition, up, down, version) INSERT INTO migration.functions as f (filename, definition, executed_at, up, down, version)
VALUES (:filename, :definition, :up, :down, :version); VALUES (?, ?, now(), ?, ?, ?)
RETURNING to_json(f);

View File

@@ -1,2 +1,3 @@
INSERT INTO migration.history (filename, up, down, version) INSERT INTO migration.history as h (filename, executed_at, up, down, version)
VALUES (:filename, :up, :down, :version); VALUES (?, now(), ?, ?, nextval('migration.version_seq'))
RETURNING to_json(h);

View File

@@ -16,8 +16,12 @@ class MigrationTest(): TestAbstract() {
fun upQuery() { fun upQuery() {
val resources = File(this::class.java.getResource("/sql/migrations").toURI()) val resources = File(this::class.java.getResource("/sql/migrations").toURI())
val m = Migrations(resources, getConnextion()) val m = Migrations(resources, getConnextion())
m.up() `should contain` Pair("1", Migration.Status.OK) m.up().let {
m.up().size `should be equal to` 1 it `should contain` Pair("1", Migration.Status.OK)
it.size `should be equal to` 1
}
m.up().size `should be equal to` 0
} }
@Test @Test
@@ -32,15 +36,22 @@ class MigrationTest(): TestAbstract() {
fun downQuery() { fun downQuery() {
val resources = File(this::class.java.getResource("/sql/migrations").toURI()) val resources = File(this::class.java.getResource("/sql/migrations").toURI())
val m = Migrations(resources, getConnextion()) val m = Migrations(resources, getConnextion())
m.down() `should contain` Pair("1", Migration.Status.OK) repeat(3) {
m.down().size `should be equal to` 1 m.down(true).let {
it `should contain` Pair("1", Migration.Status.OK)
it.size `should be equal to` 1
}
}
} }
@Test @Test
fun `test up and down migrations`() { fun `test up and down migrations`() {
val resources = File(this::class.java.getResource("/sql/real_migrations").toURI()) val resources = File(this::class.java.getResource("/sql/real_migrations").toURI())
val m = Migrations(resources, getConnextion()) Migrations(resources, getConnextion()).apply {
m.test().size `should be equal to` 2 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
}
} }
} }

View File

@@ -1,8 +1,8 @@
package fr.postgresjson package fr.postgresjson
import fr.postgresjson.connexion.Connection import fr.postgresjson.connexion.Connection
import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS
import java.io.File import java.io.File
@@ -13,14 +13,14 @@ abstract class TestAbstract {
return Connection(database = "test", username = "test", password = "test") return Connection(database = "test", username = "test", password = "test")
} }
@BeforeAll @BeforeEach
fun beforeAll() { fun beforeAll() {
val initSQL = File(this::class.java.getResource("/fixtures/init.sql").toURI()) val initSQL = File(this::class.java.getResource("/fixtures/init.sql").toURI())
val promise = getConnextion().connect().sendQuery(initSQL.readText()) val promise = getConnextion().connect().sendQuery(initSQL.readText())
promise.join() promise.join()
} }
@AfterAll @AfterEach
fun afterAll() { fun afterAll() {
val downSQL = File(this::class.java.getResource("/fixtures/down.sql").toURI()) val downSQL = File(this::class.java.getResource("/fixtures/down.sql").toURI())
getConnextion().connect().sendQuery(downSQL.readText()).join() getConnextion().connect().sendQuery(downSQL.readText()).join()