WIP: Compiled SQL function #33

Draft
flecomte wants to merge 37 commits from compiled_sql_function into master
11 changed files with 412 additions and 393 deletions
Showing only changes of commit aa1435bb4e - Show all commits

View File

@@ -1,5 +1,6 @@
package fr.postgresjson.connexion package fr.postgresjson.connexion
import fr.postgresjson.definition.parse.parseFunction
import fr.postgresjson.utils.searchSqlFiles import fr.postgresjson.utils.searchSqlFiles
import java.net.URI import java.net.URI
import fr.postgresjson.definition.Function as DefinitionFunction import fr.postgresjson.definition.Function as DefinitionFunction
@@ -48,7 +49,7 @@ class Requester(
} }
fun addFunction(sql: String) { fun addFunction(sql: String) {
DefinitionFunction(sql) parseFunction(sql)
.run { toRunnable(connection) } .run { toRunnable(connection) }
.run { functions[name] = this } .run { functions[name] = this }
} }

View File

@@ -1,345 +1,36 @@
package fr.postgresjson.definition 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 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( class Function(
override val name: String,
override val parameters: List<Parameter>,
val returns: Returns,
override val script: String, override val script: String,
override val source: Path? = null, override val source: Path? = null,
): Resource, ParametersInterface { ): 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>
// private fun <T> NextScript<T>.changeValue(block: (T) -> T): NextScript<T> {
@JvmInline // return NextScript(block(value), restOfScript)
private value class ScriptPart(val restOfScript: String) { // }
fun copy(block: (String) -> String): ScriptPart { //
return ScriptPart(block(restOfScript)) // private fun <T> NextScript<T>.changeScript(block: (String) -> String): NextScript<T> {
} // return NextScript(value, block(restOfScript))
// }
fun removeParentheses(): ScriptPart { //
return if (restOfScript.take(1) == "(" && restOfScript.takeLast(1) == ")") { // private fun <T> NextScript<T>.dropOneOf(vararg endTextList: String): NextScript<T> {
this.copy { // return changeScript { script ->
it.drop(1).dropLast(1) // endTextList
} // .filter { script.startsWith(it) }
} else { // .let { script.drop(it.size) }
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)
fun getDefinition(): String = parameters fun getDefinition(): String = parameters
.filter { it.direction == Direction.IN } .filter { it.direction == IN }
.joinToString(", ") { it.type.toString() } .joinToString(", ") { it.type.toString() }
.let { "$name ($it)" } .let { "$name ($it)" }
@@ -356,44 +47,4 @@ 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: ParameterType,
): ParameterSimpleI
}
class Any(
isSetOf: Boolean,
): Returns("any", isSetOf)
class Void: Returns("void", false)
}
} }

View File

