WIP: Compiled SQL function #33
@@ -1,67 +1,360 @@
|
|||||||
package fr.postgresjson.definition
|
package fr.postgresjson.definition
|
||||||
|
|
||||||
|
import com.github.jasync.sql.db.util.length
|
||||||
|
import fr.postgresjson.definition.Parameter.Direction
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
|
import kotlin.contracts.ExperimentalContracts
|
||||||
|
import kotlin.contracts.InvocationKind.EXACTLY_ONCE
|
||||||
|
import kotlin.contracts.contract
|
||||||
|
|
||||||
class Function(
|
class Function(
|
||||||
override val script: String,
|
override val script: String,
|
||||||
override val source: Path? = null
|
override val source: Path? = null,
|
||||||
) : Resource, ParametersInterface {
|
): Resource, ParametersInterface {
|
||||||
val returns: String
|
/**
|
||||||
|
* 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 = Returns.Void()
|
||||||
override val name: String
|
override val name: String
|
||||||
override val parameters: List<Parameter>
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the value as ScriptPart
|
||||||
|
*/
|
||||||
|
private fun NextScript<String>.valueAsScriptPart(): ScriptPart = ScriptPart(value)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val functionRegex =
|
ScriptPart(script)
|
||||||
"""create (or replace )?(procedure|function) *(?<fname>[^(\s]+)\s*\(\s*(?<params>\s*([^()]+(\([^)]+\))*)*)\s*\)(RETURNS *(?<return>[^ \n]+))?"""
|
.getFunctionOrProcedure().trimSpace().nextScriptPart
|
||||||
.toRegex(setOf(RegexOption.IGNORE_CASE, RegexOption.MULTILINE))
|
.getFunctionName().apply { name = value }.nextScriptPart
|
||||||
|
.getArguments().apply { parameters = value }.nextScriptPart
|
||||||
|
// .getReturns().hook { returns = value }
|
||||||
|
|
||||||
val paramsRegex =
|
}
|
||||||
"""\s*(?<param>((?<direction>IN|OUT|INOUT|VARIADIC)?\s+)?("?(?<pname>[^\s,")]+)"?\s+)?(?<type>((?!default)[a-z0-9]+\s?)+(\((?<precision>[0-9]+)(, (?<scale>[0-9]+))?\))?)(\s+(default\s|=)\s*(?<default>('[^']+?'|[0-9]+|true|false))(?<defaultType>\s*::\s*[a-z0-9]+(\([0-9]+(\s?,\s?[0-9]+\s?)?\))?)?)?)\s*(,|${'$'})"""
|
|
||||||
.toRegex(setOf(RegexOption.IGNORE_CASE, RegexOption.MULTILINE))
|
|
||||||
|
|
||||||
val queryMatch = functionRegex.find(script)
|
private fun ScriptPart.getFunctionOrProcedure(): NextScript<String> {
|
||||||
if (queryMatch !== null) {
|
val result = """create\s+(?:or\s+replace\s+)?(procedure|function)\s+"""
|
||||||
val functionName = queryMatch.groups["fname"]?.value?.trim() ?: error("Function name not found")
|
.toRegex()
|
||||||
val functionParameters = queryMatch.groups["params"]?.value?.trim()
|
.find(restOfScript)
|
||||||
this.returns = queryMatch.groups["return"]?.value?.trim() ?: ""
|
?: throw FunctionNotFound()
|
||||||
|
|
||||||
/* Create parameters definition */
|
val rest = result.range.last
|
||||||
val parameters = if (functionParameters !== null) {
|
.let { cursor -> restOfScript.drop(cursor + 1) }
|
||||||
paramsRegex
|
|
||||||
.findAll(functionParameters)
|
return NextScript(
|
||||||
.mapIndexed { index, paramsMatch ->
|
result.groups[1]!!.value,
|
||||||
Parameter(
|
rest
|
||||||
paramsMatch.groups["pname"]?.value?.trim() ?: """arg${index + 1}""",
|
|
||||||
paramsMatch.groups["type"]?.value?.trim() ?: throw ArgumentNotFound(),
|
|
||||||
paramsMatch.groups["direction"]?.value?.trim(),
|
|
||||||
paramsMatch.groups["default"]?.value?.trim(),
|
|
||||||
paramsMatch.groups["precision"]?.value?.trim()?.toInt(),
|
|
||||||
paramsMatch.groups["scale"]?.value?.trim()?.toInt()
|
|
||||||
)
|
)
|
||||||
}.toList()
|
|
||||||
} else {
|
|
||||||
listOf()
|
|
||||||
}
|
}
|
||||||
this.name = functionName
|
|
||||||
this.parameters = parameters
|
private fun ScriptPart.getFunctionName(): NextScript<String> {
|
||||||
} else {
|
try {
|
||||||
throw FunctionNotFound()
|
return getNextScript { status.isNotEscaped() && listOf("(", " ", "\n").any { afterBeginBy(it) } }
|
||||||
|
} catch (e: NameMalformed) {
|
||||||
|
throw FunctionNameMalformed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FunctionNotFound(cause: Throwable? = null) : Resource.ParseException("Function not found in script", cause)
|
@OptIn(ExperimentalContracts::class)
|
||||||
class ArgumentNotFound(cause: Throwable? = null) : Resource.ParseException("Argument not found in script", cause)
|
private inline fun ScriptPart.change(block: String.() -> String): ScriptPart {
|
||||||
|
contract {
|
||||||
|
callsInPlace(block, EXACTLY_ONCE)
|
||||||
|
}
|
||||||
|
return ScriptPart(restOfScript.run(block))
|
||||||
|
}
|
||||||
|
|
||||||
fun getDefinition(): String {
|
/**
|
||||||
return parameters
|
* Get a name.
|
||||||
.filter { it.direction == Parameter.Direction.IN }
|
* You can define a list of characters that end the name. Like `(` or space.
|
||||||
.joinToString(", ") { it.type }
|
*/
|
||||||
|
private fun ScriptPart.getAbstractName(endString: String, includeEnd: Boolean = false): NextScript<String> =
|
||||||
|
getAbstractName(listOf(endString), includeEnd)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a name.
|
||||||
|
* You can define a list of characters that end the name. Like `(` or space.
|
||||||
|
*/
|
||||||
|
@Deprecated("replace by getNextScript", ReplaceWith("getNextScript"))
|
||||||
|
private fun ScriptPart.getAbstractName(endStrings: List<String>, includeEnd: Boolean = false): NextScript<String> {
|
||||||
|
var nameIsEscaped = false
|
||||||
|
for ((i, c) in restOfScript.withIndex()) {
|
||||||
|
val isEndOfString = endStrings.filter { restOfScript.substring(i).take(it.length) == it }.length > 0
|
||||||
|
|
||||||
|
if (c == '"' && i == 0) {
|
||||||
|
nameIsEscaped = true
|
||||||
|
} else if (c == '"' && i > 0 && (restOfScript[i + 1] == '"' || restOfScript[i - 1] == '"')) {
|
||||||
|
continue
|
||||||
|
} else if (c == '"' && i > 0 && !nameIsEscaped) {
|
||||||
|
throw NameMalformed()
|
||||||
|
} else if ((c == '"' && i > 0 && nameIsEscaped) || (!nameIsEscaped && isEndOfString)) {
|
||||||
|
val dropCount = i + if (includeEnd) 1 else 0
|
||||||
|
return NextScript(restOfScript.take(i).trim('"').replace("\"\"", "\""), restOfScript.drop(dropCount))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw NameMalformed()
|
||||||
|
}
|
||||||
|
|
||||||
|
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): 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.getArguments(): NextScript<List<Parameter>> {
|
||||||
|
val allArgumentsScript = this.getNextScript {
|
||||||
|
currentChar == ')' && status.isNotEscaped()
|
||||||
|
}
|
||||||
|
val arguments: List<Parameter> = allArgumentsScript
|
||||||
|
.valueAsScriptPart()
|
||||||
|
.removeParentheses()
|
||||||
|
.split(",")
|
||||||
|
.map { it.toArgument() }
|
||||||
|
|
||||||
|
return NextScript(arguments.toList(), allArgumentsScript.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 ScriptPart.toArgument(): Parameter {
|
||||||
|
var script: ScriptPart = this.trimSpace()
|
||||||
|
return Parameter(
|
||||||
|
direction = script.getArgMode().apply { script = nextScriptPart }.value,
|
||||||
|
name = script.getArgName().trimSpace().apply { script = nextScriptPart }.value.trim(),
|
||||||
|
type = script.getArgType().trimSpace().apply { script = nextScriptPart }.value,
|
||||||
|
default = script.getArgDefault().trimSpace().apply { script = nextScriptPart }.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ScriptPart.getArgMode(): 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.getArgName(): NextScript<String> {
|
||||||
|
try {
|
||||||
|
return getNextScript {
|
||||||
|
listOf(" ", "\n")
|
||||||
|
.any { afterBeginBy(it) }
|
||||||
|
}
|
||||||
|
} catch (e: NameMalformed) {
|
||||||
|
throw ArgNameMalformed(null, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ScriptPart.getArgType(): NextScript<ArgumentType> {
|
||||||
|
val fullType = try {
|
||||||
|
getNextScript {
|
||||||
|
listOf(" default ", "=", ")")
|
||||||
|
.any { afterBeginBy(it) }
|
||||||
|
}
|
||||||
|
} catch (e: ParseError) {
|
||||||
|
throw ArgTypeMalformed(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(
|
||||||
|
ArgumentType(
|
||||||
|
name = name.value.trim(),
|
||||||
|
precision = precision.value,
|
||||||
|
scale = scale.value
|
||||||
|
), fullType.nextScriptPart.restOfScript
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO implement this method
|
||||||
|
*/
|
||||||
|
private fun ScriptPart.getArgDefault(): NextScript<String?> {
|
||||||
|
return NextScript("plop", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 ArgumentNotFound(cause: Throwable? = null): Resource.ParseException("Argument not found in script", cause)
|
||||||
|
class FunctionNameMalformed(message: String? = null, cause: Throwable? = null):
|
||||||
|
Resource.ParseException(message ?: "Function name is malformed", cause)
|
||||||
|
class ArgNameMalformed(message: String? = null, cause: Throwable? = null):
|
||||||
|
Resource.ParseException(message ?: "Arg name is malformed", cause)
|
||||||
|
class ArgTypeMalformed(message: String? = null, cause: Throwable? = null):
|
||||||
|
Resource.ParseException(message ?: "Arg type 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)
|
||||||
|
|
||||||
|
fun getDefinition(): String = parameters
|
||||||
|
.filter { it.direction == Direction.IN }
|
||||||
|
.joinToString(", ") { it.type.toString() }
|
||||||
.let { "$name ($it)" }
|
.let { "$name ($it)" }
|
||||||
}
|
|
||||||
|
|
||||||
fun getParametersIndexedByName(): Map<String, Parameter> {
|
fun getParametersIndexedByName(): Map<String, Parameter> = parameters
|
||||||
return parameters.associateBy { it.name }
|
.withIndex()
|
||||||
}
|
.associate { (key, param) -> Pair(param.name ?: "${key + 1}", param) }
|
||||||
|
|
||||||
|
operator fun get(name: String): Parameter? = parameters.firstOrNull { it.name == name }
|
||||||
|
|
||||||
infix fun `has same definition`(other: Function): Boolean {
|
infix fun `has same definition`(other: Function): Boolean {
|
||||||
return other.getDefinition() == this.getDefinition()
|
return other.getDefinition() == this.getDefinition()
|
||||||
@@ -70,4 +363,44 @@ class Function(
|
|||||||
infix fun `is different from`(other: Function): Boolean {
|
infix fun `is different from`(other: Function): Boolean {
|
||||||
return other.script != this.script
|
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: ArgumentType,
|
||||||
|
): ParameterSimpleI
|
||||||
|
}
|
||||||
|
|
||||||
|
class Any(
|
||||||
|
isSetOf: Boolean,
|
||||||
|
): Returns("any", isSetOf)
|
||||||
|
|
||||||
|
class Void: Returns("void", false)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,31 +2,39 @@ package fr.postgresjson.definition
|
|||||||
|
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
interface ParameterI {
|
class ArgumentType(
|
||||||
val name: String
|
val name: String,
|
||||||
val type: String
|
val precision: Int? = null,
|
||||||
val direction: Parameter.Direction
|
val scale: Int? = null,
|
||||||
val default: String
|
val isArray: Boolean = false,
|
||||||
|
) {
|
||||||
|
override fun toString(): String {
|
||||||
|
return if (precision == null && scale == null) {
|
||||||
|
name
|
||||||
|
} else if (scale == null) {
|
||||||
|
"""$name($precision)"""
|
||||||
|
} else {
|
||||||
|
"""$name($precision, $scale)"""
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Parameter(val name: String, val type: String, direction: Direction? = Direction.IN, val default: String? = null, val precision: Int? = null, val scale: Int? = null) {
|
interface ParameterSimpleI {
|
||||||
val direction: Direction
|
val name: String?
|
||||||
|
val type: ArgumentType
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
class Parameter(
|
||||||
if (direction === null) {
|
override val name: String?,
|
||||||
this.direction = Direction.IN
|
override val type: ArgumentType,
|
||||||
} else {
|
val direction: Direction = Direction.IN,
|
||||||
this.direction = direction
|
val default: String? = null,
|
||||||
}
|
): ParameterSimpleI {
|
||||||
}
|
constructor(name: String?, type: ArgumentType, direction: String = "IN", default: String? = null): this(
|
||||||
|
|
||||||
constructor(name: String, type: String, direction: String? = "IN", default: String? = null, precision: Int? = null, scale: Int? = null) : this(
|
|
||||||
name = name,
|
name = name,
|
||||||
type = type,
|
type = type,
|
||||||
direction = direction?.let { Direction.valueOf(direction.uppercase(Locale.getDefault())) },
|
direction = direction.let { Direction.valueOf(direction.uppercase(Locale.getDefault())) },
|
||||||
default = default,
|
default = default
|
||||||
precision = precision,
|
|
||||||
scale = scale
|
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class Direction { IN, OUT, INOUT }
|
enum class Direction { IN, OUT, INOUT }
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package fr.postgresjson.functionGenerator
|
package fr.postgresjson.functionGenerator
|
||||||
|
|
||||||
|
import com.github.jasync.sql.db.util.length
|
||||||
import fr.postgresjson.definition.Function
|
import fr.postgresjson.definition.Function
|
||||||
|
import fr.postgresjson.definition.Function.Returns
|
||||||
import fr.postgresjson.definition.Parameter
|
import fr.postgresjson.definition.Parameter
|
||||||
import fr.postgresjson.definition.Parameter.Direction.IN
|
import fr.postgresjson.definition.Parameter.Direction.IN
|
||||||
import fr.postgresjson.definition.Parameter.Direction.INOUT
|
import fr.postgresjson.definition.Parameter.Direction.INOUT
|
||||||
@@ -19,15 +21,16 @@ class FunctionGenerator(private val functionsDirectories: List<URI>) {
|
|||||||
|
|
||||||
private fun List<Parameter>.toKotlinArgs(): String {
|
private fun List<Parameter>.toKotlinArgs(): String {
|
||||||
return filter { it.direction == IN || it.direction == INOUT }
|
return filter { it.direction == IN || it.direction == INOUT }
|
||||||
.joinToString(", ") {
|
.mapIndexed { index, parameter -> index to parameter }
|
||||||
val base = """${it.kotlinName}: ${it.kotlinType}"""
|
.joinToString(", ") { (idx, param) ->
|
||||||
val default = if (it.default == null) {
|
val base = """${param.kotlinName ?: "arg$idx"}: ${param.kotlinType}"""
|
||||||
|
val default = if (param.default == null) {
|
||||||
""
|
""
|
||||||
} else {
|
} else {
|
||||||
when (it.kotlinType) {
|
when (param.kotlinType) {
|
||||||
"String" -> """ = "${it.default.trim('\'')}""""
|
"String" -> """ = "${param.default.trim('\'')}""""
|
||||||
"Int" -> """ = ${it.default}"""
|
"Int" -> """ = ${param.default}"""
|
||||||
"Boolean" -> """ = ${it.default.lowercase()}"""
|
"Boolean" -> """ = ${param.default.lowercase()}"""
|
||||||
else -> ""
|
else -> ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,7 +46,7 @@ class FunctionGenerator(private val functionsDirectories: List<URI>) {
|
|||||||
|
|
||||||
private val Parameter.kotlinType: String
|
private val Parameter.kotlinType: String
|
||||||
get() {
|
get() {
|
||||||
return when (type.lowercase()) {
|
return when (type.name.lowercase()) {
|
||||||
"text" -> "String"
|
"text" -> "String"
|
||||||
"varchar" -> "String"
|
"varchar" -> "String"
|
||||||
"character varying" -> "String"
|
"character varying" -> "String"
|
||||||
@@ -68,9 +71,9 @@ class FunctionGenerator(private val functionsDirectories: List<URI>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val Parameter.kotlinName: String
|
private val Parameter.kotlinName: String?
|
||||||
get() {
|
get() {
|
||||||
return name.toCamelCase().trimStart('_')
|
return name?.toCamelCase()?.trimStart('_')
|
||||||
}
|
}
|
||||||
|
|
||||||
private val Function.kotlinName: String
|
private val Function.kotlinName: String
|
||||||
@@ -107,7 +110,7 @@ class FunctionGenerator(private val functionsDirectories: List<URI>) {
|
|||||||
val args = parameters.toKotlinArgs()
|
val args = parameters.toKotlinArgs()
|
||||||
|
|
||||||
val hasInputArgs: Boolean = parameters.filter { it.direction != OUT }.any { it.kotlinType == "S" }
|
val hasInputArgs: Boolean = parameters.filter { it.direction != OUT }.any { it.kotlinType == "S" }
|
||||||
val hasReturn: Boolean = parameters.any { it.direction != IN } || (returns != "" && returns != "void")
|
val hasReturn: Boolean = parameters.any { it.direction != IN } || (returns !is Returns.Void)
|
||||||
|
|
||||||
val generics = mutableListOf<String>()
|
val generics = mutableListOf<String>()
|
||||||
if (hasReturn) generics.add("reified E: Any")
|
if (hasReturn) generics.add("reified E: Any")
|
||||||
|
|||||||
295
src/test/kotlin/fr/postgresjson/definition/FunctionTest.kt
Normal file
295
src/test/kotlin/fr/postgresjson/definition/FunctionTest.kt
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
package fr.postgresjson.definition
|
||||||
|
|
||||||
|
import io.kotest.core.spec.style.FreeSpec
|
||||||
|
import io.kotest.matchers.collections.shouldHaveSize
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
|
||||||
|
class FunctionTest: FreeSpec({
|
||||||
|
"Function name" - {
|
||||||
|
"all in lower" {
|
||||||
|
Function(
|
||||||
|
// language=PostgreSQL
|
||||||
|
"""
|
||||||
|
create or replace function myfun() returns text language plpgsql as
|
||||||
|
$$ begin; end$$;
|
||||||
|
""".trimIndent()
|
||||||
|
).apply {
|
||||||
|
name shouldBe "myfun"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"first letter caps" {
|
||||||
|
Function(
|
||||||
|
// language=PostgreSQL
|
||||||
|
"""
|
||||||
|
create or replace function Myfun() returns text language plpgsql as
|
||||||
|
$$ begin; end$$;
|
||||||
|
""".trimIndent()
|
||||||
|
).apply {
|
||||||
|
name shouldBe "Myfun"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"with numbers" {
|
||||||
|
Function(
|
||||||
|
// language=PostgreSQL
|
||||||
|
"""
|
||||||
|
create or replace function myfun001() returns text language plpgsql as
|
||||||
|
$$ begin; end$$;
|
||||||
|
""".trimIndent()
|
||||||
|
).apply {
|
||||||
|
name shouldBe "myfun001"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"escaped name with space" {
|
||||||
|
Function(
|
||||||
|
// language=PostgreSQL
|
||||||
|
"""
|
||||||
|
create or replace function "My fun"() returns text language plpgsql as
|
||||||
|
$$ begin; end$$;
|
||||||
|
""".trimIndent()
|
||||||
|
).apply {
|
||||||
|
name shouldBe "My fun"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"escaped name with double quote in name" {
|
||||||
|
Function(
|
||||||
|
// language=PostgreSQL
|
||||||
|
"""
|
||||||
|
create or replace function "My""fun" () returns text language plpgsql as
|
||||||
|
$$ begin; end$$;
|
||||||
|
""".trimIndent()
|
||||||
|
).apply {
|
||||||
|
name shouldBe "My\"fun"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"name with new line before and after" {
|
||||||
|
Function(
|
||||||
|
// language=PostgreSQL
|
||||||
|
"""
|
||||||
|
create or replace function
|
||||||
|
myfun
|
||||||
|
()
|
||||||
|
returns text language plpgsql as
|
||||||
|
$$ begin; end$$;
|
||||||
|
""".trimIndent()
|
||||||
|
).apply {
|
||||||
|
name shouldBe "myfun"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
"Parameters" - {
|
||||||
|
"One parameter text" - {
|
||||||
|
val param = Function(
|
||||||
|
// language=PostgreSQL
|
||||||
|
"""
|
||||||
|
create or replace function myfun(one text) returns text language plpgsql as
|
||||||
|
$$ begin end;$$;
|
||||||
|
""".trimIndent()
|
||||||
|
).parameters
|
||||||
|
|
||||||
|
"should have one parameter" {
|
||||||
|
param shouldHaveSize 1
|
||||||
|
}
|
||||||
|
|
||||||
|
"should have first parameter name" {
|
||||||
|
param[0].name shouldBe "one"
|
||||||
|
}
|
||||||
|
|
||||||
|
"should have first parameter type name" {
|
||||||
|
param[0].type.name shouldBe "text"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"Two parameters" - {
|
||||||
|
val param = Function(
|
||||||
|
// language=PostgreSQL
|
||||||
|
"""
|
||||||
|
create or replace function myfun(one text, two int) returns text language plpgsql as
|
||||||
|
$$ begin end;$$;
|
||||||
|
""".trimIndent()
|
||||||
|
).parameters
|
||||||
|
|
||||||
|
"should have 2 parameters" {
|
||||||
|
param shouldHaveSize 2
|
||||||
|
}
|
||||||
|
|
||||||
|
"should have names" {
|
||||||
|
param[0].name shouldBe "one"
|
||||||
|
param[1].name shouldBe "two"
|
||||||
|
}
|
||||||
|
|
||||||
|
"should have first parameter type name" {
|
||||||
|
param[0].type.name shouldBe "text"
|
||||||
|
param[1].type.name shouldBe "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// "function returns" - {
|
||||||
|
// "should return the type text if function return text" {
|
||||||
|
// Function(
|
||||||
|
// // language=PostgreSQL
|
||||||
|
// """
|
||||||
|
// create or replace function test001() returns text language plpgsql as
|
||||||
|
// $$ begin; end$$;
|
||||||
|
// """.trimIndent()
|
||||||
|
// ).returns shouldBe "text"
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// "return null if function return void" {
|
||||||
|
// Function(
|
||||||
|
// // language=PostgreSQL
|
||||||
|
// """
|
||||||
|
// create or replace function test001() returns void language plpgsql as
|
||||||
|
// $$ begin; end$$;
|
||||||
|
// """.trimIndent()
|
||||||
|
// ).returns shouldBe null
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// "Parameters" - {
|
||||||
|
// "One parameter text" - {
|
||||||
|
// val param = Function(
|
||||||
|
// // language=PostgreSQL
|
||||||
|
// """
|
||||||
|
// create or replace function myfun(
|
||||||
|
// one text
|
||||||
|
// ) returns text language plpgsql as
|
||||||
|
// $$ begin end;$$;
|
||||||
|
// """.trimIndent()
|
||||||
|
// ).parameters
|
||||||
|
//
|
||||||
|
// "Function must have one parameter" {
|
||||||
|
// param shouldHaveSize 1
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// "The parameter must be in lower case" {
|
||||||
|
// param.getOrNull(0)?.name shouldBe "one"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// "parameters" - {
|
||||||
|
// val param = Function(
|
||||||
|
// // language=PostgreSQL
|
||||||
|
// """
|
||||||
|
// create or replace function myfun(
|
||||||
|
// one text,
|
||||||
|
// "Two" INTEGER default 5,
|
||||||
|
// "Three ""and"" half" character varying = 'Yes',
|
||||||
|
// Three_and_more character varying(255) default 'Hello',
|
||||||
|
// dot point default '(1, 2)'::point,
|
||||||
|
// num NUMERIC(10, 3) default 123.654,
|
||||||
|
// arr01 text[] default '{hello, world, "and others", and\ more, "with \", ], [ , ) and as $$ in text"}'::text[],
|
||||||
|
// arr02 "point"[] default array['(1, 2)'::point, '(7, 12)'::point]::point[],
|
||||||
|
// arr03 text[] default array[
|
||||||
|
// 'text01',
|
||||||
|
// 'text02"([,#-',
|
||||||
|
// null
|
||||||
|
// ],
|
||||||
|
// last "text" default 'Hi'
|
||||||
|
// ) returns text language plpgsql as
|
||||||
|
// $$ begin end;$$;
|
||||||
|
// """.trimIndent()
|
||||||
|
// ).parameters
|
||||||
|
//
|
||||||
|
// "count must be correct" {
|
||||||
|
// param shouldHaveSize 10
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// "name" - {
|
||||||
|
// "in lower case" {
|
||||||
|
// param.getOrNull(0)?.name shouldBe "one"
|
||||||
|
// }
|
||||||
|
// "in camel case with double quote" {
|
||||||
|
// param.getOrNull(1)?.name shouldBe "Two"
|
||||||
|
// }
|
||||||
|
// "with spaces and double quote" {
|
||||||
|
// param.getOrNull(2)?.name shouldBe "Three \"and\" half"
|
||||||
|
// }
|
||||||
|
// "in snake_case" {
|
||||||
|
// param.getOrNull(3)?.name shouldBe "three_and_more"
|
||||||
|
// }
|
||||||
|
// "with numbers" {
|
||||||
|
// param.getOrNull(5)?.name shouldBe "arr01"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// "type" - {
|
||||||
|
// "text in lower case" {
|
||||||
|
// param.getOrNull(0)?.type shouldBe "text"
|
||||||
|
// }
|
||||||
|
// "integer in UPPER case" {
|
||||||
|
// param.getOrNull(1)?.type shouldBe "integer"
|
||||||
|
// }
|
||||||
|
// "character varying in two word" {
|
||||||
|
// param.getOrNull(2)?.type shouldBe "character varying"
|
||||||
|
// }
|
||||||
|
// "character varying with max size" - {
|
||||||
|
// "dont return the scale type in name" {
|
||||||
|
// param.getOrNull(3)?.type shouldBe "character varying"
|
||||||
|
// }
|
||||||
|
// "return the correct size" {
|
||||||
|
// param.getOrNull(3)?.type?.precision shouldBe 255
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// "numeric with precision and scale" - {
|
||||||
|
// "dont return the precision and scale type in name" {
|
||||||
|
// param.getOrNull(5)?.type shouldBe "numeric"
|
||||||
|
// }
|
||||||
|
// "return the correct precision" {
|
||||||
|
// param.getOrNull(5)?.type?.precision shouldBe 10
|
||||||
|
// }
|
||||||
|
// "return the correct scale" {
|
||||||
|
// param.getOrNull(5)?.type?.scale shouldBe 3
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// "array of text" {
|
||||||
|
// param.getOrNull(7)?.type shouldBe "text[]"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// "default" - {
|
||||||
|
// "with array of composite type Point" - {
|
||||||
|
// """must return "arr02" at name""" {
|
||||||
|
// param.getOrNull(7)?.name shouldBe "arr02"
|
||||||
|
// }
|
||||||
|
// """must return "point[]" at type""" {
|
||||||
|
// param.getOrNull(7)?.type shouldBe "point[]"
|
||||||
|
// }
|
||||||
|
// """must return "array[(1, 2)::point, (7, 12)::point]::point[]" at default""" {
|
||||||
|
// param.getOrNull(7)?.default shouldBe "array[(1, 2)::point, (7, 12)::point]::point[]"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// "getDefinition" - {
|
||||||
|
// TODO("must be implement")
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// "getParametersIndexedByName" - {
|
||||||
|
// TODO("must be implement")
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// "has same definition" - {
|
||||||
|
// TODO("must be implement")
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// "is different from" - {
|
||||||
|
// TODO("must be implement")
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// "script" - {
|
||||||
|
// TODO("must be implement")
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// "source" - {
|
||||||
|
// TODO("must be implement")
|
||||||
|
// }
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user