From 1d4dbe1b5710fa81da865b4411832b8718469631 Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Fri, 7 Mar 2025 09:40:55 -0500 Subject: [PATCH 1/6] Kotlin version of a custom condition --- .../kotlin/mybatis3/mariadb/KIsLikeEscape.kt | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/test/kotlin/examples/kotlin/mybatis3/mariadb/KIsLikeEscape.kt diff --git a/src/test/kotlin/examples/kotlin/mybatis3/mariadb/KIsLikeEscape.kt b/src/test/kotlin/examples/kotlin/mybatis3/mariadb/KIsLikeEscape.kt new file mode 100644 index 000000000..454f47d96 --- /dev/null +++ b/src/test/kotlin/examples/kotlin/mybatis3/mariadb/KIsLikeEscape.kt @@ -0,0 +1,44 @@ +package examples.kotlin.mybatis3.mariadb + +import org.mybatis.dynamic.sql.AbstractSingleValueCondition +import org.mybatis.dynamic.sql.BindableColumn +import org.mybatis.dynamic.sql.render.RenderingContext +import org.mybatis.dynamic.sql.util.FragmentAndParameters +import java.util.function.Predicate + +open class KIsLikeEscape(value: T, private val escapeCharacter: Char? = null) : AbstractSingleValueCondition(value) { + + override fun operator(): String = "like" + + override fun renderCondition( + renderingContext: RenderingContext, + leftColumn: BindableColumn + ): FragmentAndParameters { + val f = super.renderCondition(renderingContext, leftColumn) + + return escapeCharacter?.let { f.mapFragment{ "$it ESCAPE '$escapeCharacter'"} } ?: f + } + + override fun filter(predicate: Predicate): KIsLikeEscape { + return filterSupport(predicate, ::empty, this) + } + + fun map(mapper : (T) -> R): KIsLikeEscape { + return mapSupport(mapper, { r -> KIsLikeEscape(r, escapeCharacter) }, ::empty) + } + + private class EmptyCondition : KIsLikeEscape(-1) { + override fun isEmpty(): Boolean = true + + override fun value(): Any { + throw NoSuchElementException("No value present") //$NON-NLS-1$ + } + } + + companion object { + private val EMPTY: KIsLikeEscape = EmptyCondition() + + @Suppress("UNCHECKED_CAST") + fun empty(): KIsLikeEscape = EMPTY as KIsLikeEscape + } +} From 90ddb51e93948e90345f128b81e1619aa43efea2 Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Fri, 7 Mar 2025 11:00:06 -0500 Subject: [PATCH 2/6] Example of a Custom Condition in Kotlin --- .../kotlin/mybatis3/mariadb/KIsLikeEscape.kt | 46 ++++++----- .../kotlin/mybatis3/mariadb/KMariaDBTest.kt | 77 +++++++++++++++++++ 2 files changed, 105 insertions(+), 18 deletions(-) diff --git a/src/test/kotlin/examples/kotlin/mybatis3/mariadb/KIsLikeEscape.kt b/src/test/kotlin/examples/kotlin/mybatis3/mariadb/KIsLikeEscape.kt index 454f47d96..e018a8329 100644 --- a/src/test/kotlin/examples/kotlin/mybatis3/mariadb/KIsLikeEscape.kt +++ b/src/test/kotlin/examples/kotlin/mybatis3/mariadb/KIsLikeEscape.kt @@ -1,44 +1,54 @@ package examples.kotlin.mybatis3.mariadb +import java.util.function.Predicate +import java.util.function.Function import org.mybatis.dynamic.sql.AbstractSingleValueCondition import org.mybatis.dynamic.sql.BindableColumn import org.mybatis.dynamic.sql.render.RenderingContext import org.mybatis.dynamic.sql.util.FragmentAndParameters -import java.util.function.Predicate -open class KIsLikeEscape(value: T, private val escapeCharacter: Char? = null) : AbstractSingleValueCondition(value) { +sealed class KIsLikeEscape( + value: T, + private val escapeCharacter: Char? = null +) : AbstractSingleValueCondition(value) { override fun operator(): String = "like" override fun renderCondition( renderingContext: RenderingContext, leftColumn: BindableColumn - ): FragmentAndParameters { - val f = super.renderCondition(renderingContext, leftColumn) - - return escapeCharacter?.let { f.mapFragment{ "$it ESCAPE '$escapeCharacter'"} } ?: f + ): FragmentAndParameters = with(super.renderCondition(renderingContext, leftColumn)) { + escapeCharacter?.let { mapFragment { "$it ESCAPE '$escapeCharacter'" } } ?: this } - override fun filter(predicate: Predicate): KIsLikeEscape { - return filterSupport(predicate, ::empty, this) - } + override fun filter(predicate: Predicate): KIsLikeEscape = + filterSupport(predicate, EmptyIsLikeEscape::empty, this) - fun map(mapper : (T) -> R): KIsLikeEscape { - return mapSupport(mapper, { r -> KIsLikeEscape(r, escapeCharacter) }, ::empty) + fun map(mapper : Function): KIsLikeEscape = + mapSupport(mapper, { r -> ConcreteIsLikeEscape(r, escapeCharacter) }, EmptyIsLikeEscape::empty) + + companion object { + fun isLike(value: T, escapeCharacter: Char? = null) : KIsLikeEscape = + ConcreteIsLikeEscape(value, escapeCharacter) } +} + +private class ConcreteIsLikeEscape( + value: T, + escapeCharacter: Char? = null +) : KIsLikeEscape(value, escapeCharacter) - private class EmptyCondition : KIsLikeEscape(-1) { - override fun isEmpty(): Boolean = true +private class EmptyIsLikeEscape : KIsLikeEscape(-1) { + override fun isEmpty(): Boolean = true - override fun value(): Any { - throw NoSuchElementException("No value present") //$NON-NLS-1$ - } + override fun value(): Any { + throw NoSuchElementException("No value present") } companion object { - private val EMPTY: KIsLikeEscape = EmptyCondition() + private val EMPTY: KIsLikeEscape = EmptyIsLikeEscape() @Suppress("UNCHECKED_CAST") - fun empty(): KIsLikeEscape = EMPTY as KIsLikeEscape + internal fun empty(): KIsLikeEscape = EMPTY as KIsLikeEscape } } diff --git a/src/test/kotlin/examples/kotlin/mybatis3/mariadb/KMariaDBTest.kt b/src/test/kotlin/examples/kotlin/mybatis3/mariadb/KMariaDBTest.kt index 8ce870c01..11593f2ac 100644 --- a/src/test/kotlin/examples/kotlin/mybatis3/mariadb/KMariaDBTest.kt +++ b/src/test/kotlin/examples/kotlin/mybatis3/mariadb/KMariaDBTest.kt @@ -38,6 +38,7 @@ import org.mybatis.dynamic.sql.util.mybatis3.CommonUpdateMapper import org.testcontainers.containers.MariaDBContainer import org.testcontainers.junit.jupiter.Container import org.testcontainers.junit.jupiter.Testcontainers +import java.util.* @Testcontainers @TestInstance(TestInstance.Lifecycle.PER_CLASS) @@ -142,6 +143,82 @@ class KMariaDBTest { } } + @Test + fun testIsLikeEscape() { + sqlSessionFactory.openSession().use { session -> + val mapper = session.getMapper(CommonSelectMapper::class.java) + val selectStatement = select(id, description) { + from(items) + where { + description(KIsLikeEscape.isLike("Item 1%", '#')) + } + } + + assertThat(selectStatement.selectStatement).isEqualTo("select id, description from items where description like #{parameters.p1,jdbcType=VARCHAR} ESCAPE '#'") + assertThat(selectStatement.parameters).containsEntry("p1", "Item 1%") + + val rows = mapper.selectManyMappedRows(selectStatement) + assertThat(rows).hasSize(11) + } + } + + @Test + fun testIsLikeEscapeNoEscapeCharacter() { + val selectStatement = select(id, description) { + from(items) + where { + description(KIsLikeEscape.isLike("%fred%")) + } + } + + assertThat(selectStatement.selectStatement).isEqualTo("select id, description from items where description like #{parameters.p1,jdbcType=VARCHAR}") + assertThat(selectStatement.parameters).containsEntry("p1", "%fred%") + } + + @Test + fun testIsLikeEscapeMap() { + val selectStatement = select(id, description) { + from(items) + where { + description(KIsLikeEscape.isLike("%fred%", '#').map { s -> s.uppercase(Locale.getDefault()) }) + } + } + + assertThat(selectStatement.selectStatement).isEqualTo("select id, description from items where description like #{parameters.p1,jdbcType=VARCHAR} ESCAPE '#'") + assertThat(selectStatement.parameters).containsEntry("p1", "%FRED%") + } + + @Test + fun testIsLikeEscapeFilter() { + val selectStatement = select(id, description) { + from(items) + where { + description(KIsLikeEscape.isLike("%fred%", '#').filter { _ -> false }) + } + configureStatement { isNonRenderingWhereClauseAllowed = true } + } + + assertThat(selectStatement.selectStatement).isEqualTo("select id, description from items") + assertThat(selectStatement.parameters).isEmpty() + } + + @Test + fun testIsLikeEscapeFilterMapFilter() { + val selectStatement = select(id, description) { + from(items) + where { + description(KIsLikeEscape.isLike("%fred%", '#') + .filter { _ -> true } + .map { s -> s.uppercase(Locale.getDefault()) } + .filter{_ -> false }) + } + configureStatement { isNonRenderingWhereClauseAllowed = true } + } + + assertThat(selectStatement.selectStatement).isEqualTo("select id, description from items") + assertThat(selectStatement.parameters).isEmpty() + } + companion object { @Container private val mariadb = MariaDBContainer(TestContainersConfiguration.MARIADB_LATEST) From 80e0d64ad0d5ee84dbf4e99579bf02251bbaa8b0 Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Fri, 7 Mar 2025 11:03:14 -0500 Subject: [PATCH 3/6] Rename to match the super interface --- ...leCondition.java => CaseInsensitiveRenderableCondition.java} | 2 +- .../dynamic/sql/where/condition/IsInCaseInsensitive.java | 2 +- .../sql/where/condition/IsInCaseInsensitiveWhenPresent.java | 2 +- .../dynamic/sql/where/condition/IsLikeCaseInsensitive.java | 2 +- .../dynamic/sql/where/condition/IsNotInCaseInsensitive.java | 2 +- .../sql/where/condition/IsNotInCaseInsensitiveWhenPresent.java | 2 +- .../dynamic/sql/where/condition/IsNotLikeCaseInsensitive.java | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) rename src/main/java/org/mybatis/dynamic/sql/where/condition/{CaseInsensitiveVisitableCondition.java => CaseInsensitiveRenderableCondition.java} (93%) diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/CaseInsensitiveVisitableCondition.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/CaseInsensitiveRenderableCondition.java similarity index 93% rename from src/main/java/org/mybatis/dynamic/sql/where/condition/CaseInsensitiveVisitableCondition.java rename to src/main/java/org/mybatis/dynamic/sql/where/condition/CaseInsensitiveRenderableCondition.java index 9224ad594..2c837c4c7 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/CaseInsensitiveVisitableCondition.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/CaseInsensitiveRenderableCondition.java @@ -20,7 +20,7 @@ import org.mybatis.dynamic.sql.render.RenderingContext; import org.mybatis.dynamic.sql.util.FragmentAndParameters; -public interface CaseInsensitiveVisitableCondition extends RenderableCondition { +public interface CaseInsensitiveRenderableCondition extends RenderableCondition { @Override default FragmentAndParameters renderLeftColumn(RenderingContext renderingContext, diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsInCaseInsensitive.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsInCaseInsensitive.java index 6f14406fa..67f37951e 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsInCaseInsensitive.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsInCaseInsensitive.java @@ -27,7 +27,7 @@ import org.mybatis.dynamic.sql.util.Validator; public class IsInCaseInsensitive extends AbstractListValueCondition - implements CaseInsensitiveVisitableCondition { + implements CaseInsensitiveRenderableCondition { private static final IsInCaseInsensitive EMPTY = new IsInCaseInsensitive(Collections.emptyList()); public static IsInCaseInsensitive empty() { diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsInCaseInsensitiveWhenPresent.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsInCaseInsensitiveWhenPresent.java index d366f0857..cff58415d 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsInCaseInsensitiveWhenPresent.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsInCaseInsensitiveWhenPresent.java @@ -27,7 +27,7 @@ import org.mybatis.dynamic.sql.util.Utilities; public class IsInCaseInsensitiveWhenPresent extends AbstractListValueCondition - implements CaseInsensitiveVisitableCondition { + implements CaseInsensitiveRenderableCondition { private static final IsInCaseInsensitiveWhenPresent EMPTY = new IsInCaseInsensitiveWhenPresent(Collections.emptyList()); diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLikeCaseInsensitive.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLikeCaseInsensitive.java index ccf6699ee..4ebdb00fb 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLikeCaseInsensitive.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsLikeCaseInsensitive.java @@ -23,7 +23,7 @@ import org.mybatis.dynamic.sql.util.StringUtilities; public class IsLikeCaseInsensitive extends AbstractSingleValueCondition - implements CaseInsensitiveVisitableCondition { + implements CaseInsensitiveRenderableCondition { private static final IsLikeCaseInsensitive EMPTY = new IsLikeCaseInsensitive("") { //$NON-NLS-1$ @Override public String value() { diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotInCaseInsensitive.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotInCaseInsensitive.java index b4c1e96a7..f5f970c45 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotInCaseInsensitive.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotInCaseInsensitive.java @@ -27,7 +27,7 @@ import org.mybatis.dynamic.sql.util.Validator; public class IsNotInCaseInsensitive extends AbstractListValueCondition - implements CaseInsensitiveVisitableCondition { + implements CaseInsensitiveRenderableCondition { private static final IsNotInCaseInsensitive EMPTY = new IsNotInCaseInsensitive(Collections.emptyList()); public static IsNotInCaseInsensitive empty() { diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotInCaseInsensitiveWhenPresent.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotInCaseInsensitiveWhenPresent.java index 5562b0ff4..a8873a7e4 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotInCaseInsensitiveWhenPresent.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotInCaseInsensitiveWhenPresent.java @@ -27,7 +27,7 @@ import org.mybatis.dynamic.sql.util.Utilities; public class IsNotInCaseInsensitiveWhenPresent extends AbstractListValueCondition - implements CaseInsensitiveVisitableCondition { + implements CaseInsensitiveRenderableCondition { private static final IsNotInCaseInsensitiveWhenPresent EMPTY = new IsNotInCaseInsensitiveWhenPresent(Collections.emptyList()); diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotLikeCaseInsensitive.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotLikeCaseInsensitive.java index 5d9d8c3af..1fb6847c0 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotLikeCaseInsensitive.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/IsNotLikeCaseInsensitive.java @@ -23,7 +23,7 @@ import org.mybatis.dynamic.sql.util.StringUtilities; public class IsNotLikeCaseInsensitive extends AbstractSingleValueCondition - implements CaseInsensitiveVisitableCondition { + implements CaseInsensitiveRenderableCondition { private static final IsNotLikeCaseInsensitive EMPTY = new IsNotLikeCaseInsensitive("") { //$NON-NLS-1$ @Override public String value() { From ff530c6d3dd7014a417289f227564fcc386cf988 Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Fri, 7 Mar 2025 11:15:19 -0500 Subject: [PATCH 4/6] Add comment about Kotlin context parameters --- .../kotlin/examples/kotlin/mybatis3/mariadb/KMariaDBTest.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/test/kotlin/examples/kotlin/mybatis3/mariadb/KMariaDBTest.kt b/src/test/kotlin/examples/kotlin/mybatis3/mariadb/KMariaDBTest.kt index 11593f2ac..ee593404c 100644 --- a/src/test/kotlin/examples/kotlin/mybatis3/mariadb/KMariaDBTest.kt +++ b/src/test/kotlin/examples/kotlin/mybatis3/mariadb/KMariaDBTest.kt @@ -143,6 +143,10 @@ class KMariaDBTest { } } + // Note that the following example uses of KIsLikeEscape are a bit awkward and don't look as natural as the + // built-in conditions. We should be able to improve this once Kotlin implements the context parameters + // proposal (https://github.com/Kotlin/KEEP/issues/367) + @Test fun testIsLikeEscape() { sqlSessionFactory.openSession().use { session -> From 6694eba0403e8bacae901be0c2388c6ec7350c97 Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Fri, 7 Mar 2025 11:29:26 -0500 Subject: [PATCH 5/6] Add comment about Kotlin context parameters --- .../util/kotlin/GroupingCriteriaCollector.kt | 4 ++-- .../kotlin/mybatis3/mariadb/KMariaDBTest.kt | 22 ++++++++++++------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/GroupingCriteriaCollector.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/GroupingCriteriaCollector.kt index 0aaaff460..be4ba72af 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/GroupingCriteriaCollector.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/GroupingCriteriaCollector.kt @@ -235,8 +235,8 @@ open class GroupingCriteriaCollector : SubCriteriaCollector() { .build() } - // infix functions...we may be able to rewrite these as extension functions once Kotlin solves the multiple - // receivers problem (https://youtrack.jetbrains.com/issue/KT-42435) + // infix functions...we may be able to rewrite these as extension functions once Kotlin implements the context + // parameters proposal (https://github.com/Kotlin/KEEP/issues/367) // conditions for all data types fun BindableColumn<*>.isNull() = invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNull()) diff --git a/src/test/kotlin/examples/kotlin/mybatis3/mariadb/KMariaDBTest.kt b/src/test/kotlin/examples/kotlin/mybatis3/mariadb/KMariaDBTest.kt index ee593404c..fe1dd2906 100644 --- a/src/test/kotlin/examples/kotlin/mybatis3/mariadb/KMariaDBTest.kt +++ b/src/test/kotlin/examples/kotlin/mybatis3/mariadb/KMariaDBTest.kt @@ -143,9 +143,15 @@ class KMariaDBTest { } } - // Note that the following example uses of KIsLikeEscape are a bit awkward and don't look as natural as the - // built-in conditions. We should be able to improve this once Kotlin implements the context parameters - // proposal (https://github.com/Kotlin/KEEP/issues/367) + + /** + * Shortcut function for KIsLikeEscape + * + * Note that the following example uses of this function are a bit awkward and don't look as natural as the + * built-in conditions. We should be able to improve this once Kotlin implements the context parameters + * proposal (https://github.com/Kotlin/KEEP/issues/367) + */ + fun isLike(value: T, escapeCharacter: Char? = null) = KIsLikeEscape.isLike(value, escapeCharacter) @Test fun testIsLikeEscape() { @@ -154,7 +160,7 @@ class KMariaDBTest { val selectStatement = select(id, description) { from(items) where { - description(KIsLikeEscape.isLike("Item 1%", '#')) + description(isLike("Item 1%", '#')) } } @@ -171,7 +177,7 @@ class KMariaDBTest { val selectStatement = select(id, description) { from(items) where { - description(KIsLikeEscape.isLike("%fred%")) + description(isLike("%fred%")) } } @@ -184,7 +190,7 @@ class KMariaDBTest { val selectStatement = select(id, description) { from(items) where { - description(KIsLikeEscape.isLike("%fred%", '#').map { s -> s.uppercase(Locale.getDefault()) }) + description(isLike("%fred%", '#').map { s -> s.uppercase(Locale.getDefault()) }) } } @@ -197,7 +203,7 @@ class KMariaDBTest { val selectStatement = select(id, description) { from(items) where { - description(KIsLikeEscape.isLike("%fred%", '#').filter { _ -> false }) + description(isLike("%fred%", '#').filter { _ -> false }) } configureStatement { isNonRenderingWhereClauseAllowed = true } } @@ -211,7 +217,7 @@ class KMariaDBTest { val selectStatement = select(id, description) { from(items) where { - description(KIsLikeEscape.isLike("%fred%", '#') + description(isLike("%fred%", '#') .filter { _ -> true } .map { s -> s.uppercase(Locale.getDefault()) } .filter{_ -> false }) From 10fe79dbec366cc984a67610050b69240b44eeed Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Fri, 7 Mar 2025 11:31:06 -0500 Subject: [PATCH 6/6] Star import is not necessary --- .../kotlin/examples/kotlin/mybatis3/mariadb/KMariaDBTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/kotlin/examples/kotlin/mybatis3/mariadb/KMariaDBTest.kt b/src/test/kotlin/examples/kotlin/mybatis3/mariadb/KMariaDBTest.kt index fe1dd2906..28bf98a3b 100644 --- a/src/test/kotlin/examples/kotlin/mybatis3/mariadb/KMariaDBTest.kt +++ b/src/test/kotlin/examples/kotlin/mybatis3/mariadb/KMariaDBTest.kt @@ -38,7 +38,7 @@ import org.mybatis.dynamic.sql.util.mybatis3.CommonUpdateMapper import org.testcontainers.containers.MariaDBContainer import org.testcontainers.junit.jupiter.Container import org.testcontainers.junit.jupiter.Testcontainers -import java.util.* +import java.util.Locale @Testcontainers @TestInstance(TestInstance.Lifecycle.PER_CLASS)