From 1ae2e43d103aa5f538477530fd7cc9b27304e891 Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Wed, 5 Mar 2025 14:29:25 -0500 Subject: [PATCH 1/6] Allow Conditions Increased Control over Rendering --- .../AbstractColumnComparisonCondition.java | 15 +- .../sql/AbstractListValueCondition.java | 30 +++- .../dynamic/sql/AbstractNoValueCondition.java | 13 +- .../sql/AbstractSingleValueCondition.java | 21 ++- .../sql/AbstractSubselectCondition.java | 18 ++- .../sql/AbstractTwoValueCondition.java | 27 +++- .../mybatis/dynamic/sql/ConditionVisitor.java | 30 ---- .../dynamic/sql/VisitableCondition.java | 21 +-- .../SimpleCaseWhenConditionRenderer.java | 8 +- .../CaseInsensitiveVisitableCondition.java | 9 +- .../render/ColumnAndConditionRenderer.java | 15 +- .../where/render/DefaultConditionVisitor.java | 137 ------------------ .../java/examples/mysql/IsLikeEscape.java | 71 +++++++++ src/test/java/examples/mysql/MySQLTest.java | 21 +++ 14 files changed, 198 insertions(+), 238 deletions(-) delete mode 100644 src/main/java/org/mybatis/dynamic/sql/ConditionVisitor.java delete mode 100644 src/main/java/org/mybatis/dynamic/sql/where/render/DefaultConditionVisitor.java create mode 100644 src/test/java/examples/mysql/IsLikeEscape.java diff --git a/src/main/java/org/mybatis/dynamic/sql/AbstractColumnComparisonCondition.java b/src/main/java/org/mybatis/dynamic/sql/AbstractColumnComparisonCondition.java index ff6923ecb..502025401 100644 --- a/src/main/java/org/mybatis/dynamic/sql/AbstractColumnComparisonCondition.java +++ b/src/main/java/org/mybatis/dynamic/sql/AbstractColumnComparisonCondition.java @@ -15,6 +15,11 @@ */ package org.mybatis.dynamic.sql; +import static org.mybatis.dynamic.sql.util.StringUtilities.spaceBefore; + +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; + public abstract class AbstractColumnComparisonCondition implements VisitableCondition { protected final BasicColumn rightColumn; @@ -23,14 +28,10 @@ protected AbstractColumnComparisonCondition(BasicColumn rightColumn) { this.rightColumn = rightColumn; } - public BasicColumn rightColumn() { - return rightColumn; - } + public abstract String operator(); @Override - public R accept(ConditionVisitor visitor) { - return visitor.visit(this); + public FragmentAndParameters renderCondition(RenderingContext renderingContext, BindableColumn leftColumn) { + return rightColumn.render(renderingContext).mapFragment(f -> operator() + spaceBefore(f)); } - - public abstract String operator(); } diff --git a/src/main/java/org/mybatis/dynamic/sql/AbstractListValueCondition.java b/src/main/java/org/mybatis/dynamic/sql/AbstractListValueCondition.java index 21230c821..eeed434ab 100644 --- a/src/main/java/org/mybatis/dynamic/sql/AbstractListValueCondition.java +++ b/src/main/java/org/mybatis/dynamic/sql/AbstractListValueCondition.java @@ -23,6 +23,11 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import org.mybatis.dynamic.sql.render.RenderedParameterInfo; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; +import org.mybatis.dynamic.sql.util.FragmentCollector; + public abstract class AbstractListValueCondition implements VisitableCondition { protected final Collection values; @@ -39,19 +44,14 @@ public boolean isEmpty() { return values.isEmpty(); } - @Override - public R accept(ConditionVisitor visitor) { - return visitor.visit(this); - } - private Collection applyMapper(Function mapper) { Objects.requireNonNull(mapper); - return values.stream().map(mapper).collect(Collectors.toList()); + return values().map(mapper).collect(Collectors.toList()); } private Collection applyFilter(Predicate predicate) { Objects.requireNonNull(predicate); - return values.stream().filter(predicate).toList(); + return values().filter(predicate).toList(); } protected > S filterSupport(Predicate predicate, @@ -84,4 +84,20 @@ protected > S mapSupport(Function filter(Predicate predicate); public abstract String operator(); + + @Override + public FragmentAndParameters renderCondition(RenderingContext renderingContext, BindableColumn leftColumn) { + return values().map(v -> toFragmentAndParameters(v, renderingContext, leftColumn)) + .collect(FragmentCollector.collect()) + .toFragmentAndParameters(Collectors.joining(",", //$NON-NLS-1$ + operator() + " (", ")")); //$NON-NLS-1$ //$NON-NLS-2$ + } + + private FragmentAndParameters toFragmentAndParameters(T value, RenderingContext renderingContext, + BindableColumn leftColumn) { + RenderedParameterInfo parameterInfo = renderingContext.calculateParameterInfo(leftColumn); + return FragmentAndParameters.withFragment(parameterInfo.renderedPlaceHolder()) + .withParameter(parameterInfo.parameterMapKey(), leftColumn.convertParameterType(value)) + .build(); + } } diff --git a/src/main/java/org/mybatis/dynamic/sql/AbstractNoValueCondition.java b/src/main/java/org/mybatis/dynamic/sql/AbstractNoValueCondition.java index a60f2a843..02f6e25f4 100644 --- a/src/main/java/org/mybatis/dynamic/sql/AbstractNoValueCondition.java +++ b/src/main/java/org/mybatis/dynamic/sql/AbstractNoValueCondition.java @@ -18,12 +18,10 @@ import java.util.function.BooleanSupplier; import java.util.function.Supplier; -public abstract class AbstractNoValueCondition implements VisitableCondition { +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; - @Override - public R accept(ConditionVisitor visitor) { - return visitor.visit(this); - } +public abstract class AbstractNoValueCondition implements VisitableCondition { protected > S filterSupport(BooleanSupplier booleanSupplier, Supplier emptySupplier, S self) { @@ -35,4 +33,9 @@ protected > S filterSupport(BooleanSupplie } public abstract String operator(); + + @Override + public FragmentAndParameters renderCondition(RenderingContext renderingContext, BindableColumn leftColumn) { + return FragmentAndParameters.fromFragment(operator()); + } } diff --git a/src/main/java/org/mybatis/dynamic/sql/AbstractSingleValueCondition.java b/src/main/java/org/mybatis/dynamic/sql/AbstractSingleValueCondition.java index 13d4dc8e1..a361c4ed0 100644 --- a/src/main/java/org/mybatis/dynamic/sql/AbstractSingleValueCondition.java +++ b/src/main/java/org/mybatis/dynamic/sql/AbstractSingleValueCondition.java @@ -15,10 +15,16 @@ */ package org.mybatis.dynamic.sql; +import static org.mybatis.dynamic.sql.util.StringUtilities.spaceBefore; + import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; +import org.mybatis.dynamic.sql.render.RenderedParameterInfo; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; + public abstract class AbstractSingleValueCondition implements VisitableCondition { protected final T value; @@ -30,11 +36,6 @@ public T value() { return value; } - @Override - public R accept(ConditionVisitor visitor) { - return visitor.visit(this); - } - protected > S filterSupport(Predicate predicate, Supplier emptySupplier, S self) { if (isEmpty()) { @@ -64,4 +65,14 @@ protected > S mapSupport(Function filter(Predicate predicate); public abstract String operator(); + + @Override + public FragmentAndParameters renderCondition(RenderingContext renderingContext, BindableColumn leftColumn) { + RenderedParameterInfo parameterInfo = renderingContext.calculateParameterInfo(leftColumn); + String finalFragment = operator() + spaceBefore(parameterInfo.renderedPlaceHolder()); + + return FragmentAndParameters.withFragment(finalFragment) + .withParameter(parameterInfo.parameterMapKey(), leftColumn.convertParameterType(value())) + .build(); + } } diff --git a/src/main/java/org/mybatis/dynamic/sql/AbstractSubselectCondition.java b/src/main/java/org/mybatis/dynamic/sql/AbstractSubselectCondition.java index 4408f279c..3254fbd2d 100644 --- a/src/main/java/org/mybatis/dynamic/sql/AbstractSubselectCondition.java +++ b/src/main/java/org/mybatis/dynamic/sql/AbstractSubselectCondition.java @@ -15,8 +15,11 @@ */ package org.mybatis.dynamic.sql; +import org.mybatis.dynamic.sql.render.RenderingContext; import org.mybatis.dynamic.sql.select.SelectModel; +import org.mybatis.dynamic.sql.select.render.SubQueryRenderer; import org.mybatis.dynamic.sql.util.Buildable; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; public abstract class AbstractSubselectCondition implements VisitableCondition { private final SelectModel selectModel; @@ -25,14 +28,15 @@ protected AbstractSubselectCondition(Buildable selectModelBuilder) this.selectModel = selectModelBuilder.build(); } - public SelectModel selectModel() { - return selectModel; - } + public abstract String operator(); @Override - public R accept(ConditionVisitor visitor) { - return visitor.visit(this); + public FragmentAndParameters renderCondition(RenderingContext renderingContext, BindableColumn leftColumn) { + return SubQueryRenderer.withSelectModel(selectModel) + .withRenderingContext(renderingContext) + .withPrefix(operator() + " (") //$NON-NLS-1$ + .withSuffix(")") //$NON-NLS-1$ + .build() + .render(); } - - public abstract String operator(); } diff --git a/src/main/java/org/mybatis/dynamic/sql/AbstractTwoValueCondition.java b/src/main/java/org/mybatis/dynamic/sql/AbstractTwoValueCondition.java index c36fe186d..fe34333a2 100644 --- a/src/main/java/org/mybatis/dynamic/sql/AbstractTwoValueCondition.java +++ b/src/main/java/org/mybatis/dynamic/sql/AbstractTwoValueCondition.java @@ -15,12 +15,18 @@ */ package org.mybatis.dynamic.sql; +import static org.mybatis.dynamic.sql.util.StringUtilities.spaceBefore; + import java.util.function.BiFunction; import java.util.function.BiPredicate; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; +import org.mybatis.dynamic.sql.render.RenderedParameterInfo; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; + public abstract class AbstractTwoValueCondition implements VisitableCondition { protected final T value1; protected final T value2; @@ -38,11 +44,6 @@ public T value2() { return value2; } - @Override - public R accept(ConditionVisitor visitor) { - return visitor.visit(this); - } - protected > S filterSupport(BiPredicate predicate, Supplier emptySupplier, S self) { if (isEmpty()) { @@ -90,4 +91,20 @@ protected > S mapSupport(Function leftColumn) { + RenderedParameterInfo parameterInfo1 = renderingContext.calculateParameterInfo(leftColumn); + RenderedParameterInfo parameterInfo2 = renderingContext.calculateParameterInfo(leftColumn); + + String finalFragment = operator1() + + spaceBefore(parameterInfo1.renderedPlaceHolder()) + + spaceBefore(operator2()) + + spaceBefore(parameterInfo2.renderedPlaceHolder()); + + return FragmentAndParameters.withFragment(finalFragment) + .withParameter(parameterInfo1.parameterMapKey(), leftColumn.convertParameterType(value1())) + .withParameter(parameterInfo2.parameterMapKey(), leftColumn.convertParameterType(value2())) + .build(); + } } diff --git a/src/main/java/org/mybatis/dynamic/sql/ConditionVisitor.java b/src/main/java/org/mybatis/dynamic/sql/ConditionVisitor.java deleted file mode 100644 index 99a4fd36c..000000000 --- a/src/main/java/org/mybatis/dynamic/sql/ConditionVisitor.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2016-2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.mybatis.dynamic.sql; - -public interface ConditionVisitor { - R visit(AbstractListValueCondition condition); - - R visit(AbstractNoValueCondition condition); - - R visit(AbstractSingleValueCondition condition); - - R visit(AbstractTwoValueCondition condition); - - R visit(AbstractSubselectCondition condition); - - R visit(AbstractColumnComparisonCondition condition); -} diff --git a/src/main/java/org/mybatis/dynamic/sql/VisitableCondition.java b/src/main/java/org/mybatis/dynamic/sql/VisitableCondition.java index fc974b932..d201ae4dd 100644 --- a/src/main/java/org/mybatis/dynamic/sql/VisitableCondition.java +++ b/src/main/java/org/mybatis/dynamic/sql/VisitableCondition.java @@ -16,10 +16,17 @@ package org.mybatis.dynamic.sql; import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; @FunctionalInterface public interface VisitableCondition { - R accept(ConditionVisitor visitor); + FragmentAndParameters renderCondition(RenderingContext renderingContext, BindableColumn leftColumn); + + default FragmentAndParameters renderLeftColumn(RenderingContext renderingContext, BindableColumn leftColumn) { + return leftColumn.alias() + .map(FragmentAndParameters::fromFragment) + .orElseGet(() -> leftColumn.render(renderingContext)); + } /** * Subclasses can override this to inform the renderer if the condition should not be included @@ -46,16 +53,4 @@ default boolean isEmpty() { * returns false. */ default void renderingSkipped() {} - - /** - * This method is called during rendering. Its purpose is to allow conditions to change - * the value of the rendered left column. This is primarily used in the case-insensitive conditions - * where we surround the rendered column with "upper(" and ")". - * - * @param renderedLeftColumn the rendered left column - * @return the altered column - by default no change is applied - */ - default String overrideRenderedLeftColumn(String renderedLeftColumn) { - return renderedLeftColumn; - } } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseWhenConditionRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseWhenConditionRenderer.java index 702f1a0ad..cb13434d3 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseWhenConditionRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseWhenConditionRenderer.java @@ -28,20 +28,14 @@ import org.mybatis.dynamic.sql.util.FragmentAndParameters; import org.mybatis.dynamic.sql.util.FragmentCollector; import org.mybatis.dynamic.sql.util.Validator; -import org.mybatis.dynamic.sql.where.render.DefaultConditionVisitor; public class SimpleCaseWhenConditionRenderer implements SimpleCaseWhenConditionVisitor { private final RenderingContext renderingContext; private final BindableColumn column; - private final DefaultConditionVisitor conditionVisitor; public SimpleCaseWhenConditionRenderer(RenderingContext renderingContext, BindableColumn column) { this.renderingContext = Objects.requireNonNull(renderingContext); this.column = Objects.requireNonNull(column); - conditionVisitor = new DefaultConditionVisitor.Builder() - .withColumn(column) - .withRenderingContext(renderingContext) - .build(); } @Override @@ -68,7 +62,7 @@ private boolean shouldRender(VisitableCondition condition) { } private FragmentAndParameters renderCondition(VisitableCondition condition) { - return condition.accept(conditionVisitor); + return condition.renderCondition(renderingContext, column); } private FragmentAndParameters renderBasicValue(T value) { diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/CaseInsensitiveVisitableCondition.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/CaseInsensitiveVisitableCondition.java index c0eef325a..9315b68fa 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/CaseInsensitiveVisitableCondition.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/CaseInsensitiveVisitableCondition.java @@ -15,12 +15,17 @@ */ package org.mybatis.dynamic.sql.where.condition; +import org.mybatis.dynamic.sql.BindableColumn; import org.mybatis.dynamic.sql.VisitableCondition; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; public interface CaseInsensitiveVisitableCondition extends VisitableCondition { @Override - default String overrideRenderedLeftColumn(String renderedLeftColumn) { - return "upper(" + renderedLeftColumn + ")"; //$NON-NLS-1$ //$NON-NLS-2$ + default FragmentAndParameters renderLeftColumn(RenderingContext renderingContext, + BindableColumn leftColumn) { + return VisitableCondition.super.renderLeftColumn(renderingContext, leftColumn) + .mapFragment(s -> "upper(" + s + ")"); //$NON-NLS-1$ //$NON-NLS-2$ } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/render/ColumnAndConditionRenderer.java b/src/main/java/org/mybatis/dynamic/sql/where/render/ColumnAndConditionRenderer.java index 6035c143b..d640c0796 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/render/ColumnAndConditionRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/render/ColumnAndConditionRenderer.java @@ -29,31 +29,20 @@ public class ColumnAndConditionRenderer { private final BindableColumn column; private final VisitableCondition condition; private final RenderingContext renderingContext; - private final DefaultConditionVisitor visitor; private ColumnAndConditionRenderer(Builder builder) { column = Objects.requireNonNull(builder.column); condition = Objects.requireNonNull(builder.condition); renderingContext = Objects.requireNonNull(builder.renderingContext); - visitor = DefaultConditionVisitor.withColumn(column) - .withRenderingContext(renderingContext) - .build(); } public FragmentAndParameters render() { FragmentCollector fc = new FragmentCollector(); - fc.add(renderLeftColumn()); - fc.add(condition.accept(visitor)); + fc.add(condition.renderLeftColumn(renderingContext, column)); + fc.add(condition.renderCondition(renderingContext, column)); return fc.toFragmentAndParameters(Collectors.joining(" ")); //$NON-NLS-1$ } - private FragmentAndParameters renderLeftColumn() { - return column.alias() - .map(FragmentAndParameters::fromFragment) - .orElseGet(() -> column.render(renderingContext)) - .mapFragment(condition::overrideRenderedLeftColumn); - } - public static class Builder { private @Nullable BindableColumn column; private @Nullable VisitableCondition condition; diff --git a/src/main/java/org/mybatis/dynamic/sql/where/render/DefaultConditionVisitor.java b/src/main/java/org/mybatis/dynamic/sql/where/render/DefaultConditionVisitor.java deleted file mode 100644 index 78bbc81e7..000000000 --- a/src/main/java/org/mybatis/dynamic/sql/where/render/DefaultConditionVisitor.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright 2016-2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.mybatis.dynamic.sql.where.render; - -import static org.mybatis.dynamic.sql.util.StringUtilities.spaceBefore; - -import java.util.Objects; -import java.util.stream.Collectors; - -import org.jspecify.annotations.Nullable; -import org.mybatis.dynamic.sql.AbstractColumnComparisonCondition; -import org.mybatis.dynamic.sql.AbstractListValueCondition; -import org.mybatis.dynamic.sql.AbstractNoValueCondition; -import org.mybatis.dynamic.sql.AbstractSingleValueCondition; -import org.mybatis.dynamic.sql.AbstractSubselectCondition; -import org.mybatis.dynamic.sql.AbstractTwoValueCondition; -import org.mybatis.dynamic.sql.BindableColumn; -import org.mybatis.dynamic.sql.ConditionVisitor; -import org.mybatis.dynamic.sql.render.RenderedParameterInfo; -import org.mybatis.dynamic.sql.render.RenderingContext; -import org.mybatis.dynamic.sql.select.render.SubQueryRenderer; -import org.mybatis.dynamic.sql.util.FragmentAndParameters; -import org.mybatis.dynamic.sql.util.FragmentCollector; - -public class DefaultConditionVisitor implements ConditionVisitor { - - private final BindableColumn column; - private final RenderingContext renderingContext; - - private DefaultConditionVisitor(Builder builder) { - column = Objects.requireNonNull(builder.column); - renderingContext = Objects.requireNonNull(builder.renderingContext); - } - - @Override - public FragmentAndParameters visit(AbstractListValueCondition condition) { - return condition.values().map(this::toFragmentAndParameters) - .collect(FragmentCollector.collect()) - .toFragmentAndParameters(Collectors.joining(",", //$NON-NLS-1$ - condition.operator() + " (", ")")); //$NON-NLS-1$ //$NON-NLS-2$ - } - - @Override - public FragmentAndParameters visit(AbstractNoValueCondition condition) { - return FragmentAndParameters.fromFragment(condition.operator()); - } - - @Override - public FragmentAndParameters visit(AbstractSingleValueCondition condition) { - RenderedParameterInfo parameterInfo = renderingContext.calculateParameterInfo(column); - String finalFragment = condition.operator() - + spaceBefore(parameterInfo.renderedPlaceHolder()); - - return FragmentAndParameters.withFragment(finalFragment) - .withParameter(parameterInfo.parameterMapKey(), convertValue(condition.value())) - .build(); - } - - @Override - public FragmentAndParameters visit(AbstractTwoValueCondition condition) { - RenderedParameterInfo parameterInfo1 = renderingContext.calculateParameterInfo(column); - RenderedParameterInfo parameterInfo2 = renderingContext.calculateParameterInfo(column); - - String finalFragment = condition.operator1() - + spaceBefore(parameterInfo1.renderedPlaceHolder()) - + spaceBefore(condition.operator2()) - + spaceBefore(parameterInfo2.renderedPlaceHolder()); - - return FragmentAndParameters.withFragment(finalFragment) - .withParameter(parameterInfo1.parameterMapKey(), convertValue(condition.value1())) - .withParameter(parameterInfo2.parameterMapKey(), convertValue(condition.value2())) - .build(); - } - - @Override - public FragmentAndParameters visit(AbstractSubselectCondition condition) { - return SubQueryRenderer.withSelectModel(condition.selectModel()) - .withRenderingContext(renderingContext) - .withPrefix(condition.operator() + " (") //$NON-NLS-1$ - .withSuffix(")") //$NON-NLS-1$ - .build() - .render(); - } - - @Override - public FragmentAndParameters visit(AbstractColumnComparisonCondition condition) { - return condition.rightColumn().render(renderingContext) - .mapFragment(f -> condition.operator() + spaceBefore(f)); - } - - private @Nullable Object convertValue(T value) { - return column.convertParameterType(value); - } - - private FragmentAndParameters toFragmentAndParameters(T value) { - RenderedParameterInfo parameterInfo = renderingContext.calculateParameterInfo(column); - return FragmentAndParameters.withFragment(parameterInfo.renderedPlaceHolder()) - .withParameter(parameterInfo.parameterMapKey(), convertValue(value)) - .build(); - } - - public static Builder withColumn(BindableColumn column) { - return new Builder().withColumn(column); - } - - public static class Builder { - private @Nullable BindableColumn column; - private @Nullable RenderingContext renderingContext; - - public Builder withColumn(BindableColumn column) { - this.column = column; - return this; - } - - public Builder withRenderingContext(RenderingContext renderingContext) { - this.renderingContext = renderingContext; - return this; - } - - public DefaultConditionVisitor build() { - return new DefaultConditionVisitor<>(this); - } - } -} diff --git a/src/test/java/examples/mysql/IsLikeEscape.java b/src/test/java/examples/mysql/IsLikeEscape.java new file mode 100644 index 000000000..28e3045c2 --- /dev/null +++ b/src/test/java/examples/mysql/IsLikeEscape.java @@ -0,0 +1,71 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package examples.mysql; + +import java.util.NoSuchElementException; +import java.util.function.Function; + +import org.jspecify.annotations.NullMarked; +import org.mybatis.dynamic.sql.BindableColumn; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; +import org.mybatis.dynamic.sql.where.condition.IsLike; + +@NullMarked +public class IsLikeEscape extends IsLike { + private static final IsLikeEscape EMPTY = new IsLikeEscape(-1, "") { + @Override + public Object value() { + throw new NoSuchElementException("No value present"); //$NON-NLS-1$ + } + + @Override + public boolean isEmpty() { + return true; + } + }; + + public static IsLikeEscape empty() { + @SuppressWarnings("unchecked") + IsLikeEscape t = (IsLikeEscape) EMPTY; + return t; + } + + private final String escapeString; + + protected IsLikeEscape(T value, String escapeString) { + super(value); + this.escapeString = escapeString; + } + + @Override + public FragmentAndParameters renderCondition(RenderingContext renderingContext, BindableColumn leftColumn) { + return super.renderCondition(renderingContext, leftColumn).mapFragment(this::addEscape); + } + + private String addEscape(String s) { + return s + " ESCAPE '" + escapeString + "'"; + } + + @Override + public IsLike map(Function mapper) { + return mapSupport(mapper, v -> new IsLikeEscape<>(v, escapeString), IsLikeEscape::empty); + } + + public static IsLikeEscape isLike(T value, String escapeString) { + return new IsLikeEscape<>(value, escapeString); + } +} diff --git a/src/test/java/examples/mysql/MySQLTest.java b/src/test/java/examples/mysql/MySQLTest.java index 54c43c34a..20dd9fd39 100644 --- a/src/test/java/examples/mysql/MySQLTest.java +++ b/src/test/java/examples/mysql/MySQLTest.java @@ -119,4 +119,25 @@ void testMemberOfAsFunction() { assertThat(rows.get(2)).containsOnly(entry("id", 3), entry("inList", 1L)); } } + + @Test + void testIsLikeEscape() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(id, description) + .from(items) + .where(description, IsLikeEscape.isLike("Item 1%", "#").map(s -> s)) + .orderBy(id) + .build() + .render(RenderingStrategies.MYBATIS3); + + assertThat(selectStatement.getSelectStatement()) + .isEqualTo("select id, description from items where description like #{parameters.p1,jdbcType=VARCHAR} ESCAPE '#' order by id"); + + List> rows = mapper.selectManyMappedRows(selectStatement); + assertThat(rows).hasSize(11); + assertThat(rows.get(2)).containsOnly(entry("id", 11), entry("description", "Item 11")); + } + } } From 0cfecd75f30a46898b5cab77d566f436830f51b3 Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Wed, 5 Mar 2025 15:53:31 -0500 Subject: [PATCH 2/6] Rename VisitableCondition to RenderableCondition Conditions are no longer rendered via a Visitor, so the new name makes more sense --- .../dynamic/sql/RenderableCondition.java | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/main/java/org/mybatis/dynamic/sql/RenderableCondition.java diff --git a/src/main/java/org/mybatis/dynamic/sql/RenderableCondition.java b/src/main/java/org/mybatis/dynamic/sql/RenderableCondition.java new file mode 100644 index 000000000..4ca7a17f2 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/RenderableCondition.java @@ -0,0 +1,56 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mybatis.dynamic.sql; + +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; + +@FunctionalInterface +public interface RenderableCondition { + FragmentAndParameters renderCondition(RenderingContext renderingContext, BindableColumn leftColumn); + + default FragmentAndParameters renderLeftColumn(RenderingContext renderingContext, BindableColumn leftColumn) { + return leftColumn.alias() + .map(FragmentAndParameters::fromFragment) + .orElseGet(() -> leftColumn.render(renderingContext)); + } + + /** + * Subclasses can override this to inform the renderer if the condition should not be included + * in the rendered SQL. Typically, conditions will not render if they are empty. + * + * @return true if the condition should render. + */ + default boolean shouldRender(RenderingContext renderingContext) { + return !isEmpty(); + } + + /** + * Subclasses can override this to indicate whether the condition is considered empty. This is primarily used in + * map and filter operations - the map and filter functions will not be applied if the condition is empty. + * + * @return true if the condition is empty. + */ + default boolean isEmpty() { + return false; + } + + /** + * This method will be called during rendering when {@link RenderableCondition#shouldRender(RenderingContext)} + * returns false. + */ + default void renderingSkipped() {} +} From bc4becaebee9ba07ef3f525e9537e6cdf05e9aec Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Wed, 5 Mar 2025 15:54:21 -0500 Subject: [PATCH 3/6] Rename VisitableCondition to RenderableCondition Conditions are no longer rendered via a Visitor, so the new name makes more sense --- CHANGELOG.md | 4 ++ .../AbstractColumnComparisonCondition.java | 2 +- .../sql/AbstractListValueCondition.java | 2 +- .../dynamic/sql/AbstractNoValueCondition.java | 2 +- .../sql/AbstractSingleValueCondition.java | 2 +- .../sql/AbstractSubselectCondition.java | 2 +- .../sql/AbstractTwoValueCondition.java | 2 +- .../sql/ColumnAndConditionCriterion.java | 8 +-- .../org/mybatis/dynamic/sql/SqlBuilder.java | 20 +++---- .../dynamic/sql/VisitableCondition.java | 54 ++++++------------- .../common/AbstractBooleanExpressionDSL.java | 12 ++--- .../sql/select/AbstractHavingStarter.java | 6 +-- .../sql/select/QueryExpressionDSL.java | 11 ++-- .../dynamic/sql/select/aggregate/Sum.java | 6 +-- .../ConditionBasedWhenCondition.java | 8 +-- .../caseexpression/SearchedCaseDSL.java | 6 +-- .../select/caseexpression/SimpleCaseDSL.java | 16 +++--- .../SimpleCaseWhenConditionRenderer.java | 6 +-- .../sql/where/AbstractWhereStarter.java | 6 +-- .../CaseInsensitiveVisitableCondition.java | 6 +-- .../render/ColumnAndConditionRenderer.java | 8 +-- .../util/kotlin/GroupingCriteriaCollector.kt | 4 +- .../dynamic/sql/util/kotlin/JoinCollector.kt | 4 +- .../sql/util/kotlin/elements/CaseDSLs.kt | 4 +- .../sql/util/kotlin/elements/SqlElements.kt | 4 +- 25 files changed, 95 insertions(+), 110 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c12f44da..8b333e3ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,6 +100,10 @@ Runtime behavior changes: these concepts for different databases it simply adds known clauses to a generated SQL statement. You should always test to make sure these functions work in your target database. Currently, we support, and test, the options supported by PostgreSQL. +- Rendering for all the conditions (isEqualTo, etc.) has changed. This should be transparent to most users unless you + have coded a direct implementation of `VisitableCondition`. The change makes it easier to code custom conditions that + are not supported by the library out of the box. The statement renderers now call methods `renderCondition` and + `renderLeftColumn` that you can override to implement any rendering you need. ## Release 1.5.2 - June 3, 2024 diff --git a/src/main/java/org/mybatis/dynamic/sql/AbstractColumnComparisonCondition.java b/src/main/java/org/mybatis/dynamic/sql/AbstractColumnComparisonCondition.java index 502025401..65e45aafe 100644 --- a/src/main/java/org/mybatis/dynamic/sql/AbstractColumnComparisonCondition.java +++ b/src/main/java/org/mybatis/dynamic/sql/AbstractColumnComparisonCondition.java @@ -20,7 +20,7 @@ import org.mybatis.dynamic.sql.render.RenderingContext; import org.mybatis.dynamic.sql.util.FragmentAndParameters; -public abstract class AbstractColumnComparisonCondition implements VisitableCondition { +public abstract class AbstractColumnComparisonCondition implements RenderableCondition { protected final BasicColumn rightColumn; diff --git a/src/main/java/org/mybatis/dynamic/sql/AbstractListValueCondition.java b/src/main/java/org/mybatis/dynamic/sql/AbstractListValueCondition.java index eeed434ab..23f48e6f4 100644 --- a/src/main/java/org/mybatis/dynamic/sql/AbstractListValueCondition.java +++ b/src/main/java/org/mybatis/dynamic/sql/AbstractListValueCondition.java @@ -28,7 +28,7 @@ import org.mybatis.dynamic.sql.util.FragmentAndParameters; import org.mybatis.dynamic.sql.util.FragmentCollector; -public abstract class AbstractListValueCondition implements VisitableCondition { +public abstract class AbstractListValueCondition implements RenderableCondition { protected final Collection values; protected AbstractListValueCondition(Collection values) { diff --git a/src/main/java/org/mybatis/dynamic/sql/AbstractNoValueCondition.java b/src/main/java/org/mybatis/dynamic/sql/AbstractNoValueCondition.java index 02f6e25f4..c0e103baf 100644 --- a/src/main/java/org/mybatis/dynamic/sql/AbstractNoValueCondition.java +++ b/src/main/java/org/mybatis/dynamic/sql/AbstractNoValueCondition.java @@ -21,7 +21,7 @@ import org.mybatis.dynamic.sql.render.RenderingContext; import org.mybatis.dynamic.sql.util.FragmentAndParameters; -public abstract class AbstractNoValueCondition implements VisitableCondition { +public abstract class AbstractNoValueCondition implements RenderableCondition { protected > S filterSupport(BooleanSupplier booleanSupplier, Supplier emptySupplier, S self) { diff --git a/src/main/java/org/mybatis/dynamic/sql/AbstractSingleValueCondition.java b/src/main/java/org/mybatis/dynamic/sql/AbstractSingleValueCondition.java index a361c4ed0..9c8248d02 100644 --- a/src/main/java/org/mybatis/dynamic/sql/AbstractSingleValueCondition.java +++ b/src/main/java/org/mybatis/dynamic/sql/AbstractSingleValueCondition.java @@ -25,7 +25,7 @@ import org.mybatis.dynamic.sql.render.RenderingContext; import org.mybatis.dynamic.sql.util.FragmentAndParameters; -public abstract class AbstractSingleValueCondition implements VisitableCondition { +public abstract class AbstractSingleValueCondition implements RenderableCondition { protected final T value; protected AbstractSingleValueCondition(T value) { diff --git a/src/main/java/org/mybatis/dynamic/sql/AbstractSubselectCondition.java b/src/main/java/org/mybatis/dynamic/sql/AbstractSubselectCondition.java index 3254fbd2d..dcfbd4b3c 100644 --- a/src/main/java/org/mybatis/dynamic/sql/AbstractSubselectCondition.java +++ b/src/main/java/org/mybatis/dynamic/sql/AbstractSubselectCondition.java @@ -21,7 +21,7 @@ import org.mybatis.dynamic.sql.util.Buildable; import org.mybatis.dynamic.sql.util.FragmentAndParameters; -public abstract class AbstractSubselectCondition implements VisitableCondition { +public abstract class AbstractSubselectCondition implements RenderableCondition { private final SelectModel selectModel; protected AbstractSubselectCondition(Buildable selectModelBuilder) { diff --git a/src/main/java/org/mybatis/dynamic/sql/AbstractTwoValueCondition.java b/src/main/java/org/mybatis/dynamic/sql/AbstractTwoValueCondition.java index fe34333a2..865f7db0b 100644 --- a/src/main/java/org/mybatis/dynamic/sql/AbstractTwoValueCondition.java +++ b/src/main/java/org/mybatis/dynamic/sql/AbstractTwoValueCondition.java @@ -27,7 +27,7 @@ import org.mybatis.dynamic.sql.render.RenderingContext; import org.mybatis.dynamic.sql.util.FragmentAndParameters; -public abstract class AbstractTwoValueCondition implements VisitableCondition { +public abstract class AbstractTwoValueCondition implements RenderableCondition { protected final T value1; protected final T value2; diff --git a/src/main/java/org/mybatis/dynamic/sql/ColumnAndConditionCriterion.java b/src/main/java/org/mybatis/dynamic/sql/ColumnAndConditionCriterion.java index 646e695c2..053c18f64 100644 --- a/src/main/java/org/mybatis/dynamic/sql/ColumnAndConditionCriterion.java +++ b/src/main/java/org/mybatis/dynamic/sql/ColumnAndConditionCriterion.java @@ -21,7 +21,7 @@ public class ColumnAndConditionCriterion extends SqlCriterion { private final BindableColumn column; - private final VisitableCondition condition; + private final RenderableCondition condition; private ColumnAndConditionCriterion(Builder builder) { super(builder); @@ -33,7 +33,7 @@ public BindableColumn column() { return column; } - public VisitableCondition condition() { + public RenderableCondition condition() { return condition; } @@ -48,14 +48,14 @@ public static Builder withColumn(BindableColumn column) { public static class Builder extends AbstractBuilder> { private @Nullable BindableColumn column; - private @Nullable VisitableCondition condition; + private @Nullable RenderableCondition condition; public Builder withColumn(BindableColumn column) { this.column = column; return this; } - public Builder withCondition(VisitableCondition condition) { + public Builder withCondition(RenderableCondition condition) { this.condition = condition; return this; } diff --git a/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java index 8b36417fe..1e27db93d 100644 --- a/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java +++ b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java @@ -250,7 +250,7 @@ static WhereDSL.StandaloneWhereFinisher where() { return new WhereDSL().where(); } - static WhereDSL.StandaloneWhereFinisher where(BindableColumn column, VisitableCondition condition, + static WhereDSL.StandaloneWhereFinisher where(BindableColumn column, RenderableCondition condition, AndOrCriteriaGroup... subCriteria) { return new WhereDSL().where(column, condition, subCriteria); } @@ -263,7 +263,7 @@ static WhereDSL.StandaloneWhereFinisher where(ExistsPredicate existsPredicate, A return new WhereDSL().where(existsPredicate, subCriteria); } - static HavingDSL.StandaloneHavingFinisher having(BindableColumn column, VisitableCondition condition, + static HavingDSL.StandaloneHavingFinisher having(BindableColumn column, RenderableCondition condition, AndOrCriteriaGroup... subCriteria) { return new HavingDSL().having(column, condition, subCriteria); } @@ -273,12 +273,12 @@ static HavingDSL.StandaloneHavingFinisher having(SqlCriterion initialCriterion, } // where condition connectors - static CriteriaGroup group(BindableColumn column, VisitableCondition condition, + static CriteriaGroup group(BindableColumn column, RenderableCondition condition, AndOrCriteriaGroup... subCriteria) { return group(column, condition, Arrays.asList(subCriteria)); } - static CriteriaGroup group(BindableColumn column, VisitableCondition condition, + static CriteriaGroup group(BindableColumn column, RenderableCondition condition, List subCriteria) { return new CriteriaGroup.Builder() .withInitialCriterion(new ColumnAndConditionCriterion.Builder().withColumn(column) @@ -316,12 +316,12 @@ static CriteriaGroup group(List subCriteria) { .build(); } - static NotCriterion not(BindableColumn column, VisitableCondition condition, + static NotCriterion not(BindableColumn column, RenderableCondition condition, AndOrCriteriaGroup... subCriteria) { return not(column, condition, Arrays.asList(subCriteria)); } - static NotCriterion not(BindableColumn column, VisitableCondition condition, + static NotCriterion not(BindableColumn column, RenderableCondition condition, List subCriteria) { return new NotCriterion.Builder() .withInitialCriterion(new ColumnAndConditionCriterion.Builder().withColumn(column) @@ -359,7 +359,7 @@ static NotCriterion not(List subCriteria) { .build(); } - static AndOrCriteriaGroup or(BindableColumn column, VisitableCondition condition, + static AndOrCriteriaGroup or(BindableColumn column, RenderableCondition condition, AndOrCriteriaGroup... subCriteria) { return new AndOrCriteriaGroup.Builder() .withInitialCriterion(ColumnAndConditionCriterion.withColumn(column) @@ -394,7 +394,7 @@ static AndOrCriteriaGroup or(List subCriteria) { .build(); } - static AndOrCriteriaGroup and(BindableColumn column, VisitableCondition condition, + static AndOrCriteriaGroup and(BindableColumn column, RenderableCondition condition, AndOrCriteriaGroup... subCriteria) { return new AndOrCriteriaGroup.Builder() .withInitialCriterion(ColumnAndConditionCriterion.withColumn(column) @@ -430,7 +430,7 @@ static AndOrCriteriaGroup and(List subCriteria) { } // join support - static ColumnAndConditionCriterion on(BindableColumn joinColumn, VisitableCondition joinCondition) { + static ColumnAndConditionCriterion on(BindableColumn joinColumn, RenderableCondition joinCondition) { return ColumnAndConditionCriterion.withColumn(joinColumn) .withCondition(joinCondition) .build(); @@ -506,7 +506,7 @@ static Sum sum(BasicColumn column) { return Sum.of(column); } - static Sum sum(BindableColumn column, VisitableCondition condition) { + static Sum sum(BindableColumn column, RenderableCondition condition) { return Sum.of(column, condition); } diff --git a/src/main/java/org/mybatis/dynamic/sql/VisitableCondition.java b/src/main/java/org/mybatis/dynamic/sql/VisitableCondition.java index d201ae4dd..9969c3997 100644 --- a/src/main/java/org/mybatis/dynamic/sql/VisitableCondition.java +++ b/src/main/java/org/mybatis/dynamic/sql/VisitableCondition.java @@ -16,41 +16,21 @@ package org.mybatis.dynamic.sql; import org.mybatis.dynamic.sql.render.RenderingContext; -import org.mybatis.dynamic.sql.util.FragmentAndParameters; -@FunctionalInterface -public interface VisitableCondition { - FragmentAndParameters renderCondition(RenderingContext renderingContext, BindableColumn leftColumn); - - default FragmentAndParameters renderLeftColumn(RenderingContext renderingContext, BindableColumn leftColumn) { - return leftColumn.alias() - .map(FragmentAndParameters::fromFragment) - .orElseGet(() -> leftColumn.render(renderingContext)); - } - - /** - * Subclasses can override this to inform the renderer if the condition should not be included - * in the rendered SQL. Typically, conditions will not render if they are empty. - * - * @return true if the condition should render. - */ - default boolean shouldRender(RenderingContext renderingContext) { - return !isEmpty(); - } - - /** - * Subclasses can override this to indicate whether the condition is considered empty. This is primarily used in - * map and filter operations - the map and filter functions will not be applied if the condition is empty. - * - * @return true if the condition is empty. - */ - default boolean isEmpty() { - return false; - } - - /** - * This method will be called during rendering when {@link VisitableCondition#shouldRender(RenderingContext)} - * returns false. - */ - default void renderingSkipped() {} -} +/** + * Deprecated interface. + * + *

Conditions are no longer rendered with a visitor, so the name is misleading. This change makes it far easier + * to implement custom conditions for functionality not supplied out of the box by the library. + * + *

If you created any direct implementations of this interface, you will need to change the rendering functions. + * The library now calls {@link RenderableCondition#renderCondition(RenderingContext, BindableColumn)} and + * {@link RenderableCondition#renderLeftColumn(RenderingContext, BindableColumn)} instead of the previous methods + * like operator, value, etc. Subclasses of the supplied abstract conditions should continue + * to function as before. + * + * @param the Java type related to the column this condition relates to. Used primarily for compiler type checking + * @deprecated since 2.0.0. Please use {@link RenderableCondition} instead. + */ +@Deprecated(since = "2.0.0", forRemoval = true) +public interface VisitableCondition extends RenderableCondition { } diff --git a/src/main/java/org/mybatis/dynamic/sql/common/AbstractBooleanExpressionDSL.java b/src/main/java/org/mybatis/dynamic/sql/common/AbstractBooleanExpressionDSL.java index c671b39a9..2f817fb5f 100644 --- a/src/main/java/org/mybatis/dynamic/sql/common/AbstractBooleanExpressionDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/common/AbstractBooleanExpressionDSL.java @@ -26,20 +26,20 @@ import org.mybatis.dynamic.sql.CriteriaGroup; import org.mybatis.dynamic.sql.ExistsCriterion; import org.mybatis.dynamic.sql.ExistsPredicate; +import org.mybatis.dynamic.sql.RenderableCondition; import org.mybatis.dynamic.sql.SqlCriterion; -import org.mybatis.dynamic.sql.VisitableCondition; import org.mybatis.dynamic.sql.util.Validator; public abstract class AbstractBooleanExpressionDSL> { private @Nullable SqlCriterion initialCriterion; protected final List subCriteria = new ArrayList<>(); - public T and(BindableColumn column, VisitableCondition condition, + public T and(BindableColumn column, RenderableCondition condition, AndOrCriteriaGroup... subCriteria) { return and(column, condition, Arrays.asList(subCriteria)); } - public T and(BindableColumn column, VisitableCondition condition, + public T and(BindableColumn column, RenderableCondition condition, List subCriteria) { addSubCriteria("and", buildCriterion(column, condition), subCriteria); //$NON-NLS-1$ return getThis(); @@ -68,12 +68,12 @@ public T and(List criteria) { return getThis(); } - public T or(BindableColumn column, VisitableCondition condition, + public T or(BindableColumn column, RenderableCondition condition, AndOrCriteriaGroup... subCriteria) { return or(column, condition, Arrays.asList(subCriteria)); } - public T or(BindableColumn column, VisitableCondition condition, + public T or(BindableColumn column, RenderableCondition condition, List subCriteria) { addSubCriteria("or", buildCriterion(column, condition), subCriteria); //$NON-NLS-1$ return getThis(); @@ -102,7 +102,7 @@ public T or(List criteria) { return getThis(); } - private SqlCriterion buildCriterion(BindableColumn column, VisitableCondition condition) { + private SqlCriterion buildCriterion(BindableColumn column, RenderableCondition condition) { return ColumnAndConditionCriterion.withColumn(column).withCondition(condition).build(); } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/AbstractHavingStarter.java b/src/main/java/org/mybatis/dynamic/sql/select/AbstractHavingStarter.java index c8375bbd3..1090fb064 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/AbstractHavingStarter.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/AbstractHavingStarter.java @@ -22,17 +22,17 @@ import org.mybatis.dynamic.sql.BindableColumn; import org.mybatis.dynamic.sql.ColumnAndConditionCriterion; import org.mybatis.dynamic.sql.CriteriaGroup; +import org.mybatis.dynamic.sql.RenderableCondition; import org.mybatis.dynamic.sql.SqlCriterion; -import org.mybatis.dynamic.sql.VisitableCondition; public interface AbstractHavingStarter> { - default F having(BindableColumn column, VisitableCondition condition, + default F having(BindableColumn column, RenderableCondition condition, AndOrCriteriaGroup... subCriteria) { return having(column, condition, Arrays.asList(subCriteria)); } - default F having(BindableColumn column, VisitableCondition condition, + default F having(BindableColumn column, RenderableCondition condition, List subCriteria) { SqlCriterion sqlCriterion = ColumnAndConditionCriterion.withColumn(column) .withCondition(condition) diff --git a/src/main/java/org/mybatis/dynamic/sql/select/QueryExpressionDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/QueryExpressionDSL.java index b61f5e98a..70ad2617d 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/QueryExpressionDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/QueryExpressionDSL.java @@ -28,10 +28,10 @@ import org.mybatis.dynamic.sql.BindableColumn; import org.mybatis.dynamic.sql.ColumnAndConditionCriterion; import org.mybatis.dynamic.sql.CriteriaGroup; +import org.mybatis.dynamic.sql.RenderableCondition; import org.mybatis.dynamic.sql.SortSpecification; import org.mybatis.dynamic.sql.SqlTable; import org.mybatis.dynamic.sql.TableExpression; -import org.mybatis.dynamic.sql.VisitableCondition; import org.mybatis.dynamic.sql.common.AbstractBooleanExpressionDSL; import org.mybatis.dynamic.sql.configuration.StatementConfiguration; import org.mybatis.dynamic.sql.select.join.JoinSpecification; @@ -324,11 +324,11 @@ public JoinSpecificationStarter(TableExpression joinTable, JoinType joinType) { this.joinType = joinType; } - public JoinSpecificationFinisher on(BindableColumn joinColumn, VisitableCondition joinCondition) { + public JoinSpecificationFinisher on(BindableColumn joinColumn, RenderableCondition joinCondition) { return new JoinSpecificationFinisher(joinTable, joinColumn, joinCondition, joinType); } - public JoinSpecificationFinisher on(BindableColumn joinColumn, VisitableCondition onJoinCondition, + public JoinSpecificationFinisher on(BindableColumn joinColumn, RenderableCondition onJoinCondition, AndOrCriteriaGroup... subCriteria) { return new JoinSpecificationFinisher(joinTable, joinColumn, onJoinCondition, joinType, subCriteria); } @@ -343,7 +343,7 @@ public class JoinSpecificationFinisher private final JoinType joinType; public JoinSpecificationFinisher(TableExpression table, BindableColumn joinColumn, - VisitableCondition joinCondition, JoinType joinType) { + RenderableCondition joinCondition, JoinType joinType) { this.table = table; this.joinType = joinType; addJoinSpecificationSupplier(this::buildJoinSpecification); @@ -356,7 +356,8 @@ public JoinSpecificationFinisher(TableExpression table, BindableColumn jo } public JoinSpecificationFinisher(TableExpression table, BindableColumn joinColumn, - VisitableCondition joinCondition, JoinType joinType, AndOrCriteriaGroup... subCriteria) { + RenderableCondition joinCondition, JoinType joinType, + AndOrCriteriaGroup... subCriteria) { this.table = table; this.joinType = joinType; addJoinSpecificationSupplier(this::buildJoinSpecification); diff --git a/src/main/java/org/mybatis/dynamic/sql/select/aggregate/Sum.java b/src/main/java/org/mybatis/dynamic/sql/select/aggregate/Sum.java index 6b9cd657e..7ccfd7854 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/aggregate/Sum.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/aggregate/Sum.java @@ -19,7 +19,7 @@ import org.mybatis.dynamic.sql.BasicColumn; import org.mybatis.dynamic.sql.BindableColumn; -import org.mybatis.dynamic.sql.VisitableCondition; +import org.mybatis.dynamic.sql.RenderableCondition; import org.mybatis.dynamic.sql.render.RenderingContext; import org.mybatis.dynamic.sql.select.function.AbstractUniTypeFunction; import org.mybatis.dynamic.sql.util.FragmentAndParameters; @@ -34,7 +34,7 @@ private Sum(BasicColumn column) { renderer = rc -> column.render(rc).mapFragment(this::applyAggregate); } - private Sum(BindableColumn column, VisitableCondition condition) { + private Sum(BindableColumn column, RenderableCondition condition) { super(column); renderer = rc -> { Validator.assertTrue(condition.shouldRender(rc), "ERROR.37", "sum"); //$NON-NLS-1$ //$NON-NLS-2$ @@ -76,7 +76,7 @@ public static Sum of(BasicColumn column) { return new Sum<>(column); } - public static Sum of(BindableColumn column, VisitableCondition condition) { + public static Sum of(BindableColumn column, RenderableCondition condition) { return new Sum<>(column, condition); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/ConditionBasedWhenCondition.java b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/ConditionBasedWhenCondition.java index fe3178bff..78f841b1d 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/ConditionBasedWhenCondition.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/ConditionBasedWhenCondition.java @@ -20,17 +20,17 @@ import java.util.stream.Stream; import org.mybatis.dynamic.sql.BasicColumn; -import org.mybatis.dynamic.sql.VisitableCondition; +import org.mybatis.dynamic.sql.RenderableCondition; public class ConditionBasedWhenCondition extends SimpleCaseWhenCondition { - private final List> conditions = new ArrayList<>(); + private final List> conditions = new ArrayList<>(); - public ConditionBasedWhenCondition(List> conditions, BasicColumn thenValue) { + public ConditionBasedWhenCondition(List> conditions, BasicColumn thenValue) { super(thenValue); this.conditions.addAll(conditions); } - public Stream> conditions() { + public Stream> conditions() { return conditions.stream(); } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SearchedCaseDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SearchedCaseDSL.java index 0c0c6ea87..c86bf7ff0 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SearchedCaseDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SearchedCaseDSL.java @@ -25,20 +25,20 @@ import org.mybatis.dynamic.sql.BindableColumn; import org.mybatis.dynamic.sql.ColumnAndConditionCriterion; import org.mybatis.dynamic.sql.CriteriaGroup; +import org.mybatis.dynamic.sql.RenderableCondition; import org.mybatis.dynamic.sql.SqlCriterion; -import org.mybatis.dynamic.sql.VisitableCondition; import org.mybatis.dynamic.sql.common.AbstractBooleanExpressionDSL; public class SearchedCaseDSL implements ElseDSL { private final List whenConditions = new ArrayList<>(); private @Nullable BasicColumn elseValue; - public WhenDSL when(BindableColumn column, VisitableCondition condition, + public WhenDSL when(BindableColumn column, RenderableCondition condition, AndOrCriteriaGroup... subCriteria) { return when(column, condition, Arrays.asList(subCriteria)); } - public WhenDSL when(BindableColumn column, VisitableCondition condition, + public WhenDSL when(BindableColumn column, RenderableCondition condition, List subCriteria) { SqlCriterion sqlCriterion = ColumnAndConditionCriterion.withColumn(column) .withCondition(condition) diff --git a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseDSL.java index 488f20060..be2e1e908 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseDSL.java @@ -23,7 +23,7 @@ import org.jspecify.annotations.Nullable; import org.mybatis.dynamic.sql.BasicColumn; import org.mybatis.dynamic.sql.BindableColumn; -import org.mybatis.dynamic.sql.VisitableCondition; +import org.mybatis.dynamic.sql.RenderableCondition; public class SimpleCaseDSL implements ElseDSL.SimpleCaseEnder> { private final BindableColumn column; @@ -35,13 +35,13 @@ private SimpleCaseDSL(BindableColumn column) { } @SafeVarargs - public final ConditionBasedWhenFinisher when(VisitableCondition condition, - VisitableCondition... subsequentConditions) { + public final ConditionBasedWhenFinisher when(RenderableCondition condition, + RenderableCondition... subsequentConditions) { return when(condition, Arrays.asList(subsequentConditions)); } - public ConditionBasedWhenFinisher when(VisitableCondition condition, - List> subsequentConditions) { + public ConditionBasedWhenFinisher when(RenderableCondition condition, + List> subsequentConditions) { return new ConditionBasedWhenFinisher(condition, subsequentConditions); } @@ -70,10 +70,10 @@ public SimpleCaseModel end() { } public class ConditionBasedWhenFinisher implements ThenDSL> { - private final List> conditions = new ArrayList<>(); + private final List> conditions = new ArrayList<>(); - private ConditionBasedWhenFinisher(VisitableCondition condition, - List> subsequentConditions) { + private ConditionBasedWhenFinisher(RenderableCondition condition, + List> subsequentConditions) { conditions.add(condition); conditions.addAll(subsequentConditions); } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseWhenConditionRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseWhenConditionRenderer.java index cb13434d3..1bb6e2adc 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseWhenConditionRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseWhenConditionRenderer.java @@ -19,7 +19,7 @@ import java.util.stream.Collectors; import org.mybatis.dynamic.sql.BindableColumn; -import org.mybatis.dynamic.sql.VisitableCondition; +import org.mybatis.dynamic.sql.RenderableCondition; import org.mybatis.dynamic.sql.render.RenderedParameterInfo; import org.mybatis.dynamic.sql.render.RenderingContext; import org.mybatis.dynamic.sql.select.caseexpression.BasicWhenCondition; @@ -57,11 +57,11 @@ public FragmentAndParameters visit(BasicWhenCondition whenCondition) { .toFragmentAndParameters(Collectors.joining(", ")); //$NON-NLS-1$ } - private boolean shouldRender(VisitableCondition condition) { + private boolean shouldRender(RenderableCondition condition) { return condition.shouldRender(renderingContext); } - private FragmentAndParameters renderCondition(VisitableCondition condition) { + private FragmentAndParameters renderCondition(RenderableCondition condition) { return condition.renderCondition(renderingContext, column); } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/AbstractWhereStarter.java b/src/main/java/org/mybatis/dynamic/sql/where/AbstractWhereStarter.java index 3a037e5e9..17efa091e 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/AbstractWhereStarter.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/AbstractWhereStarter.java @@ -25,8 +25,8 @@ import org.mybatis.dynamic.sql.CriteriaGroup; import org.mybatis.dynamic.sql.ExistsCriterion; import org.mybatis.dynamic.sql.ExistsPredicate; +import org.mybatis.dynamic.sql.RenderableCondition; import org.mybatis.dynamic.sql.SqlCriterion; -import org.mybatis.dynamic.sql.VisitableCondition; import org.mybatis.dynamic.sql.util.ConfigurableStatement; /** @@ -39,11 +39,11 @@ public interface AbstractWhereStarter, D extends AbstractWhereStarter> extends ConfigurableStatement { - default F where(BindableColumn column, VisitableCondition condition, AndOrCriteriaGroup... subCriteria) { + default F where(BindableColumn column, RenderableCondition condition, AndOrCriteriaGroup... subCriteria) { return where(column, condition, Arrays.asList(subCriteria)); } - default F where(BindableColumn column, VisitableCondition condition, + default F where(BindableColumn column, RenderableCondition condition, List subCriteria) { SqlCriterion sqlCriterion = ColumnAndConditionCriterion.withColumn(column) .withCondition(condition) diff --git a/src/main/java/org/mybatis/dynamic/sql/where/condition/CaseInsensitiveVisitableCondition.java b/src/main/java/org/mybatis/dynamic/sql/where/condition/CaseInsensitiveVisitableCondition.java index 9315b68fa..9224ad594 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/condition/CaseInsensitiveVisitableCondition.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/condition/CaseInsensitiveVisitableCondition.java @@ -16,16 +16,16 @@ package org.mybatis.dynamic.sql.where.condition; import org.mybatis.dynamic.sql.BindableColumn; -import org.mybatis.dynamic.sql.VisitableCondition; +import org.mybatis.dynamic.sql.RenderableCondition; import org.mybatis.dynamic.sql.render.RenderingContext; import org.mybatis.dynamic.sql.util.FragmentAndParameters; -public interface CaseInsensitiveVisitableCondition extends VisitableCondition { +public interface CaseInsensitiveVisitableCondition extends RenderableCondition { @Override default FragmentAndParameters renderLeftColumn(RenderingContext renderingContext, BindableColumn leftColumn) { - return VisitableCondition.super.renderLeftColumn(renderingContext, leftColumn) + return RenderableCondition.super.renderLeftColumn(renderingContext, leftColumn) .mapFragment(s -> "upper(" + s + ")"); //$NON-NLS-1$ //$NON-NLS-2$ } } diff --git a/src/main/java/org/mybatis/dynamic/sql/where/render/ColumnAndConditionRenderer.java b/src/main/java/org/mybatis/dynamic/sql/where/render/ColumnAndConditionRenderer.java index d640c0796..c094bca1b 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/render/ColumnAndConditionRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/render/ColumnAndConditionRenderer.java @@ -20,14 +20,14 @@ import org.jspecify.annotations.Nullable; import org.mybatis.dynamic.sql.BindableColumn; -import org.mybatis.dynamic.sql.VisitableCondition; +import org.mybatis.dynamic.sql.RenderableCondition; import org.mybatis.dynamic.sql.render.RenderingContext; import org.mybatis.dynamic.sql.util.FragmentAndParameters; import org.mybatis.dynamic.sql.util.FragmentCollector; public class ColumnAndConditionRenderer { private final BindableColumn column; - private final VisitableCondition condition; + private final RenderableCondition condition; private final RenderingContext renderingContext; private ColumnAndConditionRenderer(Builder builder) { @@ -45,7 +45,7 @@ public FragmentAndParameters render() { public static class Builder { private @Nullable BindableColumn column; - private @Nullable VisitableCondition condition; + private @Nullable RenderableCondition condition; private @Nullable RenderingContext renderingContext; public Builder withColumn(BindableColumn column) { @@ -53,7 +53,7 @@ public Builder withColumn(BindableColumn column) { return this; } - public Builder withCondition(VisitableCondition condition) { + public Builder withCondition(RenderableCondition condition) { this.condition = condition; return this; } 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 841539eeb..0aaaff460 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 @@ -22,9 +22,9 @@ import org.mybatis.dynamic.sql.ColumnAndConditionCriterion import org.mybatis.dynamic.sql.CriteriaGroup import org.mybatis.dynamic.sql.ExistsCriterion import org.mybatis.dynamic.sql.NotCriterion +import org.mybatis.dynamic.sql.RenderableCondition import org.mybatis.dynamic.sql.SqlBuilder import org.mybatis.dynamic.sql.SqlCriterion -import org.mybatis.dynamic.sql.VisitableCondition typealias GroupingCriteriaReceiver = GroupingCriteriaCollector.() -> Unit @@ -229,7 +229,7 @@ open class GroupingCriteriaCollector : SubCriteriaCollector() { * * @param condition the condition to be applied to this column, in this scope */ - operator fun BindableColumn.invoke(condition: VisitableCondition) { + operator fun BindableColumn.invoke(condition: RenderableCondition) { initialCriterion = ColumnAndConditionCriterion.withColumn(this) .withCondition(condition) .build() diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/JoinCollector.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/JoinCollector.kt index f85927682..c65b37a7a 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/JoinCollector.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/JoinCollector.kt @@ -16,8 +16,8 @@ package org.mybatis.dynamic.sql.util.kotlin import org.mybatis.dynamic.sql.BindableColumn +import org.mybatis.dynamic.sql.RenderableCondition import org.mybatis.dynamic.sql.SqlBuilder -import org.mybatis.dynamic.sql.VisitableCondition typealias JoinReceiver = JoinCollector.() -> Unit @@ -38,7 +38,7 @@ class JoinCollector { } } -class RightColumnCollector(private val joinConditionConsumer: (VisitableCondition) -> Unit) { +class RightColumnCollector(private val joinConditionConsumer: (RenderableCondition) -> Unit) { infix fun equalTo(rightColumn: BindableColumn) = joinConditionConsumer.invoke(SqlBuilder.isEqualTo(rightColumn)) infix fun equalTo(value: T) = joinConditionConsumer.invoke(SqlBuilder.isEqualTo(value)) diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/CaseDSLs.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/CaseDSLs.kt index bc04a992d..ce33f27b7 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/CaseDSLs.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/CaseDSLs.kt @@ -16,7 +16,7 @@ package org.mybatis.dynamic.sql.util.kotlin.elements import org.mybatis.dynamic.sql.BasicColumn -import org.mybatis.dynamic.sql.VisitableCondition +import org.mybatis.dynamic.sql.RenderableCondition import org.mybatis.dynamic.sql.select.caseexpression.BasicWhenCondition import org.mybatis.dynamic.sql.select.caseexpression.ConditionBasedWhenCondition import org.mybatis.dynamic.sql.select.caseexpression.SearchedCaseWhenCondition @@ -67,7 +67,7 @@ class KSimpleCaseDSL : KElseDSL { } internal val whenConditions = mutableListOf>() - fun `when`(vararg conditions: VisitableCondition) = + fun `when`(vararg conditions: RenderableCondition) = SimpleCaseThenGatherer { whenConditions.add(ConditionBasedWhenCondition(conditions.asList(), it)) } fun `when`(vararg values: T) = diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/SqlElements.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/SqlElements.kt index a7fe899d6..536032605 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/SqlElements.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/SqlElements.kt @@ -21,11 +21,11 @@ import org.mybatis.dynamic.sql.BasicColumn import org.mybatis.dynamic.sql.BindableColumn import org.mybatis.dynamic.sql.BoundValue import org.mybatis.dynamic.sql.Constant +import org.mybatis.dynamic.sql.RenderableCondition import org.mybatis.dynamic.sql.SortSpecification import org.mybatis.dynamic.sql.SqlBuilder import org.mybatis.dynamic.sql.SqlColumn import org.mybatis.dynamic.sql.StringConstant -import org.mybatis.dynamic.sql.VisitableCondition import org.mybatis.dynamic.sql.select.caseexpression.SearchedCaseModel import org.mybatis.dynamic.sql.select.caseexpression.SimpleCaseModel import org.mybatis.dynamic.sql.select.aggregate.Avg @@ -139,7 +139,7 @@ fun sum(column: BindableColumn): Sum = SqlBuilder.sum(column) fun sum(column: BasicColumn): Sum<*> = SqlBuilder.sum(column) -fun sum(column: BindableColumn, condition: VisitableCondition): Sum = SqlBuilder.sum(column, condition) +fun sum(column: BindableColumn, condition: RenderableCondition): Sum = SqlBuilder.sum(column, condition) // constants fun constant(constant: String): Constant = SqlBuilder.constant(constant) From fd7d6e8206b69ce292647266c73916aab6d26c08 Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Wed, 5 Mar 2025 16:35:34 -0500 Subject: [PATCH 4/6] Documentation --- src/site/markdown/docs/extending.md | 83 +++++++++++++++++++ .../java/examples/mysql/IsLikeEscape.java | 44 +++++++--- src/test/java/examples/mysql/MySQLTest.java | 2 +- 3 files changed, 116 insertions(+), 13 deletions(-) diff --git a/src/site/markdown/docs/extending.md b/src/site/markdown/docs/extending.md index a32dea137..eabee1e03 100644 --- a/src/site/markdown/docs/extending.md +++ b/src/site/markdown/docs/extending.md @@ -292,3 +292,86 @@ it. You can write your own rendering support if you are dissatisfied with the S Writing a custom renderer is quite complex. If you want to undertake that task, we suggest that you take the time to understand how the default renderers work first. Feel free to ask questions about this topic on the MyBatis mailing list. + +## Writing Custom Conditions + +The library supplies a full range of conditions for all the common SQL operators (=, !=, like, between, etc.) Some +databases support extensions to the standard operators. For example, MySQL supports an extension to the "LIKE" +condition - the "ESCAPE" clause. If you need to implement a condition like that, then you will need to code a +custom condition. + +Here's an example of implementing a LIKE condition that supports ESCAPE: + +```java +@NullMarked +public class IsLikeEscape extends AbstractSingleValueCondition { + private static final IsLikeEscape EMPTY = new IsLikeEscape(-1, null) { + @Override + public Object value() { + throw new NoSuchElementException("No value present"); //$NON-NLS-1$ + } + + @Override + public boolean isEmpty() { + return true; + } + }; + + public static IsLikeEscape empty() { + @SuppressWarnings("unchecked") + IsLikeEscape t = (IsLikeEscape) EMPTY; + return t; + } + + private final @Nullable Character escapeCharacter; + + protected IsLikeEscape(T value, @Nullable Character escapeCharacter) { + super(value); + this.escapeCharacter = escapeCharacter; + } + + @Override + public String operator() { + return "like"; + } + + @Override + public FragmentAndParameters renderCondition(RenderingContext renderingContext, BindableColumn leftColumn) { + var fragment = super.renderCondition(renderingContext, leftColumn); + if (escapeCharacter != null) { + fragment = fragment.mapFragment(this::addEscape); + } + + return fragment; + } + + private String addEscape(String s) { + return s + " ESCAPE '" + escapeCharacter + "'"; + } + + @Override + public IsLikeEscape filter(Predicate predicate) { + return filterSupport(predicate, IsLikeEscape::empty, this); + } + + public IsLikeEscape map(Function mapper) { + return mapSupport(mapper, v -> new IsLikeEscape<>(v, escapeCharacter), IsLikeEscape::empty); + } + + public static IsLikeEscape isLike(T value) { + return new IsLikeEscape<>(value, null); + } + + public static IsLikeEscape isLike(T value, Character escapeCharacter) { + return new IsLikeEscape<>(value, escapeCharacter); + } +} +``` + +Important notes: + +1. The class extends `AbstractSingleValueCondition` - which is appropriate for like conditions +2. The class constructor accepts an escape character that will be rendered into an ESCAPE phrase +3. The class overrides `renderCondition` and changes the library generated `FragmentAndParameters` to add the ESCAPE + phrase. **This is the key to what's needed to implement a custom condition.** +4. The class provides `map` and `filter` functions as is expected for any condition in the library diff --git a/src/test/java/examples/mysql/IsLikeEscape.java b/src/test/java/examples/mysql/IsLikeEscape.java index 28e3045c2..19e1a085c 100644 --- a/src/test/java/examples/mysql/IsLikeEscape.java +++ b/src/test/java/examples/mysql/IsLikeEscape.java @@ -17,16 +17,18 @@ import java.util.NoSuchElementException; import java.util.function.Function; +import java.util.function.Predicate; import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +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 org.mybatis.dynamic.sql.where.condition.IsLike; @NullMarked -public class IsLikeEscape extends IsLike { - private static final IsLikeEscape EMPTY = new IsLikeEscape(-1, "") { +public class IsLikeEscape extends AbstractSingleValueCondition { + private static final IsLikeEscape EMPTY = new IsLikeEscape(-1, null) { @Override public Object value() { throw new NoSuchElementException("No value present"); //$NON-NLS-1$ @@ -44,28 +46,46 @@ public static IsLikeEscape empty() { return t; } - private final String escapeString; + private final @Nullable Character escapeCharacter; - protected IsLikeEscape(T value, String escapeString) { + protected IsLikeEscape(T value, @Nullable Character escapeCharacter) { super(value); - this.escapeString = escapeString; + this.escapeCharacter = escapeCharacter; + } + + @Override + public String operator() { + return "like"; } @Override public FragmentAndParameters renderCondition(RenderingContext renderingContext, BindableColumn leftColumn) { - return super.renderCondition(renderingContext, leftColumn).mapFragment(this::addEscape); + var fragment = super.renderCondition(renderingContext, leftColumn); + if (escapeCharacter != null) { + fragment = fragment.mapFragment(this::addEscape); + } + + return fragment; } private String addEscape(String s) { - return s + " ESCAPE '" + escapeString + "'"; + return s + " ESCAPE '" + escapeCharacter + "'"; } @Override - public IsLike map(Function mapper) { - return mapSupport(mapper, v -> new IsLikeEscape<>(v, escapeString), IsLikeEscape::empty); + public IsLikeEscape filter(Predicate predicate) { + return filterSupport(predicate, IsLikeEscape::empty, this); + } + + public IsLikeEscape map(Function mapper) { + return mapSupport(mapper, v -> new IsLikeEscape<>(v, escapeCharacter), IsLikeEscape::empty); + } + + public static IsLikeEscape isLike(T value) { + return new IsLikeEscape<>(value, null); } - public static IsLikeEscape isLike(T value, String escapeString) { - return new IsLikeEscape<>(value, escapeString); + public static IsLikeEscape isLike(T value, Character escapeCharacter) { + return new IsLikeEscape<>(value, escapeCharacter); } } diff --git a/src/test/java/examples/mysql/MySQLTest.java b/src/test/java/examples/mysql/MySQLTest.java index 20dd9fd39..45c403bd7 100644 --- a/src/test/java/examples/mysql/MySQLTest.java +++ b/src/test/java/examples/mysql/MySQLTest.java @@ -127,7 +127,7 @@ void testIsLikeEscape() { SelectStatementProvider selectStatement = select(id, description) .from(items) - .where(description, IsLikeEscape.isLike("Item 1%", "#").map(s -> s)) + .where(description, IsLikeEscape.isLike("Item 1%", '#').map(s -> s)) .orderBy(id) .build() .render(RenderingStrategies.MYBATIS3); From 7dd08ab95c4a9cbb51ccce39cfc93a0505cf5a2f Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Thu, 6 Mar 2025 09:33:24 -0500 Subject: [PATCH 5/6] Documentation --- .../dynamic/sql/RenderableCondition.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/main/java/org/mybatis/dynamic/sql/RenderableCondition.java b/src/main/java/org/mybatis/dynamic/sql/RenderableCondition.java index 4ca7a17f2..51dc912e8 100644 --- a/src/main/java/org/mybatis/dynamic/sql/RenderableCondition.java +++ b/src/main/java/org/mybatis/dynamic/sql/RenderableCondition.java @@ -20,8 +20,33 @@ @FunctionalInterface public interface RenderableCondition { + /** + * Render a condition - typically a condition in a WHERE clause. + * + *

A rendered condition includes an SQL fragment, and any associated parameters. For example, + * the isEqual condition should be rendered as "= ?" where "?" is a properly formatted + * parameter marker (the parameter marker can be computed from the RenderingContext). + * Note that a rendered condition should NOT include the left side of the phrase - that is rendered + * by the {@link RenderableCondition#renderLeftColumn(RenderingContext, BindableColumn)} method. + * + * @param renderingContext the current rendering context + * @param leftColumn the column related to this condition in a where clause + * @return the rendered condition. Should NOT include the column. + */ FragmentAndParameters renderCondition(RenderingContext renderingContext, BindableColumn leftColumn); + /** + * Render the column in a column and condition phrase - typically in a WHERE clause. + * + *

By default, the column will be rendered as the column alias if it exists, or the column name. + * This can be complicated if the column has a table qualifier, or if the "column" is a function or + * part of a CASE expression. Columns know how to render themselves, so we just call their "render" + * methods. + * + * @param renderingContext the current rendering context + * @param leftColumn the column related to this condition in a where clause + * @return the rendered column + */ default FragmentAndParameters renderLeftColumn(RenderingContext renderingContext, BindableColumn leftColumn) { return leftColumn.alias() .map(FragmentAndParameters::fromFragment) From 13fb0cf5d7bee7781ea85ce24f8c5ff68c223d14 Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Thu, 6 Mar 2025 09:33:38 -0500 Subject: [PATCH 6/6] Convert to record --- .../dynamic/sql/render/RenderedParameterInfo.java | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/main/java/org/mybatis/dynamic/sql/render/RenderedParameterInfo.java b/src/main/java/org/mybatis/dynamic/sql/render/RenderedParameterInfo.java index c1be974f5..5c8187ef2 100644 --- a/src/main/java/org/mybatis/dynamic/sql/render/RenderedParameterInfo.java +++ b/src/main/java/org/mybatis/dynamic/sql/render/RenderedParameterInfo.java @@ -17,20 +17,9 @@ import java.util.Objects; -public class RenderedParameterInfo { - private final String parameterMapKey; - private final String renderedPlaceHolder; - +public record RenderedParameterInfo(String parameterMapKey, String renderedPlaceHolder) { public RenderedParameterInfo(String parameterMapKey, String renderedPlaceHolder) { this.parameterMapKey = Objects.requireNonNull(parameterMapKey); this.renderedPlaceHolder = Objects.requireNonNull(renderedPlaceHolder); } - - public String parameterMapKey() { - return parameterMapKey; - } - - public String renderedPlaceHolder() { - return renderedPlaceHolder; - } }