diff --git a/src/main/kotlin/fr/postgresjson/connexion/Connection.kt b/src/main/kotlin/fr/postgresjson/connexion/Connection.kt index 307489d..a7ac1bf 100644 --- a/src/main/kotlin/fr/postgresjson/connexion/Connection.kt +++ b/src/main/kotlin/fr/postgresjson/connexion/Connection.kt @@ -13,8 +13,11 @@ import java.util.concurrent.CompletableFuture interface Executable { fun ?> select(sql: String, typeReference: TypeReference, values: List = emptyList()): R? + fun ?> select(sql: String, typeReference: TypeReference, values: Map): R? fun ?>> select(sql: String, typeReference: TypeReference, values: List = emptyList()): R? + fun ?>> select(sql: String, typeReference: TypeReference, values: Map): R fun exec(sql: String, values: List = emptyList()): CompletableFuture + fun exec(sql: String, values: Map): CompletableFuture } class Connection( @@ -47,9 +50,21 @@ class Connection( serializer.deserialize(json, typeReference) } } - inline fun ?> selectOne(sql: String, values: List = emptyList()): R? = select(sql, object: TypeReference() {}, values) + override fun ?> select(sql: String, typeReference: TypeReference, values: Map): R? { + val args = compileArgs(values) + val replacedQuery = replaceArgs(sql, args) + val future = connect().sendPreparedStatement(replacedQuery.sql, replacedQuery.parameters) + val json = future.get().rows[0].getString(0) + return if (json === null) { + null + } else { + serializer.deserialize(json, typeReference) + } + } + inline fun ?> selectOne(sql: String, values: Map): R? = select(sql, object: TypeReference() {}, values) + override fun ?>> select(sql: String, typeReference: TypeReference, values: List): R { val future = connect().sendPreparedStatement(sql, compileArgs(values)) val json = future.get().rows[0].getString(0) @@ -59,13 +74,30 @@ class Connection( serializer.deserializeList(json, typeReference) } } - inline fun ?>> select(sql: String, values: List = emptyList()): R = select(sql, object : TypeReference() {}, values) + override fun ?>> select(sql: String, typeReference: TypeReference, values: Map): R { + val args = compileArgs(values) + val replacedQuery = replaceArgs(sql, args) + val future = connect().sendPreparedStatement(replacedQuery.sql, replacedQuery.parameters) + val json = future.get().rows[0].getString(0) + return if (json === null) { + listOf?>() as R + } else { + serializer.deserializeList(json, typeReference) + } + } + inline fun ?>> select(sql: String, values: Map): R = select(sql, object : TypeReference() {}, values) + override fun exec(sql: String, values: List): CompletableFuture { return connect().sendPreparedStatement(sql, compileArgs(values)) } + override fun exec(sql: String, values: Map): CompletableFuture { + val replacedQuery = replaceArgs(sql, values) + return exec(replacedQuery.sql, replacedQuery.parameters) + } + private fun compileArgs(values: List): List { return values.map { if (it is EntityI<*>) { @@ -77,5 +109,36 @@ class Connection( } } } + + private fun compileArgs(values: Map): Map { + return values.map {(key, value) -> + if (value is EntityI<*>) { + val json = serializer.serialize(value) + serializer.collection.set>(value as EntityI) + key to json + } else { + key to value + } + }.toMap() + } + + private fun replaceArgs(sql: String, values: Map): ParametersQuery { + val paramRegex = "(? + val name = match.groups[1]!!.value + values[name] ?: error("Parameter $name missing") + }.toList() + + var newSql = sql + values.forEach { (key, _) -> + val regex = ":$key".toRegex() + newSql = newSql.replace(regex, "?") + } + + + return ParametersQuery(newSql, newArgs) + } + + data class ParametersQuery(val sql: String, val parameters: List) } diff --git a/src/main/kotlin/fr/postgresjson/connexion/Requester.kt b/src/main/kotlin/fr/postgresjson/connexion/Requester.kt index 713f339..64907a3 100644 --- a/src/main/kotlin/fr/postgresjson/connexion/Requester.kt +++ b/src/main/kotlin/fr/postgresjson/connexion/Requester.kt @@ -82,18 +82,30 @@ class Requester ( override fun ?> select(typeReference: TypeReference, values: List): R? { return connection.select(this.toString(), typeReference, values) } - inline fun ?> selectOne(values: List = emptyList()): R? = select(object: TypeReference() {}, values) + override fun ?> select(typeReference: TypeReference, values: Map): R? { + return connection.select(this.toString(), typeReference, values) + } + inline fun ?> selectOne(values: Map): R? = select(object: TypeReference() {}, values) + override fun ?>> select(typeReference: TypeReference, values: List): R? { return connection.select(this.toString(), typeReference, values) } - inline fun ?>> select(values: List = emptyList()): R? = select(object: TypeReference() {}, values) + override fun ?>> select(typeReference: TypeReference, values: Map): R { + return connection.select(this.toString(), typeReference, values) + } + inline fun ?>> select(values: Map): R? = select(object: TypeReference() {}, values) + override fun exec(values: List): CompletableFuture { return connection.exec(sql, values) } + + override fun exec(values: Map): CompletableFuture { + return connection.exec(sql, values) + } } class Function(val definition: DefinitionFunction, override val connection : Connection): Executable { @@ -101,24 +113,50 @@ class Requester ( return definition.name } + /** + * Select One entity with list of parameters + */ override fun ?> select(typeReference: TypeReference, values: List): R? { val args = compileArgs(values) val sql = "SELECT * FROM ${definition.name} ($args)" return connection.select(sql, typeReference, values) } - inline fun ?> selectOne(values: List = emptyList()): R? = select(object: TypeReference() {}, values) + /** + * Select One entity with named parameters + */ + override fun ?> select(typeReference: TypeReference, values: Map): R? { + val args = compileArgs(values) + val sql = "SELECT * FROM ${definition.name} ($args)" + + return connection.select(sql, typeReference, values) + } + inline fun ?> selectOne(values: Map): R? = select(object: TypeReference() {}, values) + + /** + * Select list of entities with list of parameters + */ override fun ?>> select(typeReference: TypeReference, values: List): R? { val args = compileArgs(values) val sql = "SELECT * FROM ${definition.name} ($args)" return connection.select(sql, typeReference, values) } - inline fun ?>> select(values: List = emptyList()): R? = select(object: TypeReference() {}, values) + /** + * Select list of entities with named parameters + */ + override fun ?>> select(typeReference: TypeReference, values: Map): R { + val args = compileArgs(values) + val sql = "SELECT * FROM ${definition.name} ($args)" + + return connection.select(sql, typeReference, values) + } + inline fun ?>> select(values: Map): R? = select(object: TypeReference() {}, values) + override fun exec(values: List): CompletableFuture { val args = compileArgs(values) val sql = "SELECT * FROM ${definition.name} ($args)" @@ -126,6 +164,13 @@ class Requester ( return connection.exec(sql, values) } + override fun exec(values: Map): CompletableFuture { + val args = compileArgs(values) + val sql = "SELECT * FROM ${definition.name} ($args)" + + return connection.exec(sql, values) + } + private fun compileArgs(values: List): String { val placeholders = values .filterIndexed { index, any -> @@ -137,6 +182,21 @@ class Requester ( return placeholders.joinToString(separator=", ") } + + private fun compileArgs(values: Map): String { + val parameters = definition.getParametersIndexedByName() + val placeholders = values + .filter { entry -> + val parameter = parameters[entry.key] ?: error("Parameter ${entry.key} not exist") + parameter.default === null || entry.value !== null + } + .map { entry -> + val parameter = parameters[entry.key]!! + "${parameter.name} := :${parameter.name}::" + parameter.type + } + + return placeholders.joinToString(separator=", ") + } } interface Executable { @@ -144,10 +204,13 @@ class Requester ( override fun toString(): String fun ?> select(typeReference: TypeReference, values: List = emptyList()): R? + fun ?> select(typeReference: TypeReference, values: Map): R? fun ?>> select(typeReference: TypeReference, values: List = emptyList()): R? + fun ?>> select(typeReference: TypeReference, values: Map): R fun exec(values: List = emptyList()): CompletableFuture + fun exec(values: Map): CompletableFuture } class RequesterFactory( diff --git a/src/main/kotlin/fr/postgresjson/definition/Function.kt b/src/main/kotlin/fr/postgresjson/definition/Function.kt index 4f26d64..6be432c 100644 --- a/src/main/kotlin/fr/postgresjson/definition/Function.kt +++ b/src/main/kotlin/fr/postgresjson/definition/Function.kt @@ -12,7 +12,7 @@ open class Function ( override var source: File? = null init { - val functionRegex = """create .*(procedure|function) *(?[^(\s]+)\s*\((?(\s*((IN|OUT|INOUT|VARIADIC)?\s+)?([^\s,)]+\s+)?([^\s,)]+)(\s+(?:default\s|=)\s*[^\s,)]+)?\s*(,|(?=\))))*)\) *(?RETURNS *[^ ]+)?""" + 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 *[^ ]+)?""" .toRegex(setOf(RegexOption.IGNORE_CASE, RegexOption.MULTILINE)) val paramsRegex = """\s*(?((?IN|OUT|INOUT|VARIADIC)?\s+)?(?[^\s,)]+\s+)?(?[^\s,)]+)(\s+(?default\s|=)\s*[^\s,)]+)?)\s*(,|$)""" @@ -50,6 +50,12 @@ open class Function ( return "$name (" + parameters.joinToString(", ") + ") $returns" } + fun getParametersIndexedByName(): Map { + return parameters.map { + it.name to it + }.toMap() + } + infix fun `has same definition` (other: Function): Boolean { return other.getDefinition() == this.getDefinition() } diff --git a/src/test/kotlin/fr/postgresjson/ConnectionTest.kt b/src/test/kotlin/fr/postgresjson/ConnectionTest.kt index 2d53c78..a089e47 100644 --- a/src/test/kotlin/fr/postgresjson/ConnectionTest.kt +++ b/src/test/kotlin/fr/postgresjson/ConnectionTest.kt @@ -3,7 +3,7 @@ package fr.postgresjson import com.github.jasync.sql.db.util.isCompleted import fr.postgresjson.connexion.Connection import fr.postgresjson.entity.IdEntity -import org.junit.jupiter.api.Assertions.* +import org.junit.Assert.* import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance @@ -12,6 +12,7 @@ import org.junit.jupiter.api.TestInstance class ConnectionTest(): TestAbstract() { private class ObjTest(var name: String): IdEntity() private class ObjTest2(var title: String, var test: ObjTest?): IdEntity() + private class ObjTest3(var first: String, var seconde: String, var third: Int): IdEntity() private lateinit var connection: Connection @@ -60,10 +61,10 @@ class ConnectionTest(): TestAbstract() { val o = ObjTest("myName") o.id = 88 val obj: ObjTest? = connection.selectOne("select json_build_object('id', id, 'name', name) FROM json_to_record(?::json) as o(id int, name text);", listOf(o)) - assertTrue(obj !== null) + assertNotNull(obj) assertTrue(obj is ObjTest) - assertTrue(obj!!.id == 88) - assertTrue(obj.name == "myName") + assertEquals(obj!!.id, 88) + assertEquals(obj.name, "myName") } @Test @@ -73,4 +74,59 @@ class ConnectionTest(): TestAbstract() { future.join() assertTrue(future.isCompleted) } + + @Test + fun `select one with named parameters`() { + val result: ObjTest3? = connection.selectOne( + "SELECT json_build_object('first', :first::text, 'seconde', :seconde::text, 'third', :third::int)", + mapOf( + "first" to "ff", + "seconde" to "sec", + "third" to 123 + ) + ) + assertEquals(result!!.first, "ff") + assertEquals(result.seconde, "sec") + assertEquals(result.third, 123) + } + + @Test + fun `select with named parameters`() { + val params: Map = mapOf( + "first" to "ff", + "third" to 123, + "seconde" to "sec" + ) + val result: List = connection.select( + """ + SELECT json_build_array( + json_build_object('first', :first::text, 'seconde', :seconde::text, 'third', :third::int), + json_build_object('first', :first::text, 'seconde', :seconde::text, 'third', :third::int) + ) + """.trimIndent(), + params + ) + assertEquals(result[0]!!.first, "ff") + assertEquals(result[0]!!.seconde, "sec") + assertEquals(result[0]!!.third, 123) + } + + @Test + fun `selectOne with named parameters`() { + val params: Map = mapOf( + "first" to "ff", + "third" to 123, + "seconde" to "sec" + ) + val result: ObjTest3? = connection.selectOne( + """ + SELECT json_build_object('first', :first::text, 'seconde', :seconde::text, 'third', :third::int) + """.trimIndent(), + params + ) + assertNotNull(result) + assertEquals(result!!.first, "ff") + assertEquals(result.seconde, "sec") + assertEquals(result.third, 123) + } } \ No newline at end of file diff --git a/src/test/kotlin/fr/postgresjson/RequestTest.kt b/src/test/kotlin/fr/postgresjson/RequesterTest.kt similarity index 58% rename from src/test/kotlin/fr/postgresjson/RequestTest.kt rename to src/test/kotlin/fr/postgresjson/RequesterTest.kt index 6aa6fd8..61d28d9 100644 --- a/src/test/kotlin/fr/postgresjson/RequestTest.kt +++ b/src/test/kotlin/fr/postgresjson/RequesterTest.kt @@ -10,11 +10,11 @@ import org.junit.jupiter.api.Test import java.io.File import java.util.concurrent.CompletableFuture -class RequestTest: TestAbstract() { +class RequesterTest: TestAbstract() { class ObjTest(var name:String): IdEntity(1) @Test - fun getQueryFromFile() { + fun `get query from file`() { val resources = File(this::class.java.getResource("/sql/query").toURI()) val objTest: ObjTest? = Requester(getConnextion()) .addQuery(resources) @@ -26,7 +26,7 @@ class RequestTest: TestAbstract() { } @Test - fun getFunctionFromFile() { + fun `get function from file`() { val resources = File(this::class.java.getResource("/sql/function").toURI()) val objTest: ObjTest? = Requester(getConnextion()) .addFunction(resources) @@ -38,7 +38,7 @@ class RequestTest: TestAbstract() { } @Test - fun callExecOnQuery() { + fun `call exec on query`() { val resources = File(this::class.java.getResource("/sql/query").toURI()) val future: CompletableFuture = Requester(getConnextion()) .addQuery(resources) @@ -50,7 +50,7 @@ class RequestTest: TestAbstract() { } @Test - fun callExecOnFunction() { + fun `call exec on function`() { val resources = File(this::class.java.getResource("/sql/function").toURI()) val future: CompletableFuture = Requester(getConnextion()) .addFunction(resources) @@ -60,4 +60,37 @@ class RequestTest: TestAbstract() { future.join() Assertions.assertTrue(future.isCompleted) } + + @Test + fun `call selectOne on function`() { + val resources = File(this::class.java.getResource("/sql/function").toURI()) + val obj: ObjTest = Requester(getConnextion()) + .addFunction(resources) + .getFunction("test_function") + .selectOne(mapOf("name" to "myName"))!! + + assertEquals("myName", obj.name) + } + + @Test + fun `call selectOne on query`() { + val resources = File(this::class.java.getResource("/sql/query").toURI()) + val obj: ObjTest = Requester(getConnextion()) + .addQuery(resources) + .getQuery("Test/selectOneWithParameters") + .selectOne(mapOf("name" to "myName"))!! + + assertEquals("myName", obj.name) + } + + @Test + fun `call select (multiple) on function`() { + val resources = File(this::class.java.getResource("/sql/function").toURI()) + val obj: List? = Requester(getConnextion()) + .addFunction(resources) + .getFunction("test_function_multiple") + .select(mapOf("name" to "myName")) + + assertEquals("myName", obj!![0].name) + } } \ No newline at end of file diff --git a/src/test/resources/fixtures/init.sql b/src/test/resources/fixtures/init.sql index fc974ce..ccd1dab 100644 --- a/src/test/resources/fixtures/init.sql +++ b/src/test/resources/fixtures/init.sql @@ -33,6 +33,19 @@ CREATE OR REPLACE FUNCTION test_function (name text default 'plop', IN hi text d AS $$ BEGIN - result = json_build_object('id', 3, 'name', 'test'); + result = json_build_object('id', 3, 'name', name); END; +$$; + + +CREATE OR REPLACE FUNCTION test_function_multiple (name text default 'plop', IN hi text default 'hello', out result json) + LANGUAGE plpgsql +AS $$ +BEGIN + result = json_build_array( + json_build_object('id', 3, 'name', name), + json_build_object('id', 4, 'name', hi) + ); +END; +$$; \ No newline at end of file diff --git a/src/test/resources/sql/function/Test/test_function_multiple.sql b/src/test/resources/sql/function/Test/test_function_multiple.sql new file mode 100644 index 0000000..5f94730 --- /dev/null +++ b/src/test/resources/sql/function/Test/test_function_multiple.sql @@ -0,0 +1,11 @@ +CREATE OR REPLACE FUNCTION test_function_multiple (name text default 'plop', IN hi text default 'hello', out result json) +LANGUAGE plpgsql +AS +$$ +BEGIN + result = json_build_array( + json_build_object('id', 3, 'name', name), + json_build_object('id', 4, 'name', hi) + ); +END; +$$ \ No newline at end of file diff --git a/src/test/resources/sql/query/Test/selectOneWithParameters.sql b/src/test/resources/sql/query/Test/selectOneWithParameters.sql new file mode 100644 index 0000000..3e90686 --- /dev/null +++ b/src/test/resources/sql/query/Test/selectOneWithParameters.sql @@ -0,0 +1 @@ +select json_build_object('id', 2, 'name', :name::text); \ No newline at end of file