@@ -1,5 +1,6 @@
package fr.postgresjson.definition package fr.postgresjson.definition
import fr.postgresjson.definition.parse.parseFunction
import java.io.File import java.io.File
import java.net.URL import java.net.URL
import java.nio.file.Path import java.nio.file.Path
@@ -23,7 +24,7 @@ sealed interface Resource {
Migration(resource, path) Migration(resource, path)
} catch (e: ParseException) { } catch (e: ParseException) {
try { try {
Function(resource, path) parseFunction(resource, path)
} catch (e: ParseException) { } catch (e: ParseException) {
try { try {
Query(resource, path) Query(resource, path)

View 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)
}

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -1,8 +1,7 @@
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.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

View File

@@ -3,6 +3,7 @@ package fr.postgresjson.migration
import com.github.jasync.sql.db.postgresql.exceptions.GenericDatabaseException import com.github.jasync.sql.db.postgresql.exceptions.GenericDatabaseException
import fr.postgresjson.connexion.Connection import fr.postgresjson.connexion.Connection
import fr.postgresjson.connexion.execute import fr.postgresjson.connexion.execute
import fr.postgresjson.definition.parse.parseFunction
import fr.postgresjson.migration.Migration.Action import fr.postgresjson.migration.Migration.Action
import fr.postgresjson.migration.Migration.Status import fr.postgresjson.migration.Migration.Status
import java.util.Date import java.util.Date
@@ -30,8 +31,8 @@ data class Function(
connection: Connection, connection: Connection,
executedAt: Date? = null executedAt: Date? = null
) : this( ) : this(
DefinitionFunction(up), parseFunction(up),
DefinitionFunction(down), parseFunction(down),
connection, connection,
executedAt executedAt
) )

View File

@@ -2,6 +2,7 @@ package fr.postgresjson.migration
import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.core.type.TypeReference
import fr.postgresjson.connexion.Connection import fr.postgresjson.connexion.Connection
import fr.postgresjson.definition.parse.parseFunction
import fr.postgresjson.migration.Migration.Action import fr.postgresjson.migration.Migration.Action
import fr.postgresjson.migration.Migration.Status import fr.postgresjson.migration.Migration.Status
import fr.postgresjson.utils.LoggerDelegate import fr.postgresjson.utils.LoggerDelegate
@@ -147,7 +148,7 @@ class MigrationExecutor private constructor(
} }
fun addFunction(sql: String): MigrationExecutor { fun addFunction(sql: String): MigrationExecutor {
addFunction(DefinitionFunction(sql)) addFunction(parseFunction(sql))
return this return this
} }

View File

@@ -1,5 +1,6 @@
package fr.postgresjson.definition package fr.postgresjson.definition
import fr.postgresjson.definition.parse.parseFunction
import io.kotest.core.spec.style.FreeSpec import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
@@ -7,7 +8,7 @@ import io.kotest.matchers.shouldBe
class FunctionTest: FreeSpec({ class FunctionTest: FreeSpec({
"Function name" - { "Function name" - {
"all in lower" { "all in lower" {
Function( parseFunction(
// language=PostgreSQL // language=PostgreSQL
""" """
create or replace function myfun() returns text language plpgsql as create or replace function myfun() returns text language plpgsql as
@@ -19,7 +20,7 @@ class FunctionTest: FreeSpec({
} }
"first letter caps" { "first letter caps" {
Function( parseFunction(
// language=PostgreSQL // language=PostgreSQL
""" """
create or replace function Myfun() returns text language plpgsql as create or replace function Myfun() returns text language plpgsql as
@@ -31,7 +32,7 @@ class FunctionTest: FreeSpec({
} }
"with numbers" { "with numbers" {
Function( parseFunction(
// language=PostgreSQL // language=PostgreSQL
""" """
create or replace function myfun001() returns text language plpgsql as create or replace function myfun001() returns text language plpgsql as
@@ -43,7 +44,7 @@ class FunctionTest: FreeSpec({
} }
"escaped name with space" { "escaped name with space" {
Function( parseFunction(
// language=PostgreSQL // language=PostgreSQL
""" """
create or replace function "My fun"() returns text language plpgsql as 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" { "escaped name with double quote in name" {
Function( parseFunction(
// language=PostgreSQL // language=PostgreSQL
""" """
create or replace function "My""fun" () returns text language plpgsql as 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" { "name with new line before and after" {
Function( parseFunction(
// language=PostgreSQL // language=PostgreSQL
""" """
create or replace function create or replace function
@@ -85,7 +86,7 @@ class FunctionTest: FreeSpec({
"Parameters" - { "Parameters" - {
"One parameter text" - { "One parameter text" - {
val param = Function( val param = parseFunction(
// language=PostgreSQL // language=PostgreSQL
""" """
create or replace function myfun(one text) returns text language plpgsql as create or replace function myfun(one text) returns text language plpgsql as
@@ -107,7 +108,7 @@ class FunctionTest: FreeSpec({
} }
"Two parameters" - { "Two parameters" - {
val param = Function( val param = parseFunction(
// language=PostgreSQL // language=PostgreSQL
""" """
create or replace function myfun(one text, two int) returns text language plpgsql as 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)`" - { "parameters with `character varying(255)`" - {
val param = Function( val param = parseFunction(
// language=PostgreSQL // language=PostgreSQL
""" """
create or replace function myfun(one character varying(255)) returns text language plpgsql as 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)`" - { "parameters with `numeric(16, 8)`" - {
val param = Function( val param = parseFunction(
// language=PostgreSQL // language=PostgreSQL
""" """
create or replace function myfun(one numeric(16, 8)) returns text language plpgsql as 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" - { "parameters with default text" - {
val param = Function( val param = parseFunction(
// language=PostgreSQL // language=PostgreSQL
""" """
create or replace function myfun(one text default 'example') returns text language plpgsql as create or replace function myfun(one text default 'example') returns text language plpgsql as

View File

@@ -1,6 +1,6 @@
package fr.postgresjson.functionGenerator package fr.postgresjson.functionGenerator
import fr.postgresjson.definition.Function import fr.postgresjson.definition.parse.parseFunction
import io.kotest.core.Tag import io.kotest.core.Tag
import io.kotest.core.spec.style.StringSpec import io.kotest.core.spec.style.StringSpec
import org.amshove.kluent.`should be equal to` import org.amshove.kluent.`should be equal to`
@@ -34,7 +34,7 @@ class FunctionGeneratorTest : StringSpec({
|} |}
""".trimMargin() """.trimMargin()
generator.generate(Function(functionSql)) `should be equal to` expectedGenerated generator.generate(parseFunction(functionSql)) `should be equal to` expectedGenerated
} }
"generate function with return void" { "generate function with return void" {
@@ -60,7 +60,7 @@ class FunctionGeneratorTest : StringSpec({
|} |}
""".trimMargin() """.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" { "generate function with multiple args and defaults" {
@@ -90,6 +90,6 @@ class FunctionGeneratorTest : StringSpec({
|} |}
""".trimMargin() """.trimMargin()
generator.generate(Function(functionSql)) `should be equal to` expectedGenerated generator.generate(parseFunction(functionSql)) `should be equal to` expectedGenerated
} }
}) })