From 75066d977ceff45e43a6cc16902f98beb9eb71c6 Mon Sep 17 00:00:00 2001 From: Fabrice Lecomte Date: Wed, 31 May 2023 01:38:41 +0200 Subject: [PATCH] WIP: Function parsing --- .../fr/postgresjson/definition/Function.kt | 423 ++++++++++++++++-- .../fr/postgresjson/definition/Parameter.kt | 46 +- .../functionGenerator/FunctionGenerator.kt | 25 +- .../postgresjson/definition/FunctionTest.kt | 295 ++++++++++++ 4 files changed, 714 insertions(+), 75 deletions(-) create mode 100644 src/test/kotlin/fr/postgresjson/definition/FunctionTest.kt diff --git a/src/main/kotlin/fr/postgresjson/definition/Function.kt b/src/main/kotlin/fr/postgresjson/definition/Function.kt index 4b1cdd1..eccd959 100644 --- a/src/main/kotlin/fr/postgresjson/definition/Function.kt +++ b/src/main/kotlin/fr/postgresjson/definition/Function.kt @@ -1,68 +1,361 @@ package fr.postgresjson.definition +import com.github.jasync.sql.db.util.length +import fr.postgresjson.definition.Parameter.Direction import java.nio.file.Path +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind.EXACTLY_ONCE +import kotlin.contracts.contract class Function( override val script: String, - override val source: Path? = null -) : Resource, ParametersInterface { - val returns: String + override val source: Path? = null, +): Resource, ParametersInterface { + /** + * TEXT, INT, TEXT[], CUSTOM_TYPE => String, Int, List, Any; + * TABLE(id INT, name TEXT) => Object { id: Int; name: String }; + * SETOF TEXT => List; + * MY_TABLE.id%TYPE => Any; + * VOID => null; + */ + val returns: Returns = Returns.Void() override val name: String override val parameters: List - init { - val functionRegex = - """create (or replace )?(procedure|function) *(?[^(\s]+)\s*\(\s*(?\s*([^()]+(\([^)]+\))*)*)\s*\)(RETURNS *(?[^ \n]+))?""" - .toRegex(setOf(RegexOption.IGNORE_CASE, RegexOption.MULTILINE)) - val paramsRegex = - """\s*(?((?IN|OUT|INOUT|VARIADIC)?\s+)?("?(?[^\s,")]+)"?\s+)?(?((?!default)[a-z0-9]+\s?)+(\((?[0-9]+)(, (?[0-9]+))?\))?)(\s+(default\s|=)\s*(?('[^']+?'|[0-9]+|true|false))(?\s*::\s*[a-z0-9]+(\([0-9]+(\s?,\s?[0-9]+\s?)?\))?)?)?)\s*(,|${'$'})""" - .toRegex(setOf(RegexOption.IGNORE_CASE, RegexOption.MULTILINE)) + @JvmInline + private value class ScriptPart(val restOfScript: String) { + fun copy(block: (String) -> String): ScriptPart { + return ScriptPart(block(restOfScript)) + } - val queryMatch = functionRegex.find(script) - if (queryMatch !== null) { - val functionName = queryMatch.groups["fname"]?.value?.trim() ?: error("Function name not found") - val functionParameters = queryMatch.groups["params"]?.value?.trim() - this.returns = queryMatch.groups["return"]?.value?.trim() ?: "" - - /* Create parameters definition */ - val parameters = if (functionParameters !== null) { - paramsRegex - .findAll(functionParameters) - .mapIndexed { index, paramsMatch -> - Parameter( - 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() + fun removeParentheses(): ScriptPart { + return if (restOfScript.take(1) == "(" && restOfScript.takeLast(1) == ")") { + this.copy { + it.drop(1).dropLast(1) + } } else { - listOf() + this } - this.name = functionName - this.parameters = parameters - } else { - throw FunctionNotFound() + } + + fun isEmpty() = restOfScript.isEmpty() + } + + private fun emptyScriptPart(): ScriptPart = ScriptPart("") + + private class NextScript(val value: T, val restOfScript: String) { + val nextScriptPart: ScriptPart = ScriptPart(restOfScript) + } + + /** + * Return the value as ScriptPart + */ + private fun NextScript.valueAsScriptPart(): ScriptPart = ScriptPart(value) + + init { + ScriptPart(script) + .getFunctionOrProcedure().trimSpace().nextScriptPart + .getFunctionName().apply { name = value }.nextScriptPart + .getArguments().apply { parameters = value }.nextScriptPart +// .getReturns().hook { returns = value } + + } + + private fun ScriptPart.getFunctionOrProcedure(): NextScript { + 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 { + try { + 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) - class ArgumentNotFound(cause: Throwable? = null) : Resource.ParseException("Argument not found in script", cause) - - fun getDefinition(): String { - return parameters - .filter { it.direction == Parameter.Direction.IN } - .joinToString(", ") { it.type } - .let { "$name ($it)" } + @OptIn(ExperimentalContracts::class) + private inline fun ScriptPart.change(block: String.() -> String): ScriptPart { + contract { + callsInPlace(block, EXACTLY_ONCE) + } + return ScriptPart(restOfScript.run(block)) } - fun getParametersIndexedByName(): Map { - return parameters.associateBy { it.name } + /** + * Get a name. + * You can define a list of characters that end the name. Like `(` or space. + */ + private fun ScriptPart.getAbstractName(endString: String, includeEnd: Boolean = false): NextScript = + 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, includeEnd: Boolean = false): NextScript { + 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 { + 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 { + val parts: MutableList = 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 { + 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> { + val allArgumentsScript = this.getNextScript { + currentChar == ')' && status.isNotEscaped() + } + val arguments: List = 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 NextScript.trimSpace(): NextScript { + 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 NextScript.trim(vararg chars: Char): NextScript { + 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 { + 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 { + try { + return getNextScript { + listOf(" ", "\n") + .any { afterBeginBy(it) } + } + } catch (e: NameMalformed) { + throw ArgNameMalformed(null, e) + } + } + + private fun ScriptPart.getArgType(): NextScript { + 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 { + return NextScript("plop", "") + } + + /** + * TODO Finalize this + */ + private fun ScriptPart.getReturns(): NextScript { + 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)" } + + fun getParametersIndexedByName(): Map = parameters + .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 { return other.getDefinition() == this.getDefinition() } @@ -70,4 +363,44 @@ 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, + ): 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) + } + } diff --git a/src/main/kotlin/fr/postgresjson/definition/Parameter.kt b/src/main/kotlin/fr/postgresjson/definition/Parameter.kt index f43170e..5351330 100644 --- a/src/main/kotlin/fr/postgresjson/definition/Parameter.kt +++ b/src/main/kotlin/fr/postgresjson/definition/Parameter.kt @@ -2,31 +2,39 @@ package fr.postgresjson.definition import java.util.Locale -interface ParameterI { - val name: String - val type: String - val direction: Parameter.Direction - val default: String -} - -class Parameter(val name: String, val type: String, direction: Direction? = Direction.IN, val default: String? = null, val precision: Int? = null, val scale: Int? = null) { - val direction: Direction - - init { - if (direction === null) { - this.direction = Direction.IN +class ArgumentType( + val name: String, + val precision: Int? = null, + val scale: Int? = null, + val isArray: Boolean = false, +) { + override fun toString(): String { + return if (precision == null && scale == null) { + name + } else if (scale == null) { + """$name($precision)""" } else { - this.direction = direction + """$name($precision, $scale)""" } } +} - constructor(name: String, type: String, direction: String? = "IN", default: String? = null, precision: Int? = null, scale: Int? = null) : this( +interface ParameterSimpleI { + val name: String? + val type: ArgumentType +} + +class Parameter( + override val name: String?, + override val type: ArgumentType, + val direction: Direction = Direction.IN, + val default: String? = null, +): ParameterSimpleI { + constructor(name: String?, type: ArgumentType, direction: String = "IN", default: String? = null): this( name = name, type = type, - direction = direction?.let { Direction.valueOf(direction.uppercase(Locale.getDefault())) }, - default = default, - precision = precision, - scale = scale + direction = direction.let { Direction.valueOf(direction.uppercase(Locale.getDefault())) }, + default = default ) enum class Direction { IN, OUT, INOUT } diff --git a/src/main/kotlin/fr/postgresjson/functionGenerator/FunctionGenerator.kt b/src/main/kotlin/fr/postgresjson/functionGenerator/FunctionGenerator.kt index b443ddd..d11eea0 100644 --- a/src/main/kotlin/fr/postgresjson/functionGenerator/FunctionGenerator.kt +++ b/src/main/kotlin/fr/postgresjson/functionGenerator/FunctionGenerator.kt @@ -1,6 +1,8 @@ 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.Parameter import fr.postgresjson.definition.Parameter.Direction.IN import fr.postgresjson.definition.Parameter.Direction.INOUT @@ -19,15 +21,16 @@ class FunctionGenerator(private val functionsDirectories: List) { private fun List.toKotlinArgs(): String { return filter { it.direction == IN || it.direction == INOUT } - .joinToString(", ") { - val base = """${it.kotlinName}: ${it.kotlinType}""" - val default = if (it.default == null) { + .mapIndexed { index, parameter -> index to parameter } + .joinToString(", ") { (idx, param) -> + val base = """${param.kotlinName ?: "arg$idx"}: ${param.kotlinType}""" + val default = if (param.default == null) { "" } else { - when (it.kotlinType) { - "String" -> """ = "${it.default.trim('\'')}"""" - "Int" -> """ = ${it.default}""" - "Boolean" -> """ = ${it.default.lowercase()}""" + when (param.kotlinType) { + "String" -> """ = "${param.default.trim('\'')}"""" + "Int" -> """ = ${param.default}""" + "Boolean" -> """ = ${param.default.lowercase()}""" else -> "" } } @@ -43,7 +46,7 @@ class FunctionGenerator(private val functionsDirectories: List) { private val Parameter.kotlinType: String get() { - return when (type.lowercase()) { + return when (type.name.lowercase()) { "text" -> "String" "varchar" -> "String" "character varying" -> "String" @@ -68,9 +71,9 @@ class FunctionGenerator(private val functionsDirectories: List) { } } - private val Parameter.kotlinName: String + private val Parameter.kotlinName: String? get() { - return name.toCamelCase().trimStart('_') + return name?.toCamelCase()?.trimStart('_') } private val Function.kotlinName: String @@ -107,7 +110,7 @@ class FunctionGenerator(private val functionsDirectories: List) { val args = parameters.toKotlinArgs() 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() if (hasReturn) generics.add("reified E: Any") diff --git a/src/test/kotlin/fr/postgresjson/definition/FunctionTest.kt b/src/test/kotlin/fr/postgresjson/definition/FunctionTest.kt new file mode 100644 index 0000000..1ebdf03 --- /dev/null +++ b/src/test/kotlin/fr/postgresjson/definition/FunctionTest.kt @@ -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") +// } +})