From 6cd8b89e133faecd1f8a5fa28bf2bd443529f913 Mon Sep 17 00:00:00 2001 From: "jonghyon.s" Date: Mon, 15 Jan 2024 18:41:35 +0900 Subject: [PATCH] feat: custom predicate --- benchmark/build.gradle.kts | 11 ++ docs/en/jpql-with-kotlin-jdsl/expressions.md | 5 + docs/en/jpql-with-kotlin-jdsl/predicates.md | 23 ++++ docs/ko/jpql-with-kotlin-jdsl/expressions.md | 5 + docs/ko/jpql-with-kotlin-jdsl/predicates.md | 23 ++++ .../com/linecorp/kotlinjdsl/dsl/jpql/Jpql.kt | 62 +++++++++ .../jpql/predicate/CustomPredicateDslTest.kt | 60 +++++++++ .../dsl/jpql/predicate/FunctionDslTest.kt | 60 +++++++++ .../H2DBConnectionPoolConfiguration.kt | 4 +- .../querymodel/jpql/expression/Expressions.kt | 6 +- ...lFunction.kt => JpqlFunctionExpression.kt} | 2 +- .../querymodel/jpql/predicate/Predicates.kt | 33 +++++ .../predicate/impl/JpqlCustomPredicate.kt | 11 ++ .../predicate/impl/JpqlFunctionPredicate.kt | 11 ++ .../jpql/expression/ExpressionsTest.kt | 4 +- .../jpql/predicate/PredicatesTest.kt | 39 ++++++ .../render/jpql/JpqlRenderContext.kt | 8 +- .../impl/JpqlCustomExpressionSerializer.kt | 58 +------- .../impl/JpqlCustomPredicateSerializer.kt | 20 +++ .../impl/JpqlFunctionExpressionSerializer.kt | 20 +++ .../impl/JpqlFunctionPredicateSerializer.kt | 20 +++ .../JpqlFunctionSerializerSupport.kt} | 20 +-- .../support/JpqlTemplateSerializerSupport.kt | 41 ++++++ .../JpqlCustomExpressionSerializerTest.kt | 23 ++++ .../impl/JpqlCustomPredicateSerializerTest.kt | 126 ++++++++++++++++++ ...> JpqlFunctionExpressionSerializerTest.kt} | 12 +- .../JpqlFunctionPredicateSerializerTest.kt | 95 +++++++++++++ .../kotlinjdsl/render/template/Template.kt | 54 ++++++++ .../render/template/TemplateElement.kt | 14 ++ .../render/template/TemplateTest.kt | 66 +++++++++ 30 files changed, 852 insertions(+), 84 deletions(-) create mode 100644 dsl/jpql/src/test/kotlin/com/linecorp/kotlinjdsl/dsl/jpql/predicate/CustomPredicateDslTest.kt create mode 100644 dsl/jpql/src/test/kotlin/com/linecorp/kotlinjdsl/dsl/jpql/predicate/FunctionDslTest.kt rename query-model/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/querymodel/jpql/expression/impl/{JpqlFunction.kt => JpqlFunctionExpression.kt} (83%) create mode 100644 query-model/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/querymodel/jpql/predicate/impl/JpqlCustomPredicate.kt create mode 100644 query-model/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/querymodel/jpql/predicate/impl/JpqlFunctionPredicate.kt create mode 100644 render/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/impl/JpqlCustomPredicateSerializer.kt create mode 100644 render/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/impl/JpqlFunctionExpressionSerializer.kt create mode 100644 render/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/impl/JpqlFunctionPredicateSerializer.kt rename render/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/{impl/JpqlFunctionSerializer.kt => support/JpqlFunctionSerializerSupport.kt} (50%) create mode 100644 render/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/support/JpqlTemplateSerializerSupport.kt create mode 100644 render/jpql/src/test/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/impl/JpqlCustomPredicateSerializerTest.kt rename render/jpql/src/test/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/impl/{JpqlFunctionSerializerTest.kt => JpqlFunctionExpressionSerializerTest.kt} (87%) create mode 100644 render/jpql/src/test/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/impl/JpqlFunctionPredicateSerializerTest.kt create mode 100644 render/src/main/kotlin/com/linecorp/kotlinjdsl/render/template/Template.kt create mode 100644 render/src/main/kotlin/com/linecorp/kotlinjdsl/render/template/TemplateElement.kt create mode 100644 render/src/test/kotlin/com/linecorp/kotlinjdsl/render/template/TemplateTest.kt diff --git a/benchmark/build.gradle.kts b/benchmark/build.gradle.kts index 852ec6285..63aad6029 100644 --- a/benchmark/build.gradle.kts +++ b/benchmark/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.KotlinVersion + plugins { alias(exampleLibs.plugins.kotlin.benchmark) alias(exampleLibs.plugins.kotlin.allopen) @@ -13,6 +15,15 @@ dependencies { implementation(projects.jpqlRender) } +kotlin { + jvmToolchain(17) + + compilerOptions { + apiVersion = KotlinVersion.KOTLIN_1_9 + languageVersion = KotlinVersion.KOTLIN_1_9 + } +} + allOpen { annotation("org.openjdk.jmh.annotations.State") annotation("jakarta.persistence.Entity") diff --git a/docs/en/jpql-with-kotlin-jdsl/expressions.md b/docs/en/jpql-with-kotlin-jdsl/expressions.md index a9854f4b3..ec118d430 100644 --- a/docs/en/jpql-with-kotlin-jdsl/expressions.md +++ b/docs/en/jpql-with-kotlin-jdsl/expressions.md @@ -239,6 +239,11 @@ Call `function()` to create predefined database functions and user-defined datab function(String::class, "myFunction", path(Book::isbn)) ``` +{% hint style="info" %} +You may need to register information about the function you want to use with the JPA Provider. +For example, if you are using Hibernate, you need to register a `FunctionContributor`. +{% endhint %} + ## Cases Use `caseWhen()` and `caseValue()` to build cases. diff --git a/docs/en/jpql-with-kotlin-jdsl/predicates.md b/docs/en/jpql-with-kotlin-jdsl/predicates.md index 02be79427..a039d6088 100644 --- a/docs/en/jpql-with-kotlin-jdsl/predicates.md +++ b/docs/en/jpql-with-kotlin-jdsl/predicates.md @@ -158,3 +158,26 @@ path(Employee::departments).isEmpty() path(Employee::departments).isNotEmpty() ``` + +## Database function + +Call `function()` with `KClass` to create predefined database functions and user-defined database functions. + +```kotlin +function(Boolean::class, "myFunction", path(Book::isbn)) +``` + +{% hint style="info" %} +You may need to register information about the function you want to use with the JPA Provider. +For example, if you are using Hibernate, you need to register a `FunctionContributor`. +{% endhint %} + +## Custom predicate + +Call `customPredicate()` to build a custom predicate. + +```kotlin +customPredicate("{0} MEMBER OF {1}", value(author), path(Book::authors)) +``` + +If you frequently use `customPredicate()`, you can create [your own DSL](custom-dsl.md). diff --git a/docs/ko/jpql-with-kotlin-jdsl/expressions.md b/docs/ko/jpql-with-kotlin-jdsl/expressions.md index b927d3206..e77eadfec 100644 --- a/docs/ko/jpql-with-kotlin-jdsl/expressions.md +++ b/docs/ko/jpql-with-kotlin-jdsl/expressions.md @@ -237,6 +237,11 @@ DB 함수나 사용자 정의 함수를 만들기 위해, `function()`을 사용 function(String::class, "myFunction", path(Book::isbn)) ``` +{% hint style="info" %} +사용할 함수의 정보를 JPA 제공자에 등록할 필요가 있을 수 있습니다. +예를 들어 Hibernate를 사용하고 있다면 `FunctionContributor`를 반드시 등록해야 합니다. +{% endhint %} + ## Cases case를 만들기 위해, `caseWhen()`과 `caseValue()`를 사용할 수 있습니다. diff --git a/docs/ko/jpql-with-kotlin-jdsl/predicates.md b/docs/ko/jpql-with-kotlin-jdsl/predicates.md index da44c725c..d4af8c089 100644 --- a/docs/ko/jpql-with-kotlin-jdsl/predicates.md +++ b/docs/ko/jpql-with-kotlin-jdsl/predicates.md @@ -158,3 +158,26 @@ path(Employee::departments).isEmpty() path(Employee::departments).isNotEmpty() ``` + +## Database function + +DB 함수나 사용자 정의 함수를 만들기 위해, `KClass`과 함께 `function()`을 사용할 수 있습니다. + +```kotlin +function(Boolean::class, "myFunction", path(Book::isbn)) +``` + +{% hint style="info" %} +사용할 함수의 정보를 JPA 제공자에 등록할 필요가 있을 수 있습니다. +예를 들어 Hibernate를 사용하고 있다면 `FunctionContributor`를 반드시 등록해야 합니다. +{% endhint %} + +## Custom predicate + +커스텀 predicate를 만들기 위해, `customPredicate()`을 사용할 수 있습니다. + +```kotlin +customPredicate("{0} MEMBER OF {1}", value(author), path(Book::authors)) +``` + +만약 `customPredicate()`을 많이 사용한다면 [나만의 DSL](custom-dsl.md)을 만드는 것을 고려해보세요. diff --git a/dsl/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/dsl/jpql/Jpql.kt b/dsl/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/dsl/jpql/Jpql.kt index a5d05c994..6d4802fd1 100644 --- a/dsl/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/dsl/jpql/Jpql.kt +++ b/dsl/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/dsl/jpql/Jpql.kt @@ -1383,6 +1383,7 @@ open class Jpql : JpqlDsl { /** * Creates an expression that represents predefined database functions and user-defined database functions. */ + @LowPriorityInOverloadResolution @SinceJdsl("3.0.0") fun function(type: KClass, name: String, vararg args: Any): Expression { return Expressions.function(type, name, args.map { Expressions.value(it) }) @@ -1411,6 +1412,7 @@ open class Jpql : JpqlDsl { * customExpression(String::class, "CAST({0} AS VARCHAR)", 100) * ``` */ + @LowPriorityInOverloadResolution @SinceJdsl("3.0.0") fun customExpression(type: KClass, template: String, vararg args: Any): Expression { return Expressions.customExpression(type, template, args.map { Expressions.value(it) }) @@ -2705,6 +2707,66 @@ open class Jpql : JpqlDsl { return Predicates.notExists(subquery) } + /** + * Creates a predicate that represents predefined database functions and user-defined database functions. + */ + @Suppress("UNUSED_PARAMETER") + @LowPriorityInOverloadResolution + @SinceJdsl("3.0.0") + fun function(type: KClass, name: String, vararg args: Any): Predicate { + return Predicates.function(name, args.map { Expressions.value(it) }) + } + + /** + * Creates a predicate that represents predefined database functions and user-defined database functions. + */ + @Suppress("UNUSED_PARAMETER") + @SinceJdsl("3.0.0") + fun function(type: KClass, name: String, vararg args: Expressionable<*>): Predicate { + return Predicates.function(name, args.map { it.toExpression() }) + } + + /** + * Creates a predicate that represents the user-defined predicate. + * + * The template for the user-defined predicate can have placeholders. + * Placeholders in template are replaced with the expression in args, matching with index. + * + * ``` + * Placeholder: { ArgumentIndex } + * ``` + * + * Examples: + * ``` + * customPredicate("{0} MEMBER OF {1}", value(author), path(Book::authors)) + * ``` + */ + @LowPriorityInOverloadResolution + @SinceJdsl("3.3.0") + fun customPredicate(template: String, vararg args: Any): Predicate { + return Predicates.customPredicate(template, args.map { Expressions.value(it) }) + } + + /** + * Creates a predicate that represents the user-defined predicate. + * + * The template for the user-defined predicate can have placeholders. + * Placeholders in template are replaced with the expression in args, matching with index. + * + * ``` + * Placeholder: { ArgumentIndex } + * ``` + * + * Examples: + * ``` + * customPredicate("{0} MEMBER OF {1}", value(author), path(Book::authors)) + * ``` + */ + @SinceJdsl("3.3.0") + fun customPredicate(template: String, vararg args: Expressionable<*>): Predicate { + return Predicates.customPredicate(template, args.map { it.toExpression() }) + } + /** * Creates a sort that sorts the expression in ascending order. */ diff --git a/dsl/jpql/src/test/kotlin/com/linecorp/kotlinjdsl/dsl/jpql/predicate/CustomPredicateDslTest.kt b/dsl/jpql/src/test/kotlin/com/linecorp/kotlinjdsl/dsl/jpql/predicate/CustomPredicateDslTest.kt new file mode 100644 index 000000000..2800a89a8 --- /dev/null +++ b/dsl/jpql/src/test/kotlin/com/linecorp/kotlinjdsl/dsl/jpql/predicate/CustomPredicateDslTest.kt @@ -0,0 +1,60 @@ +package com.linecorp.kotlinjdsl.dsl.jpql.predicate + +import com.linecorp.kotlinjdsl.dsl.jpql.queryPart +import com.linecorp.kotlinjdsl.querymodel.jpql.expression.Expressions +import com.linecorp.kotlinjdsl.querymodel.jpql.predicate.Predicate +import com.linecorp.kotlinjdsl.querymodel.jpql.predicate.Predicates +import org.assertj.core.api.WithAssertions +import org.junit.jupiter.api.Test + +class CustomPredicateDslTest : WithAssertions { + private val template1: String = "template1" + + private val stringExpression1 = Expressions.value("string1") + private val stringExpression2 = Expressions.value("string2") + + private val string1 = "string1" + private val string2 = "string2" + + @Test + fun `customPredicate() with strings`() { + // when + val predicate = queryPart { + customPredicate(template1, string1, string2) + } + + val actual: Predicate = predicate // for type check + + // then + val expected = Predicates.customPredicate( + template1, + listOf( + Expressions.value(string1), + Expressions.value(string2), + ), + ) + + assertThat(actual).isEqualTo(expected) + } + + @Test + fun `customPredicate() with string expressions`() { + // when + val predicate = queryPart { + customPredicate(template1, stringExpression1, stringExpression2) + } + + val actual: Predicate = predicate // for type check + + // then + val expected = Predicates.customPredicate( + template1, + listOf( + stringExpression1, + stringExpression2, + ), + ) + + assertThat(actual).isEqualTo(expected) + } +} diff --git a/dsl/jpql/src/test/kotlin/com/linecorp/kotlinjdsl/dsl/jpql/predicate/FunctionDslTest.kt b/dsl/jpql/src/test/kotlin/com/linecorp/kotlinjdsl/dsl/jpql/predicate/FunctionDslTest.kt new file mode 100644 index 000000000..9a6f29188 --- /dev/null +++ b/dsl/jpql/src/test/kotlin/com/linecorp/kotlinjdsl/dsl/jpql/predicate/FunctionDslTest.kt @@ -0,0 +1,60 @@ +package com.linecorp.kotlinjdsl.dsl.jpql.predicate + +import com.linecorp.kotlinjdsl.dsl.jpql.queryPart +import com.linecorp.kotlinjdsl.querymodel.jpql.expression.Expressions +import com.linecorp.kotlinjdsl.querymodel.jpql.predicate.Predicate +import com.linecorp.kotlinjdsl.querymodel.jpql.predicate.Predicates +import org.assertj.core.api.WithAssertions +import org.junit.jupiter.api.Test + +class FunctionDslTest : WithAssertions { + private val name1: String = "name1" + + private val stringExpression1 = Expressions.value("string1") + private val stringExpression2 = Expressions.value("string2") + + private val string1 = "string1" + private val string2 = "string2" + + @Test + fun `function() with strings`() { + // when + val predicate = queryPart { + function(Boolean::class, name1, string1, string2) + } + + val actual: Predicate = predicate // for type check + + // then + val expected = Predicates.function( + name1, + listOf( + Expressions.value(string1), + Expressions.value(string2), + ), + ) + + assertThat(actual).isEqualTo(expected) + } + + @Test + fun `function() with string expressions`() { + // when + val predicate = queryPart { + function(Boolean::class, name1, stringExpression1, stringExpression2) + } + + val actual: Predicate = predicate // for type check + + // then + val expected = Predicates.function( + name1, + listOf( + stringExpression1, + stringExpression2, + ), + ) + + assertThat(actual).isEqualTo(expected) + } +} diff --git a/example/hibernate-reactive/src/main/kotlin/com/linecorp/kotlinjdsl/example/hibernate/reactive/jakarta/jpql/configuration/H2DBConnectionPoolConfiguration.kt b/example/hibernate-reactive/src/main/kotlin/com/linecorp/kotlinjdsl/example/hibernate/reactive/jakarta/jpql/configuration/H2DBConnectionPoolConfiguration.kt index 3891deac4..db137e57b 100644 --- a/example/hibernate-reactive/src/main/kotlin/com/linecorp/kotlinjdsl/example/hibernate/reactive/jakarta/jpql/configuration/H2DBConnectionPoolConfiguration.kt +++ b/example/hibernate-reactive/src/main/kotlin/com/linecorp/kotlinjdsl/example/hibernate/reactive/jakarta/jpql/configuration/H2DBConnectionPoolConfiguration.kt @@ -11,8 +11,8 @@ class H2DBConnectionPoolConfiguration : DefaultSqlClientPoolConfiguration() { private lateinit var password: String override fun configure(configuration: MutableMap) { - user = ConfigurationHelper.getString(Settings.USER, configuration) - password = ConfigurationHelper.getString(Settings.PASS, configuration) + user = ConfigurationHelper.getString(Settings.JAKARTA_JDBC_USER, configuration) + password = ConfigurationHelper.getString(Settings.JAKARTA_JDBC_PASSWORD, configuration) super.configure(configuration) } diff --git a/query-model/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/querymodel/jpql/expression/Expressions.kt b/query-model/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/querymodel/jpql/expression/Expressions.kt index 855e04a5e..25f378881 100644 --- a/query-model/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/querymodel/jpql/expression/Expressions.kt +++ b/query-model/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/querymodel/jpql/expression/Expressions.kt @@ -14,7 +14,7 @@ import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlDivide import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlEntityType import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlExpression import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlExpressionParentheses -import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlFunction +import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlFunctionExpression import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlLength import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlLiteral import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlLocate @@ -548,7 +548,7 @@ object Expressions { */ @SinceJdsl("3.0.0") fun function(type: KClass, name: String, args: Iterable>): Expression { - return JpqlFunction(type, name, args) + return JpqlFunctionExpression(type, name, args) } /** @@ -563,7 +563,7 @@ object Expressions { * * Examples: * ``` - * customExpression(String::class, "CAST({0} AS VARCHAR)", 100) + * Expressions.customExpression(String::class, "CAST({0} AS VARCHAR)", listOf(Paths.path(User::age))) * ``` */ @SinceJdsl("3.0.0") diff --git a/query-model/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/querymodel/jpql/expression/impl/JpqlFunction.kt b/query-model/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/querymodel/jpql/expression/impl/JpqlFunctionExpression.kt similarity index 83% rename from query-model/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/querymodel/jpql/expression/impl/JpqlFunction.kt rename to query-model/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/querymodel/jpql/expression/impl/JpqlFunctionExpression.kt index 7df5f2237..d5dcfa59f 100644 --- a/query-model/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/querymodel/jpql/expression/impl/JpqlFunction.kt +++ b/query-model/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/querymodel/jpql/expression/impl/JpqlFunctionExpression.kt @@ -5,7 +5,7 @@ import com.linecorp.kotlinjdsl.querymodel.jpql.expression.Expression import kotlin.reflect.KClass @Internal -data class JpqlFunction internal constructor( +data class JpqlFunctionExpression internal constructor( val type: KClass, val name: String, val args: Iterable>, diff --git a/query-model/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/querymodel/jpql/predicate/Predicates.kt b/query-model/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/querymodel/jpql/predicate/Predicates.kt index 7bc2e55fd..2fea681e3 100644 --- a/query-model/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/querymodel/jpql/predicate/Predicates.kt +++ b/query-model/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/querymodel/jpql/predicate/Predicates.kt @@ -7,10 +7,12 @@ import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlNull import com.linecorp.kotlinjdsl.querymodel.jpql.path.Path import com.linecorp.kotlinjdsl.querymodel.jpql.predicate.impl.JpqlAnd import com.linecorp.kotlinjdsl.querymodel.jpql.predicate.impl.JpqlBetween +import com.linecorp.kotlinjdsl.querymodel.jpql.predicate.impl.JpqlCustomPredicate import com.linecorp.kotlinjdsl.querymodel.jpql.predicate.impl.JpqlEqual import com.linecorp.kotlinjdsl.querymodel.jpql.predicate.impl.JpqlEqualAll import com.linecorp.kotlinjdsl.querymodel.jpql.predicate.impl.JpqlEqualAny import com.linecorp.kotlinjdsl.querymodel.jpql.predicate.impl.JpqlExists +import com.linecorp.kotlinjdsl.querymodel.jpql.predicate.impl.JpqlFunctionPredicate import com.linecorp.kotlinjdsl.querymodel.jpql.predicate.impl.JpqlGreaterThan import com.linecorp.kotlinjdsl.querymodel.jpql.predicate.impl.JpqlGreaterThanAll import com.linecorp.kotlinjdsl.querymodel.jpql.predicate.impl.JpqlGreaterThanAny @@ -335,6 +337,37 @@ object Predicates { return JpqlNotExists(subquery) } + /** + * Creates a predicate that represents predefined database functions and user-defined database functions. + */ + @SinceJdsl("3.0.0") + fun function(name: String, args: Iterable>): Predicate { + return JpqlFunctionPredicate(name, args) + } + + /** + * Creates a predicate that represents the user-defined predicate. + * + * The template for the user-defined predicate can have placeholders. + * Placeholders in template are replaced with the expression in args, matching with index. + * + * ``` + * Placeholder: { ArgumentIndex } + * ``` + * + * Examples: + * ``` + * Predicates.customPredicate("{0} MEMBER OF {1}", Expressions.value(author), Paths.path(Book::authors)) + * ``` + */ + @SinceJdsl("3.3.0") + fun customPredicate( + template: String, + args: Iterable>, + ): Predicate { + return JpqlCustomPredicate(template, args) + } + /** * Creates a predicate that is enclosed in parentheses. */ diff --git a/query-model/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/querymodel/jpql/predicate/impl/JpqlCustomPredicate.kt b/query-model/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/querymodel/jpql/predicate/impl/JpqlCustomPredicate.kt new file mode 100644 index 000000000..8472b5daa --- /dev/null +++ b/query-model/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/querymodel/jpql/predicate/impl/JpqlCustomPredicate.kt @@ -0,0 +1,11 @@ +package com.linecorp.kotlinjdsl.querymodel.jpql.predicate.impl + +import com.linecorp.kotlinjdsl.Internal +import com.linecorp.kotlinjdsl.querymodel.jpql.expression.Expression +import com.linecorp.kotlinjdsl.querymodel.jpql.predicate.Predicate + +@Internal +data class JpqlCustomPredicate( + val template: String, + val args: Iterable>, +) : Predicate diff --git a/query-model/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/querymodel/jpql/predicate/impl/JpqlFunctionPredicate.kt b/query-model/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/querymodel/jpql/predicate/impl/JpqlFunctionPredicate.kt new file mode 100644 index 000000000..4b0f417ba --- /dev/null +++ b/query-model/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/querymodel/jpql/predicate/impl/JpqlFunctionPredicate.kt @@ -0,0 +1,11 @@ +package com.linecorp.kotlinjdsl.querymodel.jpql.predicate.impl + +import com.linecorp.kotlinjdsl.Internal +import com.linecorp.kotlinjdsl.querymodel.jpql.expression.Expression +import com.linecorp.kotlinjdsl.querymodel.jpql.predicate.Predicate + +@Internal +data class JpqlFunctionPredicate internal constructor( + val name: String, + val args: Iterable>, +) : Predicate diff --git a/query-model/jpql/src/test/kotlin/com/linecorp/kotlinjdsl/querymodel/jpql/expression/ExpressionsTest.kt b/query-model/jpql/src/test/kotlin/com/linecorp/kotlinjdsl/querymodel/jpql/expression/ExpressionsTest.kt index e1d02e005..fae31a8ba 100644 --- a/query-model/jpql/src/test/kotlin/com/linecorp/kotlinjdsl/querymodel/jpql/expression/ExpressionsTest.kt +++ b/query-model/jpql/src/test/kotlin/com/linecorp/kotlinjdsl/querymodel/jpql/expression/ExpressionsTest.kt @@ -16,7 +16,7 @@ import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlDivide import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlEntityType import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlExpression import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlExpressionParentheses -import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlFunction +import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlFunctionExpression import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlLength import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlLiteral import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlLocate @@ -784,7 +784,7 @@ class ExpressionsTest : WithAssertions { ) // then - val expected = JpqlFunction( + val expected = JpqlFunctionExpression( type = Class1::class, name = name1, args = listOf(intExpression1, intExpression2), diff --git a/query-model/jpql/src/test/kotlin/com/linecorp/kotlinjdsl/querymodel/jpql/predicate/PredicatesTest.kt b/query-model/jpql/src/test/kotlin/com/linecorp/kotlinjdsl/querymodel/jpql/predicate/PredicatesTest.kt index f5d369221..ff7b18f7d 100644 --- a/query-model/jpql/src/test/kotlin/com/linecorp/kotlinjdsl/querymodel/jpql/predicate/PredicatesTest.kt +++ b/query-model/jpql/src/test/kotlin/com/linecorp/kotlinjdsl/querymodel/jpql/predicate/PredicatesTest.kt @@ -7,10 +7,12 @@ import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlNull import com.linecorp.kotlinjdsl.querymodel.jpql.path.Paths import com.linecorp.kotlinjdsl.querymodel.jpql.predicate.impl.JpqlAnd import com.linecorp.kotlinjdsl.querymodel.jpql.predicate.impl.JpqlBetween +import com.linecorp.kotlinjdsl.querymodel.jpql.predicate.impl.JpqlCustomPredicate import com.linecorp.kotlinjdsl.querymodel.jpql.predicate.impl.JpqlEqual import com.linecorp.kotlinjdsl.querymodel.jpql.predicate.impl.JpqlEqualAll import com.linecorp.kotlinjdsl.querymodel.jpql.predicate.impl.JpqlEqualAny import com.linecorp.kotlinjdsl.querymodel.jpql.predicate.impl.JpqlExists +import com.linecorp.kotlinjdsl.querymodel.jpql.predicate.impl.JpqlFunctionPredicate import com.linecorp.kotlinjdsl.querymodel.jpql.predicate.impl.JpqlGreaterThan import com.linecorp.kotlinjdsl.querymodel.jpql.predicate.impl.JpqlGreaterThanAll import com.linecorp.kotlinjdsl.querymodel.jpql.predicate.impl.JpqlGreaterThanAny @@ -47,6 +49,9 @@ import org.junit.jupiter.api.Test import java.math.BigDecimal class PredicatesTest : WithAssertions { + private val name1 = "name1" + private val template1 = "template1" + private val stringExpression1 = Expressions.value("string1") private val stringExpression2 = Expressions.value("string2") @@ -718,6 +723,40 @@ class PredicatesTest : WithAssertions { assertThat(actual).isEqualTo(expected) } + @Test + fun function() { + // when + val actual = Predicates.function( + name = name1, + args = listOf(stringExpression1, stringExpression2), + ) + + // then + val expected = JpqlFunctionPredicate( + name = name1, + args = listOf(stringExpression1, stringExpression2), + ) + + assertThat(actual).isEqualTo(expected) + } + + @Test + fun customPredicate() { + // when + val actual = Predicates.customPredicate( + template = template1, + args = listOf(stringExpression1, stringExpression2), + ) + + // then + val expected = JpqlCustomPredicate( + template = template1, + args = listOf(stringExpression1, stringExpression2), + ) + + assertThat(actual).isEqualTo(expected) + } + @Test fun parentheses() { // when diff --git a/render/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/render/jpql/JpqlRenderContext.kt b/render/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/render/jpql/JpqlRenderContext.kt index 3ca03695c..ed5e4e135 100644 --- a/render/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/render/jpql/JpqlRenderContext.kt +++ b/render/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/render/jpql/JpqlRenderContext.kt @@ -23,6 +23,7 @@ import com.linecorp.kotlinjdsl.render.jpql.serializer.impl.JpqlCoalesceSerialize import com.linecorp.kotlinjdsl.render.jpql.serializer.impl.JpqlConcatSerializer import com.linecorp.kotlinjdsl.render.jpql.serializer.impl.JpqlCountSerializer import com.linecorp.kotlinjdsl.render.jpql.serializer.impl.JpqlCustomExpressionSerializer +import com.linecorp.kotlinjdsl.render.jpql.serializer.impl.JpqlCustomPredicateSerializer import com.linecorp.kotlinjdsl.render.jpql.serializer.impl.JpqlDeleteQuerySerializer import com.linecorp.kotlinjdsl.render.jpql.serializer.impl.JpqlDerivedEntitySerializer import com.linecorp.kotlinjdsl.render.jpql.serializer.impl.JpqlDivideSerializer @@ -36,7 +37,8 @@ import com.linecorp.kotlinjdsl.render.jpql.serializer.impl.JpqlEqualSerializer import com.linecorp.kotlinjdsl.render.jpql.serializer.impl.JpqlExistsSerializer import com.linecorp.kotlinjdsl.render.jpql.serializer.impl.JpqlExpressionParenthesesSerializer import com.linecorp.kotlinjdsl.render.jpql.serializer.impl.JpqlExpressionSerializer -import com.linecorp.kotlinjdsl.render.jpql.serializer.impl.JpqlFunctionSerializer +import com.linecorp.kotlinjdsl.render.jpql.serializer.impl.JpqlFunctionExpressionSerializer +import com.linecorp.kotlinjdsl.render.jpql.serializer.impl.JpqlFunctionPredicateSerializer import com.linecorp.kotlinjdsl.render.jpql.serializer.impl.JpqlGreaterThanAllSerializer import com.linecorp.kotlinjdsl.render.jpql.serializer.impl.JpqlGreaterThanAnySerializer import com.linecorp.kotlinjdsl.render.jpql.serializer.impl.JpqlGreaterThanOrEqualToAllSerializer @@ -266,6 +268,7 @@ private class DefaultModule : JpqlRenderModule { JpqlConcatSerializer(), JpqlCountSerializer(), JpqlCustomExpressionSerializer(), + JpqlCustomPredicateSerializer(), JpqlDeleteQuerySerializer(), JpqlDerivedEntitySerializer(), JpqlDivideSerializer(), @@ -279,7 +282,8 @@ private class DefaultModule : JpqlRenderModule { JpqlExistsSerializer(), JpqlExpressionParenthesesSerializer(), JpqlExpressionSerializer(), - JpqlFunctionSerializer(), + JpqlFunctionExpressionSerializer(), + JpqlFunctionPredicateSerializer(), JpqlGreaterThanAllSerializer(), JpqlGreaterThanAnySerializer(), JpqlGreaterThanOrEqualToAllSerializer(), diff --git a/render/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/impl/JpqlCustomExpressionSerializer.kt b/render/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/impl/JpqlCustomExpressionSerializer.kt index c71190cf1..69134ecfd 100644 --- a/render/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/impl/JpqlCustomExpressionSerializer.kt +++ b/render/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/impl/JpqlCustomExpressionSerializer.kt @@ -3,70 +3,18 @@ package com.linecorp.kotlinjdsl.render.jpql.serializer.impl import com.linecorp.kotlinjdsl.Internal import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlCustomExpression import com.linecorp.kotlinjdsl.render.RenderContext -import com.linecorp.kotlinjdsl.render.jpql.serializer.JpqlRenderSerializer import com.linecorp.kotlinjdsl.render.jpql.serializer.JpqlSerializer +import com.linecorp.kotlinjdsl.render.jpql.serializer.support.JpqlTemplateSerializerSupport import com.linecorp.kotlinjdsl.render.jpql.writer.JpqlWriter -import java.util.concurrent.ConcurrentHashMap import kotlin.reflect.KClass @Internal -class JpqlCustomExpressionSerializer : JpqlSerializer> { - private val cache: MutableMap = ConcurrentHashMap() - +class JpqlCustomExpressionSerializer : JpqlTemplateSerializerSupport(), JpqlSerializer> { override fun handledType(): KClass> { return JpqlCustomExpression::class } override fun serialize(part: JpqlCustomExpression<*>, writer: JpqlWriter, context: RenderContext) { - val delegate = context.getValue(JpqlRenderSerializer) - - val template = cache.computeIfAbsent(part.template) { JpqlCustomExpressionTemplate.compile(part.template) } - val args = part.args.toList() - - template.strings.forEachIndexed { index, string -> - if (index > 0) { - val argumentNumber = template.argumentNumbers[index - 1] - - delegate.serialize(args[argumentNumber], writer, context) - } - - writer.write(string) - } - } -} - -private class JpqlCustomExpressionTemplate( - val strings: List, - val argumentNumbers: List, -) { - companion object { - fun compile(template: String): JpqlCustomExpressionTemplate { - val strings = mutableListOf() - val argumentNumbers = mutableListOf() - - var match: MatchResult? = elementRegex.find(template) - ?: return JpqlCustomExpressionTemplate(listOf(template), emptyList()) - - var lastStart = 0 - val length = template.length - - do { - val foundMatch = match!! - - strings.add(template.substring(lastStart, foundMatch.range.first)) - argumentNumbers.add(foundMatch.value.let { it.substring(1, it.length - 1) }.toInt()) - - lastStart = foundMatch.range.last + 1 - match = foundMatch.next() - } while (lastStart < length && match != null) - - if (lastStart < length) { - strings.add(template.substring(lastStart, length)) - } - - return JpqlCustomExpressionTemplate(strings, argumentNumbers) - } - - private val elementRegex = Regex("\\{(\\d+)}") + serialize(part.template, part.args, writer, context) } } diff --git a/render/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/impl/JpqlCustomPredicateSerializer.kt b/render/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/impl/JpqlCustomPredicateSerializer.kt new file mode 100644 index 000000000..7056f4b15 --- /dev/null +++ b/render/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/impl/JpqlCustomPredicateSerializer.kt @@ -0,0 +1,20 @@ +package com.linecorp.kotlinjdsl.render.jpql.serializer.impl + +import com.linecorp.kotlinjdsl.Internal +import com.linecorp.kotlinjdsl.querymodel.jpql.predicate.impl.JpqlCustomPredicate +import com.linecorp.kotlinjdsl.render.RenderContext +import com.linecorp.kotlinjdsl.render.jpql.serializer.JpqlSerializer +import com.linecorp.kotlinjdsl.render.jpql.serializer.support.JpqlTemplateSerializerSupport +import com.linecorp.kotlinjdsl.render.jpql.writer.JpqlWriter +import kotlin.reflect.KClass + +@Internal +class JpqlCustomPredicateSerializer : JpqlTemplateSerializerSupport(), JpqlSerializer { + override fun handledType(): KClass { + return JpqlCustomPredicate::class + } + + override fun serialize(part: JpqlCustomPredicate, writer: JpqlWriter, context: RenderContext) { + serialize(part.template, part.args, writer, context) + } +} diff --git a/render/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/impl/JpqlFunctionExpressionSerializer.kt b/render/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/impl/JpqlFunctionExpressionSerializer.kt new file mode 100644 index 000000000..4da07596e --- /dev/null +++ b/render/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/impl/JpqlFunctionExpressionSerializer.kt @@ -0,0 +1,20 @@ +package com.linecorp.kotlinjdsl.render.jpql.serializer.impl + +import com.linecorp.kotlinjdsl.Internal +import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlFunctionExpression +import com.linecorp.kotlinjdsl.render.RenderContext +import com.linecorp.kotlinjdsl.render.jpql.serializer.JpqlSerializer +import com.linecorp.kotlinjdsl.render.jpql.serializer.support.JpqlFunctionSerializerSupport +import com.linecorp.kotlinjdsl.render.jpql.writer.JpqlWriter +import kotlin.reflect.KClass + +@Internal +class JpqlFunctionExpressionSerializer : JpqlFunctionSerializerSupport(), JpqlSerializer> { + override fun handledType(): KClass> { + return JpqlFunctionExpression::class + } + + override fun serialize(part: JpqlFunctionExpression<*>, writer: JpqlWriter, context: RenderContext) { + serialize(part.name, part.args, writer, context) + } +} diff --git a/render/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/impl/JpqlFunctionPredicateSerializer.kt b/render/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/impl/JpqlFunctionPredicateSerializer.kt new file mode 100644 index 000000000..b9793bb86 --- /dev/null +++ b/render/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/impl/JpqlFunctionPredicateSerializer.kt @@ -0,0 +1,20 @@ +package com.linecorp.kotlinjdsl.render.jpql.serializer.impl + +import com.linecorp.kotlinjdsl.Internal +import com.linecorp.kotlinjdsl.querymodel.jpql.predicate.impl.JpqlFunctionPredicate +import com.linecorp.kotlinjdsl.render.RenderContext +import com.linecorp.kotlinjdsl.render.jpql.serializer.JpqlSerializer +import com.linecorp.kotlinjdsl.render.jpql.serializer.support.JpqlFunctionSerializerSupport +import com.linecorp.kotlinjdsl.render.jpql.writer.JpqlWriter +import kotlin.reflect.KClass + +@Internal +class JpqlFunctionPredicateSerializer : JpqlFunctionSerializerSupport(), JpqlSerializer { + override fun handledType(): KClass { + return JpqlFunctionPredicate::class + } + + override fun serialize(part: JpqlFunctionPredicate, writer: JpqlWriter, context: RenderContext) { + serialize(part.name, part.args, writer, context) + } +} diff --git a/render/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/impl/JpqlFunctionSerializer.kt b/render/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/support/JpqlFunctionSerializerSupport.kt similarity index 50% rename from render/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/impl/JpqlFunctionSerializer.kt rename to render/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/support/JpqlFunctionSerializerSupport.kt index c7d7d7a62..e6aadc301 100644 --- a/render/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/impl/JpqlFunctionSerializer.kt +++ b/render/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/support/JpqlFunctionSerializerSupport.kt @@ -1,35 +1,29 @@ -package com.linecorp.kotlinjdsl.render.jpql.serializer.impl +package com.linecorp.kotlinjdsl.render.jpql.serializer.support import com.linecorp.kotlinjdsl.Internal import com.linecorp.kotlinjdsl.iterable.IterableUtils -import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlFunction +import com.linecorp.kotlinjdsl.querymodel.jpql.expression.Expression import com.linecorp.kotlinjdsl.render.RenderContext import com.linecorp.kotlinjdsl.render.jpql.serializer.JpqlRenderSerializer -import com.linecorp.kotlinjdsl.render.jpql.serializer.JpqlSerializer import com.linecorp.kotlinjdsl.render.jpql.writer.JpqlWriter -import kotlin.reflect.KClass @Internal -class JpqlFunctionSerializer : JpqlSerializer> { - override fun handledType(): KClass> { - return JpqlFunction::class - } - - override fun serialize(part: JpqlFunction<*>, writer: JpqlWriter, context: RenderContext) { +open class JpqlFunctionSerializerSupport { + protected fun serialize(name: String, args: Iterable>, writer: JpqlWriter, context: RenderContext) { val delegate = context.getValue(JpqlRenderSerializer) writer.write("FUNCTION") writer.writeParentheses { writer.write("'") - writer.write(part.name) + writer.write(name) writer.write("'") - if (IterableUtils.isNotEmpty(part.args)) { + if (IterableUtils.isNotEmpty(args)) { writer.write(",") writer.write(" ") - writer.writeEach(part.args, separator = ", ") { + writer.writeEach(args, separator = ", ") { delegate.serialize(it, writer, context) } } diff --git a/render/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/support/JpqlTemplateSerializerSupport.kt b/render/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/support/JpqlTemplateSerializerSupport.kt new file mode 100644 index 000000000..b0c68f315 --- /dev/null +++ b/render/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/support/JpqlTemplateSerializerSupport.kt @@ -0,0 +1,41 @@ +package com.linecorp.kotlinjdsl.render.jpql.serializer.support + +import com.linecorp.kotlinjdsl.Internal +import com.linecorp.kotlinjdsl.querymodel.jpql.expression.Expression +import com.linecorp.kotlinjdsl.render.RenderContext +import com.linecorp.kotlinjdsl.render.jpql.serializer.JpqlRenderSerializer +import com.linecorp.kotlinjdsl.render.jpql.writer.JpqlWriter +import com.linecorp.kotlinjdsl.render.template.Template +import com.linecorp.kotlinjdsl.render.template.TemplateElement +import java.util.concurrent.ConcurrentHashMap + +@Internal +open class JpqlTemplateSerializerSupport { + companion object { + private val cache: MutableMap = ConcurrentHashMap() + } + + protected fun serialize( + template: String, + args: Iterable>, + writer: JpqlWriter, + context: RenderContext, + ) { + val delegate = context.getValue(JpqlRenderSerializer) + + val compiledTemplate = cache.computeIfAbsent(template) { Template.compile(template) } + val orderedArgs = args.toList() + + compiledTemplate.elements.forEach { + when (it) { + is TemplateElement.String -> { + writer.write(it.value) + } + + is TemplateElement.ArgumentNumber -> { + delegate.serialize(orderedArgs[it.value], writer, context) + } + } + } + } +} diff --git a/render/jpql/src/test/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/impl/JpqlCustomExpressionSerializerTest.kt b/render/jpql/src/test/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/impl/JpqlCustomExpressionSerializerTest.kt index f258b9426..ebd215284 100644 --- a/render/jpql/src/test/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/impl/JpqlCustomExpressionSerializerTest.kt +++ b/render/jpql/src/test/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/impl/JpqlCustomExpressionSerializerTest.kt @@ -104,4 +104,27 @@ class JpqlCustomExpressionSerializerTest : WithAssertions { writer.write("TEST()") } } + + @Test + fun `serialize() draws well, even if there are no parentheses`() { + // given + val part = Expressions.customExpression( + String::class, + "{0} = {0}", + listOf( + expression1, + ), + ) + val context = TestRenderContext(serializer) + + // when + sut.serialize(part as JpqlCustomExpression<*>, writer, context) + + // then + verifySequence { + serializer.serialize(expression1, writer, context) + writer.write(" = ") + serializer.serialize(expression1, writer, context) + } + } } diff --git a/render/jpql/src/test/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/impl/JpqlCustomPredicateSerializerTest.kt b/render/jpql/src/test/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/impl/JpqlCustomPredicateSerializerTest.kt new file mode 100644 index 000000000..f80e2f301 --- /dev/null +++ b/render/jpql/src/test/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/impl/JpqlCustomPredicateSerializerTest.kt @@ -0,0 +1,126 @@ +package com.linecorp.kotlinjdsl.render.jpql.serializer.impl + +import com.linecorp.kotlinjdsl.querymodel.jpql.path.Paths +import com.linecorp.kotlinjdsl.querymodel.jpql.predicate.Predicates +import com.linecorp.kotlinjdsl.querymodel.jpql.predicate.impl.JpqlCustomPredicate +import com.linecorp.kotlinjdsl.render.TestRenderContext +import com.linecorp.kotlinjdsl.render.jpql.entity.book.Book +import com.linecorp.kotlinjdsl.render.jpql.serializer.JpqlRenderSerializer +import com.linecorp.kotlinjdsl.render.jpql.serializer.JpqlSerializerTest +import com.linecorp.kotlinjdsl.render.jpql.writer.JpqlWriter +import io.mockk.impl.annotations.MockK +import io.mockk.verifySequence +import org.assertj.core.api.WithAssertions +import org.junit.jupiter.api.Test + +@JpqlSerializerTest +class JpqlCustomPredicateSerializerTest : WithAssertions { + private val sut = JpqlCustomPredicateSerializer() + + @MockK + private lateinit var writer: JpqlWriter + + @MockK + private lateinit var serializer: JpqlRenderSerializer + + private val expression1 = Paths.path(Book::price) + private val expression2 = Paths.path(Book::salePrice) + + @Test + fun handledType() { + // when + val actual = sut.handledType() + + // then + assertThat(actual).isEqualTo(JpqlCustomPredicate::class) + } + + @Test + fun serialize() { + // given + val part = Predicates.customPredicate( + "TEST({0}, {1})", + listOf( + expression1, + expression2, + ), + ) + val context = TestRenderContext(serializer) + + // when + sut.serialize(part as JpqlCustomPredicate, writer, context) + + // then + verifySequence { + writer.write("TEST(") + serializer.serialize(expression1, writer, context) + writer.write(", ") + serializer.serialize(expression2, writer, context) + writer.write(")") + } + } + + @Test + fun `serialize() draws the same expression, when the argument number is the same`() { + // given + val part = Predicates.customPredicate( + "TEST({0}, {0})", + listOf( + expression1, + ), + ) + val context = TestRenderContext(serializer) + + // when + sut.serialize(part as JpqlCustomPredicate, writer, context) + + // then + verifySequence { + writer.write("TEST(") + serializer.serialize(expression1, writer, context) + writer.write(", ") + serializer.serialize(expression1, writer, context) + writer.write(")") + } + } + + @Test + fun `serialize() draws only the template, when there are no the argument numbers`() { + // given + val part = Predicates.customPredicate( + "TEST()", + emptyList(), + ) + val context = TestRenderContext(serializer) + + // when + sut.serialize(part as JpqlCustomPredicate, writer, context) + + // then + verifySequence { + writer.write("TEST()") + } + } + + @Test + fun `serialize() draws well, even if there are no parentheses`() { + // given + val part = Predicates.customPredicate( + "{0} = {0}", + listOf( + expression1, + ), + ) + val context = TestRenderContext(serializer) + + // when + sut.serialize(part as JpqlCustomPredicate, writer, context) + + // then + verifySequence { + serializer.serialize(expression1, writer, context) + writer.write(" = ") + serializer.serialize(expression1, writer, context) + } + } +} diff --git a/render/jpql/src/test/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/impl/JpqlFunctionSerializerTest.kt b/render/jpql/src/test/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/impl/JpqlFunctionExpressionSerializerTest.kt similarity index 87% rename from render/jpql/src/test/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/impl/JpqlFunctionSerializerTest.kt rename to render/jpql/src/test/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/impl/JpqlFunctionExpressionSerializerTest.kt index aa5f4a65e..a225447e1 100644 --- a/render/jpql/src/test/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/impl/JpqlFunctionSerializerTest.kt +++ b/render/jpql/src/test/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/impl/JpqlFunctionExpressionSerializerTest.kt @@ -1,7 +1,7 @@ package com.linecorp.kotlinjdsl.render.jpql.serializer.impl import com.linecorp.kotlinjdsl.querymodel.jpql.expression.Expressions -import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlFunction +import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlFunctionExpression import com.linecorp.kotlinjdsl.render.TestRenderContext import com.linecorp.kotlinjdsl.render.jpql.serializer.JpqlRenderSerializer import com.linecorp.kotlinjdsl.render.jpql.serializer.JpqlSerializerTest @@ -12,8 +12,8 @@ import org.assertj.core.api.WithAssertions import org.junit.jupiter.api.Test @JpqlSerializerTest -class JpqlFunctionSerializerTest : WithAssertions { - private val sut = JpqlFunctionSerializer() +class JpqlFunctionExpressionSerializerTest : WithAssertions { + private val sut = JpqlFunctionExpressionSerializer() @MockK private lateinit var writer: JpqlWriter @@ -33,7 +33,7 @@ class JpqlFunctionSerializerTest : WithAssertions { val actual = sut.handledType() // then - assertThat(actual).isEqualTo(JpqlFunction::class) + assertThat(actual).isEqualTo(JpqlFunctionExpression::class) } @Test @@ -53,7 +53,7 @@ class JpqlFunctionSerializerTest : WithAssertions { val context = TestRenderContext(serializer) // when - sut.serialize(part as JpqlFunction<*>, writer, context) + sut.serialize(part as JpqlFunctionExpression<*>, writer, context) // then verifySequence { @@ -82,7 +82,7 @@ class JpqlFunctionSerializerTest : WithAssertions { val context = TestRenderContext(serializer) // when - sut.serialize(part as JpqlFunction<*>, writer, context) + sut.serialize(part as JpqlFunctionExpression<*>, writer, context) // then verifySequence { diff --git a/render/jpql/src/test/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/impl/JpqlFunctionPredicateSerializerTest.kt b/render/jpql/src/test/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/impl/JpqlFunctionPredicateSerializerTest.kt new file mode 100644 index 000000000..612152028 --- /dev/null +++ b/render/jpql/src/test/kotlin/com/linecorp/kotlinjdsl/render/jpql/serializer/impl/JpqlFunctionPredicateSerializerTest.kt @@ -0,0 +1,95 @@ +package com.linecorp.kotlinjdsl.render.jpql.serializer.impl + +import com.linecorp.kotlinjdsl.querymodel.jpql.expression.Expressions +import com.linecorp.kotlinjdsl.querymodel.jpql.predicate.Predicates +import com.linecorp.kotlinjdsl.querymodel.jpql.predicate.impl.JpqlFunctionPredicate +import com.linecorp.kotlinjdsl.render.TestRenderContext +import com.linecorp.kotlinjdsl.render.jpql.serializer.JpqlRenderSerializer +import com.linecorp.kotlinjdsl.render.jpql.serializer.JpqlSerializerTest +import com.linecorp.kotlinjdsl.render.jpql.writer.JpqlWriter +import io.mockk.impl.annotations.MockK +import io.mockk.verifySequence +import org.assertj.core.api.WithAssertions +import org.junit.jupiter.api.Test + +@JpqlSerializerTest +class JpqlFunctionPredicateSerializerTest : WithAssertions { + private val sut = JpqlFunctionPredicateSerializer() + + @MockK + private lateinit var writer: JpqlWriter + + @MockK + private lateinit var serializer: JpqlRenderSerializer + + private val functionName1 = "functionName1" + + private val expression1 = Expressions.value(1) + private val expression2 = Expressions.value(2) + private val expression3 = Expressions.value(3) + + @Test + fun handledType() { + // when + val actual = sut.handledType() + + // then + assertThat(actual).isEqualTo(JpqlFunctionPredicate::class) + } + + @Test + fun serialize() { + // given + val expressions = listOf( + expression1, + expression2, + expression3, + ) + + val part = Predicates.function( + functionName1, + expressions, + ) + val context = TestRenderContext(serializer) + + // when + sut.serialize(part as JpqlFunctionPredicate, writer, context) + + // then + verifySequence { + writer.write("FUNCTION") + writer.writeParentheses(any()) + writer.write("'") + writer.write(functionName1) + writer.write("'") + writer.write(",") + writer.write(" ") + writer.writeEach(expressions, ", ", any()) + serializer.serialize(expression1, writer, context) + serializer.serialize(expression2, writer, context) + serializer.serialize(expression3, writer, context) + } + } + + @Test + fun `serialize() draws only the function name, when the args is empty`() { + // given + val part = Predicates.function( + functionName1, + emptyList(), + ) + val context = TestRenderContext(serializer) + + // when + sut.serialize(part as JpqlFunctionPredicate, writer, context) + + // then + verifySequence { + writer.write("FUNCTION") + writer.writeParentheses(any()) + writer.write("'") + writer.write(functionName1) + writer.write("'") + } + } +} diff --git a/render/src/main/kotlin/com/linecorp/kotlinjdsl/render/template/Template.kt b/render/src/main/kotlin/com/linecorp/kotlinjdsl/render/template/Template.kt new file mode 100644 index 000000000..5a9d50bf7 --- /dev/null +++ b/render/src/main/kotlin/com/linecorp/kotlinjdsl/render/template/Template.kt @@ -0,0 +1,54 @@ +package com.linecorp.kotlinjdsl.render.template + +import com.linecorp.kotlinjdsl.Internal + +@Internal +class Template( + val elements: List, +) { + companion object { + private val argumentNumberRegex = Regex("\\{(\\d+)}") + + fun compile(template: String): Template { + var match: MatchResult? = argumentNumberRegex.find(template) + ?: return Template(listOf(TemplateElement.String(template))) + + val elements = mutableListOf() + val length = template.length + + if (match!!.range.first != 0) { + elements.add( + TemplateElement.String(template.substring(0, match.range.first)), + ) + } + + elements.add( + TemplateElement.ArgumentNumber(match.value.let { it.substring(1, it.length - 1) }.toInt()), + ) + + var lastStart = match.range.last + 1 + match = match.next() + + while (lastStart < length && match != null) { + elements.add( + TemplateElement.String(template.substring(lastStart, match.range.first)), + ) + + elements.add( + TemplateElement.ArgumentNumber(match.value.let { it.substring(1, it.length - 1) }.toInt()), + ) + + lastStart = match.range.last + 1 + match = match.next() + } + + if (lastStart < length) { + elements.add( + TemplateElement.String(template.substring(lastStart, length)), + ) + } + + return Template(elements) + } + } +} diff --git a/render/src/main/kotlin/com/linecorp/kotlinjdsl/render/template/TemplateElement.kt b/render/src/main/kotlin/com/linecorp/kotlinjdsl/render/template/TemplateElement.kt new file mode 100644 index 000000000..3aa6be835 --- /dev/null +++ b/render/src/main/kotlin/com/linecorp/kotlinjdsl/render/template/TemplateElement.kt @@ -0,0 +1,14 @@ +package com.linecorp.kotlinjdsl.render.template + +import com.linecorp.kotlinjdsl.Internal + +@Internal +sealed interface TemplateElement { + data class String( + val value: kotlin.String, + ) : TemplateElement + + data class ArgumentNumber( + val value: Int, + ) : TemplateElement +} diff --git a/render/src/test/kotlin/com/linecorp/kotlinjdsl/render/template/TemplateTest.kt b/render/src/test/kotlin/com/linecorp/kotlinjdsl/render/template/TemplateTest.kt new file mode 100644 index 000000000..416bd4680 --- /dev/null +++ b/render/src/test/kotlin/com/linecorp/kotlinjdsl/render/template/TemplateTest.kt @@ -0,0 +1,66 @@ +package com.linecorp.kotlinjdsl.render.template + +import org.assertj.core.api.WithAssertions +import org.junit.jupiter.api.Test + +class TemplateTest : WithAssertions { + @Test + fun compile() { + // when + val actual = Template.compile("{0}, {1}, {2}, {3}") + + // then + assertThat(actual.elements).containsExactly( + TemplateElement.ArgumentNumber(0), + TemplateElement.String(", "), + TemplateElement.ArgumentNumber(1), + TemplateElement.String(", "), + TemplateElement.ArgumentNumber(2), + TemplateElement.String(", "), + TemplateElement.ArgumentNumber(3), + ) + } + + @Test + fun `compile() creates elements with the same arg number, when the template contains the same arg numbers`() { + // when + val actual = Template.compile("{0}, {1}, {0}, {1}") + + // then + assertThat(actual.elements).containsExactly( + TemplateElement.ArgumentNumber(0), + TemplateElement.String(", "), + TemplateElement.ArgumentNumber(1), + TemplateElement.String(", "), + TemplateElement.ArgumentNumber(0), + TemplateElement.String(", "), + TemplateElement.ArgumentNumber(1), + ) + } + + @Test + fun `compile() creates a prefix and postfix, when there is no arg number at the start and end of the template`() { + // when + val actual = Template.compile("START{0}, {1}END") + + // then + assertThat(actual.elements).containsExactly( + TemplateElement.String("START"), + TemplateElement.ArgumentNumber(0), + TemplateElement.String(", "), + TemplateElement.ArgumentNumber(1), + TemplateElement.String("END"), + ) + } + + @Test + fun `compile() creates only string, when there is no arg number`() { + // when + val actual = Template.compile("TEST") + + // then + assertThat(actual.elements).containsExactly( + TemplateElement.String("TEST"), + ) + } +}