Implement begining of migration

Move Function/Query definition
This commit is contained in:
2019-06-28 15:52:05 +02:00
parent bfefcac88d
commit ebc1a67420
18 changed files with 378 additions and 78 deletions

View File

@@ -1,6 +1,7 @@
package fr.postgresjson.connexion
import com.fasterxml.jackson.core.type.TypeReference
import com.github.jasync.sql.db.Connection
import com.github.jasync.sql.db.QueryResult
import com.github.jasync.sql.db.pool.ConnectionPool
import com.github.jasync.sql.db.postgresql.PostgreSQLConnection
@@ -28,6 +29,8 @@ class Connection(
return connection
}
fun <A> inTransaction(f: (Connection) -> CompletableFuture<A>) = connect().inTransaction(f)
fun <T, R : EntityI<T?>?> selectOne(sql: String, typeReference: TypeReference<R>, values: List<Any?> = emptyList()): R? {
val future = connect().sendPreparedStatement(sql, compileArgs(values))
val json = future.get().rows[0].getString(0)

View File

@@ -5,17 +5,13 @@ import com.github.jasync.sql.db.QueryResult
import fr.postgresjson.entity.EntityI
import java.io.File
import java.util.concurrent.CompletableFuture
import kotlin.text.RegexOption.IGNORE_CASE
import kotlin.text.RegexOption.MULTILINE
import fr.postgresjson.definition.Function as DefinitionFunction
class Requester (
private val connection: Connection,
queries: List<Query> = listOf(),
functions: List<Function> = listOf())
private val queries: MutableMap<String, Query> = mutableMapOf(),
private val functions: MutableMap<String, Function> = mutableMapOf())
{
private val queries = mutableMapOf<String, Query>()
private val functions = mutableMapOf<String, Function>()
fun addQuery(name: String, query: Query): Requester {
queries[name] = query
return this
@@ -38,14 +34,14 @@ class Requester (
return this
}
fun addFunction(function: Function): Requester {
functions[function.name] = function
fun addFunction(definition: DefinitionFunction): Requester {
functions[definition.name] = Function(definition, connection)
return this
}
fun addFunction(sql: String): Requester {
getDefinitions(sql).forEach {
functions[it.name] = it
DefinitionFunction.build(sql).forEach {
functions[it.name] = Function(it, connection)
}
return this
}
@@ -64,36 +60,6 @@ class Requester (
return this
}
private fun getDefinitions(functionContent: String): List<Function> {
val functionRegex = """create .*(procedure|function) *(?<name>[^(\s]+)\s*\((?<params>(\s*((IN|OUT|INOUT|VARIADIC)?\s+)?([^\s,)]+\s+)?([^\s,)]+)(\s+(?:default\s|=)\s*[^\s,)]+)?\s*(,|(?=\))))*)\) *(?<return>RETURNS *[^ ]+)?"""
.toRegex(setOf(IGNORE_CASE, MULTILINE))
val paramsRegex = """\s*(?<param>((?<direction>IN|OUT|INOUT|VARIADIC)?\s+)?(?<name>[^\s,)]+\s+)?(?<type>[^\s,)]+)(\s+(?<default>default\s|=)\s*[^\s,)]+)?)\s*(,|$)"""
.toRegex(setOf(IGNORE_CASE, MULTILINE))
return functionRegex.findAll(functionContent).map { queryMatch ->
val functionName = queryMatch.groups["name"]?.value?.trim()
val functionParameters = queryMatch.groups["params"]?.value?.trim()
val returns = queryMatch.groups["return"]?.value?.trim()
/* Create parameters definition */
val parameters = if (functionParameters !== null) {
val matchesParams = paramsRegex.findAll(functionParameters)
matchesParams.map { paramsMatch ->
Function.Parameter(
paramsMatch.groups["name"]!!.value.trim(),
paramsMatch.groups["type"]!!.value.trim(),
paramsMatch.groups["direction"]?.value?.trim(),
paramsMatch.groups["default"]?.value?.trim())
}.toList()
} else {
listOf()
}
Function(functionName!!, parameters, connection)
}.toList()
}
fun getFunction(name: String): Function {
if (functions[name] === null) {
throw Exception("No function defined for $name")
@@ -103,80 +69,59 @@ class Requester (
fun getQuery(path: String): Query {
if (queries[path] === null) {
throw Exception("No query defined for $path")
throw Exception("No query defined in $path")
}
return queries[path]!!
}
class Query(private val sql: String, private val connection : Connection) {
class Query(private val sql: String, override val connection : Connection): Executable {
override fun toString(): String {
return sql
}
fun <T, R : EntityI<T?>?> selectOne(typeReference: TypeReference<R>, values: List<Any?> = emptyList()): R? {
override fun <T, R : EntityI<T?>?> selectOne(typeReference: TypeReference<R>, values: List<Any?>): R? {
return connection.selectOne(this.toString(), typeReference, values)
}
inline fun <T, reified R : EntityI<T?>?> selectOne(values: List<Any?> = emptyList()): R? = selectOne(object: TypeReference<R>() {}, values)
fun <T, R : List<EntityI<T?>?>> select(typeReference: TypeReference<R>, values: List<Any?> = emptyList()): R? {
override fun <T, R : List<EntityI<T?>?>> select(typeReference: TypeReference<R>, values: List<Any?>): R? {
return connection.select(this.toString(), typeReference, values)
}
inline fun <T, reified R : List<EntityI<T?>?>> select(values: List<Any?> = emptyList()): R? = select(object: TypeReference<R>() {}, values)
fun exec(values: List<Any?> = emptyList()): CompletableFuture<QueryResult> {
override fun exec(values: List<Any?>): CompletableFuture<QueryResult> {
return connection.exec(sql, values)
}
}
class Function(val name: String, val parameters: List<Parameter>, private val connection : Connection) {
class Parameter(val name: String, val type: String, direction: Direction? = Direction.IN, val default: Any? = null)
{
val direction: Direction
init {
if (direction === null) {
this.direction = Direction.IN
} else {
this.direction = direction
}
}
constructor(name: String, type: String, direction: String? = "IN", default: Any? = null) : this(
name = name,
type = type,
direction = direction?.let { Direction.valueOf(direction.toUpperCase())},
default = default
)
enum class Direction { IN, OUT, INOUT }
}
class Function(val definition: DefinitionFunction, override val connection : Connection): Executable {
override fun toString(): String {
return name
return definition.name
}
fun <T, R : EntityI<T?>?> selectOne(typeReference: TypeReference<R>, values: List<String?> = emptyList()): R? {
override fun <T, R : EntityI<T?>?> selectOne(typeReference: TypeReference<R>, values: List<Any?>): R? {
val args = compileArgs(values)
val sql = "SELECT * FROM $name ($args)"
val sql = "SELECT * FROM ${definition.name} ($args)"
return connection.selectOne(sql, typeReference, values)
}
inline fun <T, reified R: EntityI<T?>?> selectOne(values: List<String?> = emptyList()): R? = selectOne(object: TypeReference<R>() {}, values)
fun <T, R : List<EntityI<T?>?>> select(typeReference: TypeReference<R>, values: List<Any?> = emptyList()): R? {
override fun <T, R : List<EntityI<T?>?>> select(typeReference: TypeReference<R>, values: List<Any?>): R? {
val args = compileArgs(values)
val sql = "SELECT * FROM $name ($args)"
val sql = "SELECT * FROM ${definition.name} ($args)"
return connection.select(sql, typeReference, values)
}
inline fun <T, reified R: List<EntityI<T?>?>> select(values: List<Any?> = emptyList()): R? = select(object: TypeReference<R>() {}, values)
fun exec(values: List<Any?> = emptyList()): CompletableFuture<QueryResult> {
override fun exec(values: List<Any?>): CompletableFuture<QueryResult> {
val args = compileArgs(values)
val sql = "SELECT * FROM $name ($args)"
val sql = "SELECT * FROM ${definition.name} ($args)"
return connection.exec(sql, values)
}
@@ -184,16 +129,27 @@ class Requester (
private fun compileArgs(values: List<Any?>): String {
val placeholders = values
.filterIndexed { index, any ->
this.parameters[index].default === null || any !== null
definition.parameters[index].default === null || any !== null
}
.mapIndexed { index, any ->
"?::" + this.parameters[index].type
"?::" + definition.parameters[index].type
}
return placeholders.joinToString(separator=", ")
}
}
interface Executable {
val connection : Connection
override fun toString(): String
fun <T, R : EntityI<T?>?> selectOne(typeReference: TypeReference<R>, values: List<Any?> = emptyList()): R?
fun <T, R : List<EntityI<T?>?>> select(typeReference: TypeReference<R>, values: List<Any?> = emptyList()): R?
fun exec(values: List<Any?> = emptyList()): CompletableFuture<QueryResult>
}
class RequesterFactory(
private val host: String = "localhost",
private val port: Int = 5432,

View File

@@ -0,0 +1,48 @@
package fr.postgresjson.definition
import java.io.File
open class Function (
override val name: String,
override val script: String,
override val parameters: List<Parameter>
) : Resource, ParametersInterface {
override var source: File? = null
companion object {
fun build(source: File): List<Function> {
return build(source.readText())
}
fun build(functionContent: String): List<Function> {
val functionRegex = """create .*(procedure|function) *(?<name>[^(\s]+)\s*\((?<params>(\s*((IN|OUT|INOUT|VARIADIC)?\s+)?([^\s,)]+\s+)?([^\s,)]+)(\s+(?:default\s|=)\s*[^\s,)]+)?\s*(,|(?=\))))*)\) *(?<return>RETURNS *[^ ]+)?"""
.toRegex(setOf(RegexOption.IGNORE_CASE, RegexOption.MULTILINE))
val paramsRegex = """\s*(?<param>((?<direction>IN|OUT|INOUT|VARIADIC)?\s+)?(?<name>[^\s,)]+\s+)?(?<type>[^\s,)]+)(\s+(?<default>default\s|=)\s*[^\s,)]+)?)\s*(,|$)"""
.toRegex(setOf(RegexOption.IGNORE_CASE, RegexOption.MULTILINE))
return functionRegex.findAll(functionContent).map { queryMatch ->
val functionName = queryMatch.groups["name"]?.value?.trim()
val functionParameters = queryMatch.groups["params"]?.value?.trim()
val returns = queryMatch.groups["return"]?.value?.trim()
/* Create parameters definition */
val parameters = if (functionParameters !== null) {
val matchesParams = paramsRegex.findAll(functionParameters)
matchesParams.map { paramsMatch ->
Parameter(
paramsMatch.groups["name"]!!.value.trim(),
paramsMatch.groups["type"]!!.value.trim(),
paramsMatch.groups["direction"]?.value?.trim(),
paramsMatch.groups["default"]?.value?.trim())
}.toList()
} else {
listOf()
}
Function(functionName!!, functionContent, parameters)
}.toList()
}
}
}

View File

@@ -0,0 +1,34 @@
package fr.postgresjson.definition
interface ParameterI {
val name: String
val type: String
val direction: Parameter.Direction
val default: String
}
class Parameter(val name: String, val type: String, direction: Direction? = Direction.IN, val default: Any? = null)
{
val direction: Direction
init {
if (direction === null) {
this.direction = Direction.IN
} else {
this.direction = direction
}
}
constructor(name: String, type: String, direction: String? = "IN", default: Any? = null) : this(
name = name,
type = type,
direction = direction?.let { Direction.valueOf(direction.toUpperCase())},
default = default
)
enum class Direction { IN, OUT, INOUT }
}
interface ParametersInterface {
val parameters: List<Parameter>
}

View File

@@ -0,0 +1,13 @@
package fr.postgresjson.definition
import java.io.File
interface Resource {
val name: String
val script: String
var source: File?
}
interface ResourceCollection {
val parameters: List<Parameter>
}

View File

@@ -0,0 +1,41 @@
package fr.postgresjson.migration
import fr.postgresjson.connexion.Connection
import fr.postgresjson.definition.Function as DefinitionFunction
class Function(
private val up: DefinitionFunction,
private val down: DefinitionFunction,
private val connection: Connection
): Migration {
enum class Status(i: Int) { OK(2), UP_FAIL(0), DOWN_FAIL(1) }
override fun up(): Int {
connection.exec(up.script)
return 1
}
override fun down(): Int {
connection.exec(down.script)
return 1
}
override fun test(): Int {
connection.inTransaction {
connection.exec(up.script)
connection.exec(down.script)
it.sendQuery("ROLLBACK");
}
return 1
}
override fun status(): Int {
val result = connection.inTransaction {
connection.exec(up.script)
connection.exec(down.script)
it.sendQuery("ROLLBACK")
}.join()
return result.rowsAffected.toInt()
}
}

View File

@@ -0,0 +1,101 @@
package fr.postgresjson.migration
import com.github.jasync.sql.db.util.size
import fr.postgresjson.connexion.Connection
import java.io.File
import java.io.FileNotFoundException
import fr.postgresjson.definition.Function as DefinitionFunction
interface Migration {
fun up(): Int
fun down(): Int
fun test(): Int
fun status(): Int
}
class Migrations(directory: File, private val connection: Connection): Migration {
private val queries: MutableList<Query> = mutableListOf()
private val functions: MutableMap<String, Function> = mutableMapOf()
init {
directory.walk().filter {
it.isDirectory
}.forEach { directory ->
directory.walk().filter {
it.isFile
}.forEach { file ->
if (file.name.endsWith(".up.sql")) {
val up = file.readText()
val down = file.path.substring(0, file.path.size - 7).let {
try {
File("$it.down.sql").readText()
} catch (e: FileNotFoundException) {
throw DownMigrationNotDefined("$it.down.sql", e)
}
}
addQuery(up, down)
} else if (file.name.endsWith(".down.sql")) {
// Nothing
} else {
val fileContent = file.readText()
addFunction(fileContent)
}
}
}
}
class DownMigrationNotDefined(path: String, cause: FileNotFoundException): Throwable("The file $path whas not found", cause)
fun addFunction(definition: DefinitionFunction): Migrations {
functions[definition.name] = Function(definition, definition, connection)
return this
}
fun addFunction(sql: String): Migrations {
DefinitionFunction.build(sql).forEach {
functions[it.name] = Function(it, it, connection)
}
return this
}
fun addQuery(up: String, down: String): Migrations {
queries.add(Query(up, down, connection))
return this
}
override fun up(): Int {
var count = 0
queries.forEach {
it.up()
++count
}
return count
}
override fun down(): Int {
var count = 0
queries.forEach {
it.down()
++count
}
return count
}
override fun test(): Int {
var count = 0
connection.inTransaction {
count += up()
count += down()
it.sendQuery("ROLLBACK");
}.join()
return count
}
override fun status(): Int {
TODO("not implemented")
}
}

View File

@@ -0,0 +1,40 @@
package fr.postgresjson.migration
import fr.postgresjson.connexion.Connection
class Query(
private val up: String,
private val down: String,
private val connection: Connection
): Migration {
enum class Status(i: Int) { OK(2), UP_FAIL(0), DOWN_FAIL(1) }
override fun up(): Int {
connection.exec(up)
return 1
}
override fun down(): Int {
connection.exec(down)
return 1
}
override fun test(): Int {
connection.inTransaction {
connection.exec(up)
connection.exec(down)
it.sendQuery("ROLLBACK");
}
return 1
}
override fun status(): Int {
val result = connection.inTransaction {
connection.exec(up)
connection.exec(down)
it.sendQuery("ROLLBACK")
}.join()
return result.rowsAffected.toInt()
}
}

View File

@@ -0,0 +1,42 @@
package fr.postgresjson
import fr.postgresjson.migration.Migrations
import org.amshove.kluent.`should be equal to`
import org.amshove.kluent.invoking
import org.amshove.kluent.shouldThrow
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import java.io.File
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class MigrationTest(): TestAbstract() {
@Test
fun upQuery() {
val resources = File(this::class.java.getResource("/sql/migrations").toURI())
val m = Migrations(resources, getConnextion())
m.up() `should be equal to` 1
}
@Test
fun `migration up Query should throw error if no down`() {
val resources = File(this::class.java.getResource("/sql/migration_without_down").toURI())
invoking {
Migrations(resources, getConnextion())
} shouldThrow Migrations.DownMigrationNotDefined::class
}
@Test
fun downQuery() {
val resources = File(this::class.java.getResource("/sql/migrations").toURI())
val m = Migrations(resources, getConnextion())
m.down() `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() `should be equal to` 2
m.test() `should be equal to` 2
}
}

View File

@@ -0,0 +1 @@
SELECT 1;

View File

@@ -0,0 +1 @@
SELECT 1;

View File

@@ -0,0 +1 @@
SELECT 1;

View File

@@ -0,0 +1 @@
DROP TABLE migration1;

View File

@@ -0,0 +1,3 @@
CREATE TABLE migration1 (
id INT
);