From 39bae86307b065200dffcd210655d734faf238ec Mon Sep 17 00:00:00 2001 From: Fabrice Lecomte Date: Fri, 13 Jan 2023 00:29:08 +0100 Subject: [PATCH] WIP: First implement of compiled function --- .../fr/postgresjson/definition/Function.kt | 26 ++-- .../fr/postgresjson/definition/Parameter.kt | 8 +- .../functionGenerator/FunctionGenerator.kt | 113 ++++++++++++++++++ .../fr/postgresjson/utils/caseChange.kt | 7 ++ .../fr/postgresjson/utils/searchSqlFiles.kt | 2 +- .../FunctionGeneratorTest.kt | 14 +++ .../sql/function/Test/function_multiparam.sql | 34 ++++++ 7 files changed, 189 insertions(+), 15 deletions(-) create mode 100644 src/main/kotlin/fr/postgresjson/functionGenerator/FunctionGenerator.kt create mode 100644 src/main/kotlin/fr/postgresjson/utils/caseChange.kt create mode 100644 src/test/kotlin/fr/postgresjson/functionGenerator/FunctionGeneratorTest.kt create mode 100644 src/test/resources/sql/function/Test/function_multiparam.sql diff --git a/src/main/kotlin/fr/postgresjson/definition/Function.kt b/src/main/kotlin/fr/postgresjson/definition/Function.kt index 1afa240..5e82eac 100644 --- a/src/main/kotlin/fr/postgresjson/definition/Function.kt +++ b/src/main/kotlin/fr/postgresjson/definition/Function.kt @@ -12,29 +12,32 @@ class Function( init { val functionRegex = - """create (or replace )?(procedure|function) *(?[^(\s]+)\s*\((?(\s*((IN|OUT|INOUT|VARIADIC)?\s+)?([^\s,)]+\s+)?([^\s,)]+)(\s+(?:default\s|=)\s*[^\s,)]+)?\s*(,|(?=\))))*)\) *(?RETURNS *[^ \n]+)?""" + """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+)?(?[^\s,)]+)(\s+(?default\s|=)\s*[^\s,)]+)?)\s*(,|$)""" + """\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)) val queryMatch = functionRegex.find(script) if (queryMatch !== null) { - val functionName = queryMatch.groups["name"]?.value?.trim() ?: error("Function name not found") + 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) { - val matchesParams = paramsRegex.findAll(functionParameters) - matchesParams.map { paramsMatch -> - Parameter( - paramsMatch.groups["name"]!!.value.trim(), - paramsMatch.groups["type"]!!.value.trim(), - paramsMatch.groups["direction"]?.value?.trim(), - paramsMatch.groups["default"]?.value?.trim() - ) + 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() } else { listOf() @@ -47,6 +50,7 @@ class Function( } 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 diff --git a/src/main/kotlin/fr/postgresjson/definition/Parameter.kt b/src/main/kotlin/fr/postgresjson/definition/Parameter.kt index 9b97294..f43170e 100644 --- a/src/main/kotlin/fr/postgresjson/definition/Parameter.kt +++ b/src/main/kotlin/fr/postgresjson/definition/Parameter.kt @@ -9,7 +9,7 @@ interface ParameterI { val default: String } -class Parameter(val name: String, val type: String, direction: Direction? = Direction.IN, val default: Any? = null) { +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 { @@ -20,11 +20,13 @@ class Parameter(val name: String, val type: String, direction: Direction? = Dire } } - constructor(name: String, type: String, direction: String? = "IN", default: Any? = null) : this( + constructor(name: String, type: String, direction: String? = "IN", default: String? = null, precision: Int? = null, scale: Int? = null) : this( name = name, type = type, direction = direction?.let { Direction.valueOf(direction.uppercase(Locale.getDefault())) }, - default = default + default = default, + precision = precision, + scale = scale ) 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 new file mode 100644 index 0000000..fc44f55 --- /dev/null +++ b/src/main/kotlin/fr/postgresjson/functionGenerator/FunctionGenerator.kt @@ -0,0 +1,113 @@ +package fr.postgresjson.functionGenerator + +import fr.postgresjson.definition.Function +import fr.postgresjson.definition.Parameter +import fr.postgresjson.definition.Parameter.Direction.IN +import fr.postgresjson.definition.Parameter.Direction.INOUT +import fr.postgresjson.definition.Parameter.Direction.OUT +import fr.postgresjson.utils.searchSqlFiles +import fr.postgresjson.utils.toCamelCase +import java.io.File +import java.net.URI +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +class FunctionGenerator(private val functionsDirectories: List) { + constructor(functionsDirectories: URI): this(listOf(functionsDirectories)) + + private val logger: Logger = LoggerFactory.getLogger("sqlFilesSearch") + + 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) { + "" + } else { + when (it.kotlinType) { + "String" -> """ = "${it.default.trim('\'')}"""" + "Int" -> """ = ${it.default}""" + "Boolean" -> """ = ${it.default.lowercase()}""" + else -> "" + } + } + + base+default + } + } + private fun List.toMapOf(): String { + return filter { it.direction == IN || it.direction == INOUT } + .joinToString(", ", prefix = "mapOf(", postfix = ")") { """"${it.kotlinName}" to ${it.kotlinName}""" } + } + + private val Parameter.kotlinType: String + get() { + return when (type.lowercase()) { + "text" -> "String" + "varchar" -> "String" + "character varying" -> "String" + "character" -> "String" + "char" -> "String" + "int" -> "Int" + "boolean" -> "Boolean" + "json" -> "S" + "jsonb" -> "S" + "any" -> "Any" + "anyelement" -> "Any" + "anyarray" -> "List<*>" + else -> "String" + } + } + + private val Parameter.kotlinName: String + get() { + return name.toCamelCase().trimStart('_') + } + + private val Function.kotlinName: String + get() { + return name.toCamelCase().trimStart('_') + } + + fun generate(outputDirectory: URI) { + File(outputDirectory.path).apply { + logger.debug("Create Directory: $absolutePath") + mkdirs() + } + + this.functionsDirectories + .flatMap { it.searchSqlFiles() } + .filterIsInstance() + .map { it.run { + val args = parameters.toKotlinArgs() + + File("${outputDirectory.path}${kotlinName}.kt").apply { + logger.debug("Create kotlin file: $absolutePath") + val hasGenerics: Boolean = parameters.filter { it.direction != OUT }.any { it.kotlinType == "S" } + val genericsType = if (hasGenerics) ", S: Serializable" else "" + + val hasReturn = parameters.any { it.direction != IN } || (it.returns != "" && it.returns != "void") + val returnTypeGenerics = if (hasReturn) "reified E: EntityI" else "" + val returnType = if (hasReturn) ": List" else "" + val returnWord = if (hasReturn) "return " else "" + val select = if (hasReturn) "select" else "exec" + val function = if (hasGenerics || hasReturn) """inline fun <$returnTypeGenerics$genericsType>""" else "fun" + + val importEntityI = if (hasReturn) "import fr.postgresjson.entity.EntityI\n" else "" + val importSerializable = if (hasGenerics) "import fr.postgresjson.entity.Serializable\n" else "" + val importSelect = if (hasReturn) "import fr.postgresjson.connexion.select\n" else "" + + writeText(""" + |package fr.postgresjson.functionGenerator.generated + | + |import fr.postgresjson.connexion.Requester + |$importSelect$importSerializable$importEntityI + |$function Requester.$kotlinName($args)$returnType { + | ${returnWord}getFunction("${it.name}") + | .$select(${parameters.toMapOf()}) + |} + """.trimMargin()) + }} + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/fr/postgresjson/utils/caseChange.kt b/src/main/kotlin/fr/postgresjson/utils/caseChange.kt new file mode 100644 index 0000000..d3f6542 --- /dev/null +++ b/src/main/kotlin/fr/postgresjson/utils/caseChange.kt @@ -0,0 +1,7 @@ +package fr.postgresjson.utils + +fun String.toCamelCase(): String { + return "_[a-zA-Z]".toRegex().replace(this) { + it.value.replace("_", "").uppercase() + } +} diff --git a/src/main/kotlin/fr/postgresjson/utils/searchSqlFiles.kt b/src/main/kotlin/fr/postgresjson/utils/searchSqlFiles.kt index 4e41e21..bfc5d27 100644 --- a/src/main/kotlin/fr/postgresjson/utils/searchSqlFiles.kt +++ b/src/main/kotlin/fr/postgresjson/utils/searchSqlFiles.kt @@ -14,7 +14,7 @@ import kotlin.streams.asSequence fun URL.searchSqlFiles() = this.toURI().searchSqlFiles() -fun URI.searchSqlFiles() = sequence { +fun URI.searchSqlFiles(): Sequence = sequence { val logger: Logger = LoggerFactory.getLogger("sqlFilesSearch") val uri: URI = this@searchSqlFiles logger.debug("""SQL files found in "${uri.toString().substringAfter('!')}" :""") diff --git a/src/test/kotlin/fr/postgresjson/functionGenerator/FunctionGeneratorTest.kt b/src/test/kotlin/fr/postgresjson/functionGenerator/FunctionGeneratorTest.kt new file mode 100644 index 0000000..00cf5b7 --- /dev/null +++ b/src/test/kotlin/fr/postgresjson/functionGenerator/FunctionGeneratorTest.kt @@ -0,0 +1,14 @@ +package fr.postgresjson.functionGenerator + +import java.net.URI +import org.junit.jupiter.api.Test + +class FunctionGeneratorTest { + + @Test + fun generate() { + val functionDirectory = this::class.java.getResource("/sql/function/Test")!!.toURI() + FunctionGenerator(functionDirectory) + .generate(URI( "./src/test/kotlin/fr/postgresjson/functionGenerator/generated/")) + } +} \ No newline at end of file diff --git a/src/test/resources/sql/function/Test/function_multiparam.sql b/src/test/resources/sql/function/Test/function_multiparam.sql new file mode 100644 index 0000000..8d599f8 --- /dev/null +++ b/src/test/resources/sql/function/Test/function_multiparam.sql @@ -0,0 +1,34 @@ +CREATE OR REPLACE FUNCTION function_multiparam ( + name varchar(45) default 'plop', + numeric(4, 5), + num float(5), + num2 timestamp without time zone default '2002-01-01T00:00:00'::timestamp, + num3 int, + num4 integer, + num5 smallint, + num6 bigint, + num7 decimal, + num8 decimal(4, 6), + num9 real, + num10 double precision, + num11 smallserial, + num12 serial, + "num13" bigserial, + num14 serial, + num15 money, + num16 character varying(789), + num16b character varying(789) default 'abc', + num16c character varying default 'abc', + num17 character(56), + num18 char(2), + num19 any, + num20 anyelement, + num21 anyarray +) +LANGUAGE plpgsql +AS +$$ +BEGIN + PERFORM 1; +END; +$$;