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 ff6923ecb..65e45aafe 100644 --- a/src/main/java/org/mybatis/dynamic/sql/AbstractColumnComparisonCondition.java +++ b/src/main/java/org/mybatis/dynamic/sql/AbstractColumnComparisonCondition.java @@ -15,7 +15,12 @@ */ package org.mybatis.dynamic.sql; -public abstract class AbstractColumnComparisonCondition implements VisitableCondition { +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 RenderableCondition { 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..23f48e6f4 100644 --- a/src/main/java/org/mybatis/dynamic/sql/AbstractListValueCondition.java +++ b/src/main/java/org/mybatis/dynamic/sql/AbstractListValueCondition.java @@ -23,7 +23,12 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -public abstract class AbstractListValueCondition implements VisitableCondition { +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 RenderableCondition { protected final Collection values; protected AbstractListValueCondition(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..c0e103baf 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 RenderableCondition { 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..9c8248d02 100644 --- a/src/main/java/org/mybatis/dynamic/sql/AbstractSingleValueCondition.java +++ b/src/main/java/org/mybatis/dynamic/sql/AbstractSingleValueCondition.java @@ -15,11 +15,17 @@ */ 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; -public abstract class AbstractSingleValueCondition implements VisitableCondition { +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 RenderableCondition { protected final T value; protected AbstractSingleValueCondition(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..dcfbd4b3c 100644 --- a/src/main/java/org/mybatis/dynamic/sql/AbstractSubselectCondition.java +++ b/src/main/java/org/mybatis/dynamic/sql/AbstractSubselectCondition.java @@ -15,24 +15,28 @@ */ 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 { +public abstract class AbstractSubselectCondition implements RenderableCondition { private final SelectModel selectModel; 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..865f7db0b 100644 --- a/src/main/java/org/mybatis/dynamic/sql/AbstractTwoValueCondition.java +++ b/src/main/java/org/mybatis/dynamic/sql/AbstractTwoValueCondition.java @@ -15,13 +15,19 @@ */ 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; -public abstract class AbstractTwoValueCondition implements VisitableCondition { +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 RenderableCondition { 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/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/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/RenderableCondition.java b/src/main/java/org/mybatis/dynamic/sql/RenderableCondition.java new file mode 100644 index 000000000..51dc912e8 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/RenderableCondition.java @@ -0,0 +1,81 @@ +/* + * 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 { + /** + * 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) + .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() {} +} 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 fc974b932..9969c3997 100644 --- a/src/main/java/org/mybatis/dynamic/sql/VisitableCondition.java +++ b/src/main/java/org/mybatis/dynamic/sql/VisitableCondition.java @@ -17,45 +17,20 @@ import org.mybatis.dynamic.sql.render.RenderingContext; -@FunctionalInterface -public interface VisitableCondition { - R accept(ConditionVisitor visitor); - - /** - * 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() {} - - /** - * 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; - } -} +/** + * 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/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; - } } 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 702f1a0ad..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; @@ -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 @@ -63,12 +57,12 @@ 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) { - return condition.accept(conditionVisitor); + private FragmentAndParameters renderCondition(RenderableCondition condition) { + return condition.renderCondition(renderingContext, column); } private FragmentAndParameters renderBasicValue(T value) { 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 c0eef325a..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 @@ -15,12 +15,17 @@ */ package org.mybatis.dynamic.sql.where.condition; -import org.mybatis.dynamic.sql.VisitableCondition; +import org.mybatis.dynamic.sql.BindableColumn; +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 String overrideRenderedLeftColumn(String renderedLeftColumn) { - return "upper(" + renderedLeftColumn + ")"; //$NON-NLS-1$ //$NON-NLS-2$ + default FragmentAndParameters renderLeftColumn(RenderingContext renderingContext, + BindableColumn 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 6035c143b..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,43 +20,32 @@ 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 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; + private @Nullable RenderableCondition condition; private @Nullable RenderingContext renderingContext; public Builder withColumn(BindableColumn column) { @@ -64,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/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/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) 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 new file mode 100644 index 000000000..19e1a085c --- /dev/null +++ b/src/test/java/examples/mysql/IsLikeEscape.java @@ -0,0 +1,91 @@ +/* + * 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 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; + +@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); + } +} diff --git a/src/test/java/examples/mysql/MySQLTest.java b/src/test/java/examples/mysql/MySQLTest.java index 54c43c34a..45c403bd7 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")); + } + } }