WIP: Compiled SQL function #33
@@ -1,5 +1,6 @@
|
||||
package fr.postgresjson.connexion
|
||||
|
||||
import fr.postgresjson.definition.parse.parseFunction
|
||||
import fr.postgresjson.utils.searchSqlFiles
|
||||
import java.net.URI
|
||||
import fr.postgresjson.definition.Function as DefinitionFunction
|
||||
@@ -48,7 +49,7 @@ class Requester(
|
||||
}
|
||||
|
||||
fun addFunction(sql: String) {
|
||||
DefinitionFunction(sql)
|
||||
parseFunction(sql)
|
||||
.run { toRunnable(connection) }
|
||||
.run { functions[name] = this }
|
||||
}
|
||||
|
||||
@@ -1,345 +1,36 @@
|
||||
package fr.postgresjson.definition
|
||||
|
||||
import fr.postgresjson.definition.Parameter.Direction
|
||||
import fr.postgresjson.definition.Parameter.Direction.IN
|
||||
import fr.postgresjson.definition.parse.ScriptPart
|
||||
import fr.postgresjson.definition.parse.trimSpace
|
||||
import java.nio.file.Path
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.InvocationKind.EXACTLY_ONCE
|
||||
import kotlin.contracts.contract
|
||||
import kotlin.text.RegexOption.IGNORE_CASE
|
||||
|
||||
class Function(
|
||||
override val name: String,
|
||||
override val parameters: List<Parameter>,
|
||||
val returns: Returns,
|
||||
override val script: String,
|
||||
override val source: Path? = null,
|
||||
): Resource, ParametersInterface {
|
||||
/**
|
||||
* TEXT, INT, TEXT[], CUSTOM_TYPE => String, Int, List<String>, Any;
|
||||
* TABLE(id INT, name TEXT) => Object { id: Int; name: String };
|
||||
* SETOF TEXT => List<String>;
|
||||
* MY_TABLE.id%TYPE => Any;
|
||||
* VOID => null;
|
||||
*/
|
||||
val returns: Returns
|
||||
override val name: String
|
||||
override val parameters: List<Parameter>
|
||||
|
||||
|
||||
@JvmInline
|
||||
private value class ScriptPart(val restOfScript: String) {
|
||||
fun copy(block: (String) -> String): ScriptPart {
|
||||
return ScriptPart(block(restOfScript))
|
||||
}
|
||||
|
||||
fun removeParentheses(): ScriptPart {
|
||||
return if (restOfScript.take(1) == "(" && restOfScript.takeLast(1) == ")") {
|
||||
this.copy {
|
||||
it.drop(1).dropLast(1)
|
||||
}
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
fun isEmpty() = restOfScript.isEmpty()
|
||||
}
|
||||
|
||||
private fun emptyScriptPart(): ScriptPart = ScriptPart("")
|
||||
|
||||
private class NextScript<T>(val value: T, val restOfScript: String) {
|
||||
val nextScriptPart: ScriptPart = ScriptPart(restOfScript)
|
||||
fun isLast() = restOfScript == ""
|
||||
fun isEmptyValue() = value == "" || value == null
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the value as ScriptPart
|
||||
*/
|
||||
private fun NextScript<String>.valueAsScriptPart(): ScriptPart = ScriptPart(value)
|
||||
|
||||
init {
|
||||
ScriptPart(script)
|
||||
.getFunctionOrProcedure().trimSpace().nextScriptPart
|
||||
.getFunctionName().apply { name = value }.nextScriptPart
|
||||
.getParameters().apply { parameters = value }.nextScriptPart
|
||||
.getReturns().apply { returns = value }
|
||||
|
||||
}
|
||||
|
||||
private fun ScriptPart.getFunctionOrProcedure(): NextScript<String> {
|
||||
val result = """create\s+(?:or\s+replace\s+)?(procedure|function)\s+"""
|
||||
.toRegex()
|
||||
.find(restOfScript)
|
||||
?: throw FunctionNotFound()
|
||||
|
||||
val rest = result.range.last
|
||||
.let { cursor -> restOfScript.drop(cursor + 1) }
|
||||
|
||||
return NextScript(
|
||||
result.groups[1]!!.value,
|
||||
rest
|
||||
)
|
||||
}
|
||||
|
||||
private fun ScriptPart.getFunctionName(): NextScript<String> {
|
||||
try {
|
||||
return getNextScript { status.isNotEscaped() && afterBeginBy("(", " ", "\n") }
|
||||
} catch (e: NameMalformed) {
|
||||
throw FunctionNameMalformed(null, e)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
private inline fun ScriptPart.change(block: String.() -> String): ScriptPart {
|
||||
contract {
|
||||
callsInPlace(block, EXACTLY_ONCE)
|
||||
}
|
||||
return ScriptPart(restOfScript.run(block))
|
||||
}
|
||||
|
||||
data class Status(
|
||||
var doubleQuoted: Boolean = false, // "
|
||||
var simpleQuoted: Boolean = false, // '
|
||||
var parentheses: Int = 0, // ()
|
||||
var brackets: Int = 0, // []
|
||||
var braces: Int = 0, // {}
|
||||
) {
|
||||
fun isQuoted(): Boolean = doubleQuoted || simpleQuoted
|
||||
fun isNotQuoted(): Boolean = !isQuoted()
|
||||
fun isNotEscaped(): Boolean = isNotQuoted() && parentheses == 0 && brackets == 0 && braces == 0
|
||||
}
|
||||
|
||||
data class Context(
|
||||
val index: Int,
|
||||
val currentChar: Char,
|
||||
val status: Status,
|
||||
val script: String,
|
||||
) {
|
||||
fun afterBeginBy(vararg texts: String): Boolean = texts.any {
|
||||
script.substring(index + 1).take(it.length) == it
|
||||
}
|
||||
|
||||
val nextChar: Char? get() = script.substring(index + 1).getOrNull(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get next part of script.
|
||||
* You can define a list of characters that end the part of script. Like `(` or space.
|
||||
*/
|
||||
private fun ScriptPart.getNextScript(isEnd: Context.() -> Boolean = { false }): NextScript<String> {
|
||||
val status = Status()
|
||||
|
||||
fun String.unescape(): String {
|
||||
val first = take(1)
|
||||
val last = takeLast(1)
|
||||
return if (first == last && first in listOf("\"", "'")) {
|
||||
drop(1).dropLast(1).replace("$first$first", first)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
for ((index, c) in restOfScript.withIndex()) {
|
||||
val nextChar = restOfScript.getOrNull(index + 1)
|
||||
val prevChar = restOfScript.getOrNull(index - 1)
|
||||
if (c == '"' && (nextChar != '"' && prevChar != '"')) {
|
||||
status.doubleQuoted = !status.doubleQuoted
|
||||
} else if (c == '\'' && (nextChar != '\'' && prevChar != '\'')) {
|
||||
status.simpleQuoted = !status.simpleQuoted
|
||||
} else if (c == '(' && status.isNotQuoted()) {
|
||||
status.parentheses++
|
||||
} else if (c == ')' && status.isNotQuoted()) {
|
||||
status.parentheses--
|
||||
} else if (c == '[' && status.isNotQuoted()) {
|
||||
status.brackets++
|
||||
} else if (c == ']' && status.isNotQuoted()) {
|
||||
status.brackets--
|
||||
} else if (c == '{' && status.isNotQuoted()) {
|
||||
status.braces++
|
||||
} else if (c == '}' && status.isNotQuoted()) {
|
||||
status.braces--
|
||||
}
|
||||
|
||||
if (isEnd(Context(index, c, status.copy(), restOfScript))) {
|
||||
return NextScript(restOfScript.take(index + 1).unescape(), restOfScript.drop(index + 1))
|
||||
}
|
||||
}
|
||||
if (status.isNotEscaped()) {
|
||||
return NextScript(restOfScript.unescape().trim(), "").trimSpace()
|
||||
}
|
||||
throw ParseError()
|
||||
}
|
||||
|
||||
private fun ScriptPart.split(delimiter: String): List<ScriptPart> {
|
||||
val parts: MutableList<ScriptPart> = mutableListOf()
|
||||
var rest: ScriptPart = this
|
||||
do {
|
||||
rest = rest.trimSpace()
|
||||
.getNextScript { status.isNotEscaped() && currentChar.toString() == delimiter }
|
||||
.trimSpace()
|
||||
.also { parts.add(it.valueAsScriptPart().trimSpace().trimEnd(',')) }
|
||||
.nextScriptPart
|
||||
} while (!rest.isEmpty())
|
||||
|
||||
return parts
|
||||
}
|
||||
|
||||
private fun ScriptPart.getNextInteger(): NextScript<Int?> {
|
||||
val trimmed = restOfScript.trimStart { !it.isDigit() }
|
||||
val digits = trimmed.takeWhile { it.isDigit() }
|
||||
val restOfScript = trimmed.trimStart { it.isDigit() }
|
||||
return NextScript(digits.toIntOrNull(), restOfScript).trimSpace()
|
||||
}
|
||||
|
||||
private fun ScriptPart.getParameters(): NextScript<List<Parameter>> {
|
||||
val allParametersScript = this.getNextScript {
|
||||
currentChar == ')' && status.isNotEscaped()
|
||||
}
|
||||
val parameterList: List<Parameter> = allParametersScript
|
||||
.valueAsScriptPart()
|
||||
.removeParentheses()
|
||||
.split(",")
|
||||
.map { it.toParameter() }
|
||||
|
||||
return NextScript(parameterList, allParametersScript.restOfScript)
|
||||
}
|
||||
|
||||
private fun ScriptPart.trimSpace(): ScriptPart {
|
||||
for ((n, char) in restOfScript.withIndex()) {
|
||||
if (char !in listOf(' ', '\n', '\t')) {
|
||||
return ScriptPart(
|
||||
restOfScript.drop(n)
|
||||
)
|
||||
}
|
||||
}
|
||||
return ScriptPart(restOfScript)
|
||||
}
|
||||
|
||||
private fun <T> NextScript<T>.trimSpace(): NextScript<T> {
|
||||
val spaces = charArrayOf(' ', '\n', '\t')
|
||||
return trim(chars = spaces)
|
||||
}
|
||||
|
||||
private fun ScriptPart.trimEnd(vararg chars: Char): ScriptPart {
|
||||
return this.change { dropLastWhile { it in chars } }
|
||||
}
|
||||
|
||||
private fun <T> NextScript<T>.trim(vararg chars: Char): NextScript<T> {
|
||||
return NextScript(value, restOfScript.apply { dropWhile { it in chars } })
|
||||
}
|
||||
|
||||
private fun <T> NextScript<T>.changeValue(block: (T) -> T): NextScript<T> {
|
||||
return NextScript(block(value), restOfScript)
|
||||
}
|
||||
|
||||
private fun <T> NextScript<T>.changeScript(block: (String) -> String): NextScript<T> {
|
||||
return NextScript(value, block(restOfScript))
|
||||
}
|
||||
|
||||
private fun <T> NextScript<T>.dropOneOf(vararg endTextList: String): NextScript<T> {
|
||||
return changeScript { script ->
|
||||
endTextList
|
||||
.filter { script.startsWith(it) }
|
||||
.let { script.drop(it.size) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun ScriptPart.toParameter(): Parameter {
|
||||
var script: ScriptPart = this.trimSpace()
|
||||
return Parameter(
|
||||
direction = script.getParameterMode().apply { script = nextScriptPart }.value,
|
||||
name = script.getParameterName().trimSpace().apply { script = nextScriptPart }.value.trim(),
|
||||
type = script.getParameterType().trimSpace().apply { script = nextScriptPart }.value,
|
||||
default = script.getParameterDefault().trimSpace().apply { script = nextScriptPart }.value,
|
||||
)
|
||||
}
|
||||
|
||||
private fun ScriptPart.getParameterMode(): NextScript<Direction> {
|
||||
return when {
|
||||
restOfScript.startsWith("inout ", true) -> NextScript(Direction.INOUT, restOfScript.drop("inout ".length))
|
||||
restOfScript.startsWith("in ", true) -> NextScript(Direction.IN, restOfScript.drop("in ".length))
|
||||
restOfScript.startsWith("out ", true) -> NextScript(Direction.OUT, restOfScript.drop("out ".length))
|
||||
else -> NextScript(Direction.IN, restOfScript)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ScriptPart.getParameterName(): NextScript<String> {
|
||||
try {
|
||||
return getNextScript { afterBeginBy(" ", "\n") }
|
||||
} catch (e: NameMalformed) {
|
||||
throw ParameterNameMalformed(null, e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ScriptPart.getParameterType(): NextScript<ParameterType> {
|
||||
val fullType = try {
|
||||
val endTextList = arrayOf(" default ", "=", ")")
|
||||
getNextScript { afterBeginBy(texts = endTextList) }
|
||||
} catch (e: ParseError) {
|
||||
throw ParameterTypeMalformed(null, e)
|
||||
}
|
||||
|
||||
var rest: ScriptPart = fullType.valueAsScriptPart()
|
||||
|
||||
val name = rest
|
||||
.getNextScript { afterBeginBy("(") }
|
||||
.apply { rest = nextScriptPart }
|
||||
val precision = rest
|
||||
.getNextInteger()
|
||||
.apply { rest = nextScriptPart }
|
||||
val scale = rest
|
||||
.getNextInteger()
|
||||
.apply { rest = nextScriptPart }
|
||||
|
||||
return NextScript(
|
||||
ParameterType(
|
||||
name = name.value.trim(),
|
||||
precision = precision.value,
|
||||
scale = scale.value
|
||||
), fullType.nextScriptPart.restOfScript
|
||||
)
|
||||
}
|
||||
|
||||
private fun ScriptPart.getParameterDefault(): NextScript<String?> {
|
||||
return if (this.isEmpty() || this.restOfScript == ")") {
|
||||
NextScript(null, "")
|
||||
} else {
|
||||
"""^(\s*=\s*|\s+default\s+)(.+)\s*$"""
|
||||
.toRegex(IGNORE_CASE)
|
||||
.find(restOfScript)
|
||||
.let { it ?: throw ParameterDefaultMalformed() }
|
||||
.let { it.groups[2]!!.value }
|
||||
.let { NextScript(it, "") }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO Finalize this
|
||||
*/
|
||||
private fun ScriptPart.getReturns(): NextScript<Returns> {
|
||||
return NextScript(Returns.Void(), "")
|
||||
}
|
||||
|
||||
class FunctionNotFound(cause: Throwable? = null): Resource.ParseException("Function not found in script", cause)
|
||||
class ParameterNotFound(cause: Throwable? = null): Resource.ParseException("Parameter not found in script", cause)
|
||||
class FunctionNameMalformed(message: String? = null, cause: Throwable? = null):
|
||||
Resource.ParseException(message ?: "Function name is malformed", cause)
|
||||
|
||||
class ParameterNameMalformed(message: String? = null, cause: Throwable? = null):
|
||||
Resource.ParseException(message ?: "Parameter name is malformed", cause)
|
||||
|
||||
class ParameterTypeMalformed(message: String? = null, cause: Throwable? = null):
|
||||
Resource.ParseException(message ?: "Parameter type is malformed", cause)
|
||||
|
||||
class ParameterDefaultMalformed(message: String? = null, cause: Throwable? = null):
|
||||
Resource.ParseException(message ?: "Parameter default is malformed", cause)
|
||||
|
||||
class NameMalformed(message: String? = null, cause: Throwable? = null):
|
||||
Resource.ParseException(message ?: "Name is malformed", cause)
|
||||
|
||||
class ParseError(message: String? = null, cause: Throwable? = null):
|
||||
Resource.ParseException(message ?: "Parsing fail", cause)
|
||||
// private fun <T> NextScript<T>.changeValue(block: (T) -> T): NextScript<T> {
|
||||
// return NextScript(block(value), restOfScript)
|
||||
// }
|
||||
//
|
||||
// private fun <T> NextScript<T>.changeScript(block: (String) -> String): NextScript<T> {
|
||||
// return NextScript(value, block(restOfScript))
|
||||
// }
|
||||
//
|
||||
// private fun <T> NextScript<T>.dropOneOf(vararg endTextList: String): NextScript<T> {
|
||||
// return changeScript { script ->
|
||||
// endTextList
|
||||
// .filter { script.startsWith(it) }
|
||||
// .let { script.drop(it.size) }
|
||||
// }
|
||||
// }
|
||||
|
||||
fun getDefinition(): String = parameters
|
||||
.filter { it.direction == Direction.IN }
|
||||
.filter { it.direction == IN }
|
||||
.joinToString(", ") { it.type.toString() }
|
||||
.let { "$name ($it)" }
|
||||
|
||||
@@ -356,44 +47,4 @@ class Function(
|
||||
infix fun `is different from`(other: Function): Boolean {
|
||||
return other.script != this.script
|
||||
}
|
||||
|
||||
sealed class Returns(
|
||||
val definition: String,
|
||||
val isSetOf: Boolean,
|
||||
) {
|
||||
class Primitive(
|
||||
definition: String,
|
||||
isSetOf: Boolean,
|
||||
): Returns(definition, isSetOf) {
|
||||
val name = definition
|
||||
.trim('"')
|
||||
}
|
||||
|
||||
class PrimitiveList(
|
||||
definition: String,
|
||||
isSetOf: Boolean,
|
||||
): Returns(definition, isSetOf) {
|
||||
val name = definition
|
||||
.drop(2)
|
||||
.trim('"')
|
||||
}
|
||||
|
||||
class Table(
|
||||
definition: String,
|
||||
isSetOf: Boolean,
|
||||
val parameters: List<ParameterTable>,
|
||||
): Returns(definition, isSetOf) {
|
||||
class ParameterTable(
|
||||
override val name: String,
|
||||
override val type: ParameterType,
|
||||
): ParameterSimpleI
|
||||
}
|
||||
|
||||
class Any(
|
||||
isSetOf: Boolean,
|
||||
): Returns("any", isSetOf)
|
||||
|
||||
class Void: Returns("void", false)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package fr.postgresjson.definition
|
||||
|
||||
import fr.postgresjson.definition.parse.parseFunction
|
||||
import java.io.File
|
||||
import java.net.URL
|
||||
import java.nio.file.Path
|
||||
@@ -23,7 +24,7 @@ sealed interface Resource {
|
||||
Migration(resource, path)
|
||||
} catch (e: ParseException) {
|
||||
try {
|
||||
Function(resource, path)
|
||||
parseFunction(resource, path)
|
||||
} catch (e: ParseException) {
|
||||
try {
|
||||
Query(resource, path)
|
||||
|
||||
40
src/main/kotlin/fr/postgresjson/definition/Returns.kt
Normal file
40
src/main/kotlin/fr/postgresjson/definition/Returns.kt
Normal file
@@ -0,0 +1,40 @@
|
||||
package fr.postgresjson.definition
|
||||
|
||||
sealed class Returns(
|
||||
val definition: String,
|
||||
val isSetOf: Boolean,
|
||||
) {
|
||||
class Primitive(
|
||||
definition: String,
|
||||
isSetOf: Boolean,
|
||||
): Returns(definition, isSetOf) {
|
||||
val name = definition
|
||||
.trim('"')
|
||||
}
|
||||
|
||||
class PrimitiveList(
|
||||
definition: String,
|
||||
isSetOf: Boolean,
|
||||
): Returns(definition, isSetOf) {
|
||||
val name = definition
|
||||
.drop(2)
|
||||
.trim('"')
|
||||
}
|
||||
|
||||
class Table(
|
||||
definition: String,
|
||||
isSetOf: Boolean,
|
||||
val parameters: List<ParameterTable>,
|
||||
): Returns(definition, isSetOf) {
|
||||
class ParameterTable(
|
||||
override val name: String,
|
||||
override val type: ParameterType,
|
||||
): ParameterSimpleI
|
||||
}
|
||||
|
||||
class Any(
|
||||
isSetOf: Boolean,
|
||||
): Returns("any", isSetOf)
|
||||
|
||||
class Void: Returns("void", false)
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package fr.postgresjson.definition.parse
|
||||
|
||||
import fr.postgresjson.definition.Function
|
||||
import fr.postgresjson.definition.Parameter
|
||||
import fr.postgresjson.definition.Returns.Void
|
||||
import fr.postgresjson.definition.Parameter.Direction
|
||||
import fr.postgresjson.definition.Parameter.Direction.IN
|
||||
import fr.postgresjson.definition.Parameter.Direction.INOUT
|
||||
import fr.postgresjson.definition.Parameter.Direction.OUT
|
||||
import fr.postgresjson.definition.ParameterType
|
||||
import fr.postgresjson.definition.Resource.ParseException
|
||||
import fr.postgresjson.definition.Returns
|
||||
import java.nio.file.Path
|
||||
import kotlin.text.RegexOption.IGNORE_CASE
|
||||
|
||||
internal fun parseFunction(script: String, source: Path? = null): Function {
|
||||
val name: String
|
||||
val parameters: List<Parameter>
|
||||
val returns: Returns
|
||||
ScriptPart(script)
|
||||
.getFunctionOrProcedure().trimSpace().nextScriptPart
|
||||
.getFunctionName().apply { name = value }.nextScriptPart
|
||||
.getParameters().apply { parameters = value }.nextScriptPart
|
||||
.getReturns().apply { returns = value }
|
||||
|
||||
return Function(name, parameters, returns, script, source)
|
||||
}
|
||||
|
||||
|
||||
@Throws(FunctionNameMalformed::class)
|
||||
internal fun ScriptPart.getFunctionName(): NextScript<String> {
|
||||
try {
|
||||
return getNextScript { status.isNotEscaped() && afterBeginBy("(", " ", "\n") }
|
||||
} catch (e: ParseException) {
|
||||
throw FunctionNameMalformed(this, e)
|
||||
}
|
||||
}
|
||||
internal class FunctionNameMalformed(val script: ScriptPart, cause: Throwable? = null):
|
||||
ParseException("Function name is malformed", cause)
|
||||
|
||||
@Throws(FunctionNotFound::class)
|
||||
internal fun ScriptPart.getFunctionOrProcedure(): NextScript<String> {
|
||||
val result = """create\s+(?:or\s+replace\s+)?(procedure|function)\s+"""
|
||||
.toRegex()
|
||||
.find(restOfScript)
|
||||
?: throw FunctionNotFound(this)
|
||||
|
||||
val rest = result.range.last
|
||||
.let { cursor -> restOfScript.drop(cursor + 1) }
|
||||
|
||||
return NextScript(
|
||||
result.groups[1]!!.value,
|
||||
rest
|
||||
)
|
||||
}
|
||||
|
||||
internal class FunctionNotFound(val script: ScriptPart):
|
||||
ParseException("Function not found in script")
|
||||
|
||||
internal fun ScriptPart.getParameters(): NextScript<List<Parameter>> {
|
||||
val allParametersScript = this.getNextScript {
|
||||
currentChar == ')' && status.isNotEscaped()
|
||||
}
|
||||
val parameterList: List<Parameter> = allParametersScript
|
||||
.valueAsScriptPart()
|
||||
.removeParentheses()
|
||||
.split(",")
|
||||
.map { it.toParameter() }
|
||||
|
||||
return NextScript(parameterList, allParametersScript.restOfScript)
|
||||
}
|
||||
|
||||
private fun ScriptPart.toParameter(): Parameter {
|
||||
var script: ScriptPart = this.trimSpace()
|
||||
return Parameter(
|
||||
direction = script.getParameterMode().apply { script = nextScriptPart }.value,
|
||||
name = script.getParameterName().trimSpace().apply { script = nextScriptPart }.value.trim(),
|
||||
type = script.getParameterType().trimSpace().apply { script = nextScriptPart }.value,
|
||||
default = script.getParameterDefault().trimSpace().apply { script = nextScriptPart }.value,
|
||||
)
|
||||
}
|
||||
private fun ScriptPart.getParameterMode(): NextScript<Direction> {
|
||||
return when {
|
||||
restOfScript.startsWith("inout ", true) -> NextScript(INOUT, restOfScript.drop("inout ".length))
|
||||
restOfScript.startsWith("in ", true) -> NextScript(IN, restOfScript.drop("in ".length))
|
||||
restOfScript.startsWith("out ", true) -> NextScript(OUT, restOfScript.drop("out ".length))
|
||||
else -> NextScript(IN, restOfScript)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(ParameterNameMalformed::class)
|
||||
private fun ScriptPart.getParameterName(): NextScript<String> {
|
||||
try {
|
||||
return getNextScript { afterBeginBy(" ", "\n") }
|
||||
} catch (e: ParseException) {
|
||||
throw ParameterNameMalformed(this, e)
|
||||
}
|
||||
}
|
||||
private class ParameterNameMalformed(val script: ScriptPart, cause: Throwable):
|
||||
ParseException("Parameter name is malformed", cause)
|
||||
|
||||
@Throws(ParameterTypeMalformed::class)
|
||||
private fun ScriptPart.getParameterType(): NextScript<ParameterType> {
|
||||
val fullType = try {
|
||||
val endTextList = arrayOf(" default ", "=", ")")
|
||||
getNextScript { afterBeginBy(texts = endTextList) }
|
||||
} catch (e: ParseError) {
|
||||
throw ParameterTypeMalformed(this, e)
|
||||
}
|
||||
|
||||
var rest: ScriptPart = fullType.valueAsScriptPart()
|
||||
|
||||
val name = rest
|
||||
.getNextScript { afterBeginBy("(") }
|
||||
.apply { rest = nextScriptPart }
|
||||
val precision = rest
|
||||
.getNextInteger()
|
||||
.apply { rest = nextScriptPart }
|
||||
val scale = rest
|
||||
.getNextInteger()
|
||||
.apply { rest = nextScriptPart }
|
||||
|
||||
return NextScript(
|
||||
ParameterType(
|
||||
name = name.value.trim(),
|
||||
precision = precision.value,
|
||||
scale = scale.value
|
||||
), fullType.nextScriptPart.restOfScript
|
||||
)
|
||||
}
|
||||
|
||||
internal class ParameterTypeMalformed(val script: ScriptPart, cause: Throwable):
|
||||
ParseException("Parameter type is malformed", cause)
|
||||
|
||||
@Throws(ParameterDefaultMalformed::class)
|
||||
private fun ScriptPart.getParameterDefault(): NextScript<String?> {
|
||||
return if (this.isEmpty() || this.restOfScript == ")") {
|
||||
NextScript(null, "")
|
||||
} else {
|
||||
"""^(\s*=\s*|\s+default\s+)(.+)\s*$"""
|
||||
.toRegex(IGNORE_CASE)
|
||||
.find(restOfScript)
|
||||
.let { it ?: throw ParameterDefaultMalformed(this) }
|
||||
.let { it.groups[2]!!.value }
|
||||
.let { NextScript(it, "") }
|
||||
}
|
||||
}
|
||||
|
||||
private class ParameterDefaultMalformed(val script: ScriptPart):
|
||||
ParseException("Parameter default is malformed")
|
||||
|
||||
/**
|
||||
* TODO Finalize this
|
||||
*/
|
||||
internal fun ScriptPart.getReturns(): NextScript<Returns> {
|
||||
return NextScript(Void(), "")
|
||||
}
|
||||
|
||||
class ParseError(message: String? = null, cause: Throwable? = null):
|
||||
ParseException(message ?: "Parsing fail", cause)
|
||||
@@ -0,0 +1,164 @@
|
||||
package fr.postgresjson.definition.parse
|
||||
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.InvocationKind.EXACTLY_ONCE
|
||||
import kotlin.contracts.contract
|
||||
|
||||
@JvmInline
|
||||
internal value class ScriptPart(val restOfScript: String) {
|
||||
fun copy(block: (String) -> String): ScriptPart {
|
||||
return ScriptPart(block(restOfScript))
|
||||
}
|
||||
|
||||
fun isEmpty() = restOfScript.isEmpty()
|
||||
}
|
||||
|
||||
internal class NextScript<T>(val value: T, val restOfScript: String) {
|
||||
val nextScriptPart: ScriptPart = ScriptPart(restOfScript)
|
||||
fun isLast() = restOfScript == ""
|
||||
fun isEmptyValue() = value == "" || value == null
|
||||
}
|
||||
|
||||
internal fun ScriptPart.removeParentheses(): ScriptPart {
|
||||
return if (restOfScript.take(1) == "(" && restOfScript.takeLast(1) == ")") {
|
||||
this.copy {
|
||||
it.drop(1).dropLast(1)
|
||||
}
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get next part of script.
|
||||
* You can define a list of characters that end the part of script. Like `(` or space.
|
||||
*/
|
||||
@Throws(ParseError::class)
|
||||
internal fun ScriptPart.getNextScript(isEnd: Context.() -> Boolean = { false }): NextScript<String> {
|
||||
val status = Status()
|
||||
|
||||
fun String.unescape(): String {
|
||||
val first = take(1)
|
||||
val last = takeLast(1)
|
||||
return if (first == last && first in listOf("\"", "'")) {
|
||||
drop(1).dropLast(1).replace("$first$first", first)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
for ((index, c) in restOfScript.withIndex()) {
|
||||
val nextChar = restOfScript.getOrNull(index + 1)
|
||||
val prevChar = restOfScript.getOrNull(index - 1)
|
||||
if (c == '"' && (nextChar != '"' && prevChar != '"')) {
|
||||
status.doubleQuoted = !status.doubleQuoted
|
||||
} else if (c == '\'' && (nextChar != '\'' && prevChar != '\'')) {
|
||||
status.simpleQuoted = !status.simpleQuoted
|
||||
} else if (c == '(' && status.isNotQuoted()) {
|
||||
status.parentheses++
|
||||
} else if (c == ')' && status.isNotQuoted()) {
|
||||
status.parentheses--
|
||||
} else if (c == '[' && status.isNotQuoted()) {
|
||||
status.brackets++
|
||||
} else if (c == ']' && status.isNotQuoted()) {
|
||||
status.brackets--
|
||||
} else if (c == '{' && status.isNotQuoted()) {
|
||||
status.braces++
|
||||
} else if (c == '}' && status.isNotQuoted()) {
|
||||
status.braces--
|
||||
}
|
||||
|
||||
if (isEnd(Context(index, c, status.copy(), restOfScript))) {
|
||||
return NextScript(restOfScript.take(index + 1).unescape(), restOfScript.drop(index + 1))
|
||||
}
|
||||
}
|
||||
if (status.isNotEscaped()) {
|
||||
return NextScript(restOfScript.unescape().trim(), "").trimSpace()
|
||||
}
|
||||
throw ParseError()
|
||||
}
|
||||
|
||||
internal fun <T> NextScript<T>.trimSpace(): NextScript<T> {
|
||||
val spaces = charArrayOf(' ', '\n', '\t')
|
||||
return trim(chars = spaces)
|
||||
}
|
||||
|
||||
internal fun ScriptPart.trimSpace(): ScriptPart {
|
||||
for ((n, char) in restOfScript.withIndex()) {
|
||||
if (char !in listOf(' ', '\n', '\t')) {
|
||||
return ScriptPart(
|
||||
restOfScript.drop(n)
|
||||
)
|
||||
}
|
||||
}
|
||||
return ScriptPart(restOfScript)
|
||||
}
|
||||
|
||||
internal fun <T> NextScript<T>.trim(vararg chars: Char): NextScript<T> {
|
||||
return NextScript(value, restOfScript.apply { dropWhile { it in chars } })
|
||||
}
|
||||
|
||||
internal fun ScriptPart.trimEnd(vararg chars: Char): ScriptPart {
|
||||
return this.change { dropLastWhile { it in chars } }
|
||||
}
|
||||
|
||||
internal fun ScriptPart.split(delimiter: String): List<ScriptPart> {
|
||||
val parts: MutableList<ScriptPart> = mutableListOf()
|
||||
var rest: ScriptPart = this
|
||||
do {
|
||||
rest = rest.trimSpace()
|
||||
.getNextScript { status.isNotEscaped() && currentChar.toString() == delimiter }
|
||||
.trimSpace()
|
||||
.also { parts.add(it.valueAsScriptPart().trimSpace().trimEnd(',')) }
|
||||
.nextScriptPart
|
||||
} while (!rest.isEmpty())
|
||||
|
||||
return parts
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the value as ScriptPart
|
||||
*/
|
||||
internal fun NextScript<String>.valueAsScriptPart(): ScriptPart = ScriptPart(value)
|
||||
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
internal inline fun ScriptPart.change(block: String.() -> String): ScriptPart {
|
||||
contract {
|
||||
callsInPlace(block, EXACTLY_ONCE)
|
||||
}
|
||||
return ScriptPart(restOfScript.run(block))
|
||||
}
|
||||
|
||||
|
||||
internal fun ScriptPart.getNextInteger(): NextScript<Int?> {
|
||||
val trimmed = restOfScript.trimStart { !it.isDigit() }
|
||||
val digits = trimmed.takeWhile { it.isDigit() }
|
||||
val restOfScript = trimmed.trimStart { it.isDigit() }
|
||||
return NextScript(digits.toIntOrNull(), restOfScript).trimSpace()
|
||||
}
|
||||
|
||||
|
||||
|
||||
internal data class Status(
|
||||
var doubleQuoted: Boolean = false, // "
|
||||
var simpleQuoted: Boolean = false, // '
|
||||
var parentheses: Int = 0, // ()
|
||||
var brackets: Int = 0, // []
|
||||
var braces: Int = 0, // {}
|
||||
) {
|
||||
fun isQuoted(): Boolean = doubleQuoted || simpleQuoted
|
||||
fun isNotQuoted(): Boolean = !isQuoted()
|
||||
fun isNotEscaped(): Boolean = isNotQuoted() && parentheses == 0 && brackets == 0 && braces == 0
|
||||
}
|
||||
|
||||
internal data class Context(
|
||||
val index: Int,
|
||||
val currentChar: Char,
|
||||
val status: Status,
|
||||
val script: String,
|
||||
) {
|
||||
fun afterBeginBy(vararg texts: String): Boolean = texts.any {
|
||||
script.substring(index + 1).take(it.length) == it
|
||||
}
|
||||
|
||||
val nextChar: Char? get() = script.substring(index + 1).getOrNull(0)
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
package fr.postgresjson.functionGenerator
|
||||
|
||||
import com.github.jasync.sql.db.util.length
|
||||
import fr.postgresjson.definition.Function
|
||||
import fr.postgresjson.definition.Function.Returns
|
||||
import fr.postgresjson.definition.Returns
|
||||
import fr.postgresjson.definition.Parameter
|
||||
import fr.postgresjson.definition.Parameter.Direction.IN
|
||||
import fr.postgresjson.definition.Parameter.Direction.INOUT
|
||||
|
||||
@@ -3,6 +3,7 @@ package fr.postgresjson.migration
|
||||
import com.github.jasync.sql.db.postgresql.exceptions.GenericDatabaseException
|
||||
import fr.postgresjson.connexion.Connection
|
||||
import fr.postgresjson.connexion.execute
|
||||
import fr.postgresjson.definition.parse.parseFunction
|
||||
import fr.postgresjson.migration.Migration.Action
|
||||
import fr.postgresjson.migration.Migration.Status
|
||||
import java.util.Date
|
||||
@@ -30,8 +31,8 @@ data class Function(
|
||||
connection: Connection,
|
||||
executedAt: Date? = null
|
||||
) : this(
|
||||
DefinitionFunction(up),
|
||||
DefinitionFunction(down),
|
||||
parseFunction(up),
|
||||
parseFunction(down),
|
||||
connection,
|
||||
executedAt
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ package fr.postgresjson.migration
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference
|
||||
import fr.postgresjson.connexion.Connection
|
||||
import fr.postgresjson.definition.parse.parseFunction
|
||||
import fr.postgresjson.migration.Migration.Action
|
||||
import fr.postgresjson.migration.Migration.Status
|
||||
import fr.postgresjson.utils.LoggerDelegate
|
||||
@@ -147,7 +148,7 @@ class MigrationExecutor private constructor(
|
||||
}
|
||||
|
||||
fun addFunction(sql: String): MigrationExecutor {
|
||||
addFunction(DefinitionFunction(sql))
|
||||
addFunction(parseFunction(sql))
|
||||
return this
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package fr.postgresjson.definition
|
||||
|
||||
import fr.postgresjson.definition.parse.parseFunction
|
||||
import io.kotest.core.spec.style.FreeSpec
|
||||
import io.kotest.matchers.collections.shouldHaveSize
|
||||
import io.kotest.matchers.shouldBe
|
||||
@@ -7,7 +8,7 @@ import io.kotest.matchers.shouldBe
|
||||
class FunctionTest: FreeSpec({
|
||||
"Function name" - {
|
||||
"all in lower" {
|
||||
Function(
|
||||
parseFunction(
|
||||
// language=PostgreSQL
|
||||
"""
|
||||
create or replace function myfun() returns text language plpgsql as
|
||||
@@ -19,7 +20,7 @@ class FunctionTest: FreeSpec({
|
||||
}
|
||||
|
||||
"first letter caps" {
|
||||
Function(
|
||||
parseFunction(
|
||||
// language=PostgreSQL
|
||||
"""
|
||||
create or replace function Myfun() returns text language plpgsql as
|
||||
@@ -31,7 +32,7 @@ class FunctionTest: FreeSpec({
|
||||
}
|
||||
|
||||
"with numbers" {
|
||||
Function(
|
||||
parseFunction(
|
||||
// language=PostgreSQL
|
||||
"""
|
||||
create or replace function myfun001() returns text language plpgsql as
|
||||
@@ -43,7 +44,7 @@ class FunctionTest: FreeSpec({
|
||||
}
|
||||
|
||||
"escaped name with space" {
|
||||
Function(
|
||||
parseFunction(
|
||||
// language=PostgreSQL
|
||||
"""
|
||||
create or replace function "My fun"() returns text language plpgsql as
|
||||
@@ -55,7 +56,7 @@ class FunctionTest: FreeSpec({
|
||||
}
|
||||
|
||||
"escaped name with double quote in name" {
|
||||
Function(
|
||||
parseFunction(
|
||||
// language=PostgreSQL
|
||||
"""
|
||||
create or replace function "My""fun" () returns text language plpgsql as
|
||||
@@ -67,7 +68,7 @@ class FunctionTest: FreeSpec({
|
||||
}
|
||||
|
||||
"name with new line before and after" {
|
||||
Function(
|
||||
parseFunction(
|
||||
// language=PostgreSQL
|
||||
"""
|
||||
create or replace function
|
||||
@@ -85,7 +86,7 @@ class FunctionTest: FreeSpec({
|
||||
|
||||
"Parameters" - {
|
||||
"One parameter text" - {
|
||||
val param = Function(
|
||||
val param = parseFunction(
|
||||
// language=PostgreSQL
|
||||
"""
|
||||
create or replace function myfun(one text) returns text language plpgsql as
|
||||
@@ -107,7 +108,7 @@ class FunctionTest: FreeSpec({
|
||||
}
|
||||
|
||||
"Two parameters" - {
|
||||
val param = Function(
|
||||
val param = parseFunction(
|
||||
// language=PostgreSQL
|
||||
"""
|
||||
create or replace function myfun(one text, two int) returns text language plpgsql as
|
||||
@@ -131,7 +132,7 @@ class FunctionTest: FreeSpec({
|
||||
}
|
||||
|
||||
"parameters with `character varying(255)`" - {
|
||||
val param = Function(
|
||||
val param = parseFunction(
|
||||
// language=PostgreSQL
|
||||
"""
|
||||
create or replace function myfun(one character varying(255)) returns text language plpgsql as
|
||||
@@ -158,7 +159,7 @@ class FunctionTest: FreeSpec({
|
||||
}
|
||||
|
||||
"parameters with `numeric(16, 8)`" - {
|
||||
val param = Function(
|
||||
val param = parseFunction(
|
||||
// language=PostgreSQL
|
||||
"""
|
||||
create or replace function myfun(one numeric(16, 8)) returns text language plpgsql as
|
||||
@@ -188,7 +189,7 @@ class FunctionTest: FreeSpec({
|
||||
}
|
||||
|
||||
"parameters with default text" - {
|
||||
val param = Function(
|
||||
val param = parseFunction(
|
||||
// language=PostgreSQL
|
||||
"""
|
||||
create or replace function myfun(one text default 'example') returns text language plpgsql as
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package fr.postgresjson.functionGenerator
|
||||
|
||||
import fr.postgresjson.definition.Function
|
||||
import fr.postgresjson.definition.parse.parseFunction
|
||||
import io.kotest.core.Tag
|
||||
import io.kotest.core.spec.style.StringSpec
|
||||
import org.amshove.kluent.`should be equal to`
|
||||
@@ -34,7 +34,7 @@ class FunctionGeneratorTest : StringSpec({
|
||||
|}
|
||||
""".trimMargin()
|
||||
|
||||
generator.generate(Function(functionSql)) `should be equal to` expectedGenerated
|
||||
generator.generate(parseFunction(functionSql)) `should be equal to` expectedGenerated
|
||||
}
|
||||
|
||||
"generate function with return void" {
|
||||
@@ -60,7 +60,7 @@ class FunctionGeneratorTest : StringSpec({
|
||||
|}
|
||||
""".trimMargin()
|
||||
|
||||
generator.generate(Function(functionSql)) `should be equal to` expectedGenerated
|
||||
generator.generate(parseFunction(functionSql)) `should be equal to` expectedGenerated
|
||||
}
|
||||
|
||||
"generate function with multiple args and defaults" {
|
||||
@@ -90,6 +90,6 @@ class FunctionGeneratorTest : StringSpec({
|
||||
|}
|
||||
""".trimMargin()
|
||||
|
||||
generator.generate(Function(functionSql)) `should be equal to` expectedGenerated
|
||||
generator.generate(parseFunction(functionSql)) `should be equal to` expectedGenerated
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user