Skip to content

Add a Kotlin example of a custom condition #918

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Mar 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import org.mybatis.dynamic.sql.render.RenderingContext;
import org.mybatis.dynamic.sql.util.FragmentAndParameters;

public interface CaseInsensitiveVisitableCondition extends RenderableCondition<String> {
public interface CaseInsensitiveRenderableCondition extends RenderableCondition<String> {

@Override
default FragmentAndParameters renderLeftColumn(RenderingContext renderingContext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
import org.mybatis.dynamic.sql.util.Validator;

public class IsInCaseInsensitive extends AbstractListValueCondition<String>
implements CaseInsensitiveVisitableCondition {
implements CaseInsensitiveRenderableCondition {
private static final IsInCaseInsensitive EMPTY = new IsInCaseInsensitive(Collections.emptyList());

public static IsInCaseInsensitive empty() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
import org.mybatis.dynamic.sql.util.Utilities;

public class IsInCaseInsensitiveWhenPresent extends AbstractListValueCondition<String>
implements CaseInsensitiveVisitableCondition {
implements CaseInsensitiveRenderableCondition {
private static final IsInCaseInsensitiveWhenPresent EMPTY =
new IsInCaseInsensitiveWhenPresent(Collections.emptyList());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import org.mybatis.dynamic.sql.util.StringUtilities;

public class IsLikeCaseInsensitive extends AbstractSingleValueCondition<String>
implements CaseInsensitiveVisitableCondition {
implements CaseInsensitiveRenderableCondition {
private static final IsLikeCaseInsensitive EMPTY = new IsLikeCaseInsensitive("") { //$NON-NLS-1$
@Override
public String value() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
import org.mybatis.dynamic.sql.util.Validator;

public class IsNotInCaseInsensitive extends AbstractListValueCondition<String>
implements CaseInsensitiveVisitableCondition {
implements CaseInsensitiveRenderableCondition {
private static final IsNotInCaseInsensitive EMPTY = new IsNotInCaseInsensitive(Collections.emptyList());

public static IsNotInCaseInsensitive empty() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
import org.mybatis.dynamic.sql.util.Utilities;

public class IsNotInCaseInsensitiveWhenPresent extends AbstractListValueCondition<String>
implements CaseInsensitiveVisitableCondition {
implements CaseInsensitiveRenderableCondition {
private static final IsNotInCaseInsensitiveWhenPresent EMPTY =
new IsNotInCaseInsensitiveWhenPresent(Collections.emptyList());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import org.mybatis.dynamic.sql.util.StringUtilities;

public class IsNotLikeCaseInsensitive extends AbstractSingleValueCondition<String>
implements CaseInsensitiveVisitableCondition {
implements CaseInsensitiveRenderableCondition {
private static final IsNotLikeCaseInsensitive EMPTY = new IsNotLikeCaseInsensitive("") { //$NON-NLS-1$
@Override
public String value() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
54 changes: 54 additions & 0 deletions src/test/kotlin/examples/kotlin/mybatis3/mariadb/KIsLikeEscape.kt
Original file line number Diff line number Diff line change
@@ -0,0 +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

sealed class KIsLikeEscape<T : Any>(
value: T,
private val escapeCharacter: Char? = null
) : AbstractSingleValueCondition<T>(value) {

override fun operator(): String = "like"

override fun renderCondition(
renderingContext: RenderingContext,
leftColumn: BindableColumn<T>
): FragmentAndParameters = with(super.renderCondition(renderingContext, leftColumn)) {
escapeCharacter?.let { mapFragment { "$it ESCAPE '$escapeCharacter'" } } ?: this
}

override fun filter(predicate: Predicate<in T>): KIsLikeEscape<T> =
filterSupport(predicate, EmptyIsLikeEscape::empty, this)

fun <R : Any> map(mapper : Function<in T, out R>): KIsLikeEscape<R> =
mapSupport(mapper, { r -> ConcreteIsLikeEscape(r, escapeCharacter) }, EmptyIsLikeEscape::empty)

companion object {
fun <T: Any> isLike(value: T, escapeCharacter: Char? = null) : KIsLikeEscape<T> =
ConcreteIsLikeEscape(value, escapeCharacter)
}
}

private class ConcreteIsLikeEscape<T: Any>(
value: T,
escapeCharacter: Char? = null
) : KIsLikeEscape<T>(value, escapeCharacter)

private class EmptyIsLikeEscape : KIsLikeEscape<Any>(-1) {
override fun isEmpty(): Boolean = true

override fun value(): Any {
throw NoSuchElementException("No value present")
}

companion object {
private val EMPTY: KIsLikeEscape<Any> = EmptyIsLikeEscape()

@Suppress("UNCHECKED_CAST")
internal fun <T : Any> empty(): KIsLikeEscape<T> = EMPTY as KIsLikeEscape<T>
}
}
87 changes: 87 additions & 0 deletions src/test/kotlin/examples/kotlin/mybatis3/mariadb/KMariaDBTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.Locale

@Testcontainers
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
Expand Down Expand Up @@ -142,6 +143,92 @@ class KMariaDBTest {
}
}


/**
* 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 <T : Any> isLike(value: T, escapeCharacter: Char? = null) = KIsLikeEscape.isLike(value, escapeCharacter)

@Test
fun testIsLikeEscape() {
sqlSessionFactory.openSession().use { session ->
val mapper = session.getMapper(CommonSelectMapper::class.java)
val selectStatement = select(id, description) {
from(items)
where {
description(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(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(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(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(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)
Expand Down