WIP: First implement of compiled function

This commit is contained in:
2023-01-13 00:29:08 +01:00
parent 72a7aa7273
commit 39bae86307
7 changed files with 189 additions and 15 deletions

View File

@@ -12,28 +12,31 @@ class Function(
init {
val functionRegex =
"""create (or replace )?(procedure|function) *(?<name>[^(\s]+)\s*\((?<params>(\s*((IN|OUT|INOUT|VARIADIC)?\s+)?([^\s,)]+\s+)?([^\s,)]+)(\s+(?:default\s|=)\s*[^\s,)]+)?\s*(,|(?=\))))*)\) *(?<return>RETURNS *[^ \n]+)?"""
"""create (or replace )?(procedure|function) *(?<fname>[^(\s]+)\s*\(\s*(?<params>\s*([^()]+(\([^)]+\))*)*)\s*\)(RETURNS *(?<return>[^ \n]+))?"""
.toRegex(setOf(RegexOption.IGNORE_CASE, RegexOption.MULTILINE))
val paramsRegex =
"""\s*(?<param>((?<direction>IN|OUT|INOUT|VARIADIC)?\s+)?("?(?<name>[^\s,")]+)"?\s+)?(?<type>[^\s,)]+)(\s+(?<default>default\s|=)\s*[^\s,)]+)?)\s*(,|$)"""
"""\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)
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 ->
paramsRegex
.findAll(functionParameters)
.mapIndexed { index, paramsMatch ->
Parameter(
paramsMatch.groups["name"]!!.value.trim(),
paramsMatch.groups["type"]!!.value.trim(),
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["default"]?.value?.trim(),
paramsMatch.groups["precision"]?.value?.trim()?.toInt(),
paramsMatch.groups["scale"]?.value?.trim()?.toInt()
)
}.toList()
} else {
@@ -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

View File

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

View File

@@ -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<URI>) {
constructor(functionsDirectories: URI): this(listOf(functionsDirectories))
private val logger: Logger = LoggerFactory.getLogger("sqlFilesSearch")
private fun List<Parameter>.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<Parameter>.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<Function>()
.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<E>" else ""
val returnWord = if (hasReturn) "return " else ""
val select = if (hasReturn) "select<E>" 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())
}}
}
}
}

View File

@@ -0,0 +1,7 @@
package fr.postgresjson.utils
fun String.toCamelCase(): String {
return "_[a-zA-Z]".toRegex().replace(this) {
it.value.replace("_", "").uppercase()
}
}

View File

@@ -14,7 +14,7 @@ import kotlin.streams.asSequence
fun URL.searchSqlFiles() = this.toURI().searchSqlFiles()
fun URI.searchSqlFiles() = sequence {
fun URI.searchSqlFiles(): Sequence<Resource> = sequence {
val logger: Logger = LoggerFactory.getLogger("sqlFilesSearch")
val uri: URI = this@searchSqlFiles
logger.debug("""SQL files found in "${uri.toString().substringAfter('!')}" :""")

View File

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

View File

@@ -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;
$$;