WIP: Compiled SQL function #33
@@ -12,29 +12,32 @@ class Function(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
val functionRegex =
|
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))
|
.toRegex(setOf(RegexOption.IGNORE_CASE, RegexOption.MULTILINE))
|
||||||
|
|
||||||
val paramsRegex =
|
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))
|
.toRegex(setOf(RegexOption.IGNORE_CASE, RegexOption.MULTILINE))
|
||||||
|
|
||||||
val queryMatch = functionRegex.find(script)
|
val queryMatch = functionRegex.find(script)
|
||||||
if (queryMatch !== null) {
|
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()
|
val functionParameters = queryMatch.groups["params"]?.value?.trim()
|
||||||
this.returns = queryMatch.groups["return"]?.value?.trim() ?: ""
|
this.returns = queryMatch.groups["return"]?.value?.trim() ?: ""
|
||||||
|
|
||||||
/* Create parameters definition */
|
/* Create parameters definition */
|
||||||
val parameters = if (functionParameters !== null) {
|
val parameters = if (functionParameters !== null) {
|
||||||
val matchesParams = paramsRegex.findAll(functionParameters)
|
paramsRegex
|
||||||
matchesParams.map { paramsMatch ->
|
.findAll(functionParameters)
|
||||||
Parameter(
|
.mapIndexed { index, paramsMatch ->
|
||||||
paramsMatch.groups["name"]!!.value.trim(),
|
Parameter(
|
||||||
paramsMatch.groups["type"]!!.value.trim(),
|
paramsMatch.groups["pname"]?.value?.trim() ?: """arg${index+1}""",
|
||||||
paramsMatch.groups["direction"]?.value?.trim(),
|
paramsMatch.groups["type"]?.value?.trim() ?: throw ArgumentNotFound(),
|
||||||
paramsMatch.groups["default"]?.value?.trim()
|
paramsMatch.groups["direction"]?.value?.trim(),
|
||||||
)
|
paramsMatch.groups["default"]?.value?.trim(),
|
||||||
|
paramsMatch.groups["precision"]?.value?.trim()?.toInt(),
|
||||||
|
paramsMatch.groups["scale"]?.value?.trim()?.toInt()
|
||||||
|
)
|
||||||
}.toList()
|
}.toList()
|
||||||
} else {
|
} else {
|
||||||
listOf()
|
listOf()
|
||||||
@@ -47,6 +50,7 @@ class Function(
|
|||||||
}
|
}
|
||||||
|
|
||||||
class FunctionNotFound(cause: Throwable? = null) : Resource.ParseException("Function not found in script", cause)
|
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 {
|
fun getDefinition(): String {
|
||||||
return parameters
|
return parameters
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ interface ParameterI {
|
|||||||
val default: String
|
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
|
val direction: Direction
|
||||||
|
|
||||||
init {
|
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,
|
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 }
|
||||||
|
|||||||
@@ -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())
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/main/kotlin/fr/postgresjson/utils/caseChange.kt
Normal file
7
src/main/kotlin/fr/postgresjson/utils/caseChange.kt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package fr.postgresjson.utils
|
||||||
|
|
||||||
|
fun String.toCamelCase(): String {
|
||||||
|
return "_[a-zA-Z]".toRegex().replace(this) {
|
||||||
|
it.value.replace("_", "").uppercase()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ import kotlin.streams.asSequence
|
|||||||
|
|
||||||
fun URL.searchSqlFiles() = this.toURI().searchSqlFiles()
|
fun URL.searchSqlFiles() = this.toURI().searchSqlFiles()
|
||||||
|
|
||||||
fun URI.searchSqlFiles() = sequence {
|
fun URI.searchSqlFiles(): Sequence<Resource> = sequence {
|
||||||
val logger: Logger = LoggerFactory.getLogger("sqlFilesSearch")
|
val logger: Logger = LoggerFactory.getLogger("sqlFilesSearch")
|
||||||
val uri: URI = this@searchSqlFiles
|
val uri: URI = this@searchSqlFiles
|
||||||
logger.debug("""SQL files found in "${uri.toString().substringAfter('!')}" :""")
|
logger.debug("""SQL files found in "${uri.toString().substringAfter('!')}" :""")
|
||||||
|
|||||||
@@ -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/"))
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/test/resources/sql/function/Test/function_multiparam.sql
Normal file
34
src/test/resources/sql/function/Test/function_multiparam.sql
Normal 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;
|
||||||
|
$$;
|
||||||
Reference in New Issue
Block a user