From 98127012dbe041daefb55e0c8a24d72ec7aaa43a Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Fri, 8 Mar 2024 14:45:52 -0500 Subject: [PATCH 01/19] ConditionVisitor should render only the condition Previously the visitor rendered both the column and the condition, but this won't work for simple case statements. We need to be able to render only the condition for simple case statements. --- .../dynamic/sql/select/aggregate/Sum.java | 11 +-- .../render/ColumnAndConditionRenderer.java | 80 +++++++++++++++++++ .../sql/where/render/CriterionRenderer.java | 8 +- .../where/render/DefaultConditionVisitor.java | 32 ++------ 4 files changed, 97 insertions(+), 34 deletions(-) create mode 100644 src/main/java/org/mybatis/dynamic/sql/where/render/ColumnAndConditionRenderer.java 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 345bf53de..02c8c7a42 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 @@ -23,7 +23,7 @@ import org.mybatis.dynamic.sql.select.function.AbstractUniTypeFunction; import org.mybatis.dynamic.sql.util.FragmentAndParameters; import org.mybatis.dynamic.sql.util.Validator; -import org.mybatis.dynamic.sql.where.render.DefaultConditionVisitor; +import org.mybatis.dynamic.sql.where.render.ColumnAndConditionRenderer; public class Sum extends AbstractUniTypeFunction> { private final Function renderer; @@ -38,12 +38,13 @@ private Sum(BindableColumn column, VisitableCondition condition) { renderer = rc -> { Validator.assertTrue(condition.shouldRender(rc), "ERROR.37", "sum"); //$NON-NLS-1$ //$NON-NLS-2$ - DefaultConditionVisitor visitor = new DefaultConditionVisitor.Builder() + return new ColumnAndConditionRenderer.Builder() .withColumn(column) + .withCondition(condition) .withRenderingContext(rc) - .build(); - - return condition.accept(visitor).mapFragment(this::applyAggregate); + .build() + .render() + .mapFragment(this::applyAggregate); }; } 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 new file mode 100644 index 000000000..2a78ccbf8 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/where/render/ColumnAndConditionRenderer.java @@ -0,0 +1,80 @@ +/* + * Copyright 2016-2024 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 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 class ColumnAndConditionRenderer { + private final BindableColumn column; + private final VisitableCondition condition; + private final RenderingContext renderingContext; + + private ColumnAndConditionRenderer(Builder builder) { + column = Objects.requireNonNull(builder.column); + condition = Objects.requireNonNull(builder.condition); + renderingContext = Objects.requireNonNull(builder.renderingContext); + } + + public FragmentAndParameters render() { + FragmentAndParameters renderedLeftColumn = column.render(renderingContext); + + DefaultConditionVisitor visitor = DefaultConditionVisitor.withColumn(column) + .withRenderingContext(renderingContext) + .build(); + + FragmentAndParameters renderedCondition = condition.accept(visitor); + + String finalFragment = condition.overrideRenderedLeftColumn(renderedLeftColumn.fragment()) + + spaceBefore(renderedCondition.fragment()); + + return FragmentAndParameters.withFragment(finalFragment) + .withParameters(renderedLeftColumn.parameters()) + .withParameters(renderedCondition.parameters()) + .build(); + } + + public static class Builder { + private BindableColumn column; + private VisitableCondition condition; + private RenderingContext renderingContext; + + public Builder withColumn(BindableColumn column) { + this.column = column; + return this; + } + + public Builder withCondition(VisitableCondition condition) { + this.condition = condition; + return this; + } + + public Builder withRenderingContext(RenderingContext renderingContext) { + this.renderingContext = renderingContext; + return this; + } + + public ColumnAndConditionRenderer build() { + return new ColumnAndConditionRenderer<>(this); + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/where/render/CriterionRenderer.java b/src/main/java/org/mybatis/dynamic/sql/where/render/CriterionRenderer.java index f5942a5e2..4be08cc2d 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/render/CriterionRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/render/CriterionRenderer.java @@ -175,10 +175,12 @@ private Optional calculateRenderedCriterion(List FragmentAndParameters renderCondition(ColumnAndConditionCriterion criterion) { - DefaultConditionVisitor visitor = DefaultConditionVisitor.withColumn(criterion.column()) + return new ColumnAndConditionRenderer.Builder() + .withColumn(criterion.column()) + .withCondition(criterion.condition()) .withRenderingContext(renderingContext) - .build(); - return criterion.condition().accept(visitor); + .build() + .render(); } /** 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 index 7ec317c60..63ded9299 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/render/DefaultConditionVisitor.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/render/DefaultConditionVisitor.java @@ -47,54 +47,41 @@ private DefaultConditionVisitor(Builder builder) { @Override public FragmentAndParameters visit(AbstractListValueCondition condition) { - FragmentAndParameters renderedLeftColumn = column.render(renderingContext); FragmentCollector fc = condition.mapValues(this::toFragmentAndParameters).collect(FragmentCollector.collect()); String joinedFragments = fc.collectFragments(Collectors.joining(",", "(", ")")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ - String finalFragment = condition.overrideRenderedLeftColumn(renderedLeftColumn.fragment()) - + spaceBefore(condition.operator()) + String finalFragment = condition.operator() + spaceBefore(joinedFragments); return FragmentAndParameters .withFragment(finalFragment) .withParameters(fc.parameters()) - .withParameters(renderedLeftColumn.parameters()) .build(); } @Override public FragmentAndParameters visit(AbstractNoValueCondition condition) { - FragmentAndParameters renderedLeftColumn = column.render(renderingContext); - String finalFragment = condition.overrideRenderedLeftColumn(renderedLeftColumn.fragment()) - + spaceBefore(condition.operator()); - return FragmentAndParameters.withFragment(finalFragment) - .withParameters(renderedLeftColumn.parameters()) - .build(); + return FragmentAndParameters.fromFragment(condition.operator()); } @Override public FragmentAndParameters visit(AbstractSingleValueCondition condition) { - FragmentAndParameters renderedLeftColumn = column.render(renderingContext); RenderedParameterInfo parameterInfo = renderingContext.calculateParameterInfo(column); - String finalFragment = condition.overrideRenderedLeftColumn(renderedLeftColumn.fragment()) - + spaceBefore(condition.operator()) + String finalFragment = condition.operator() + spaceBefore(parameterInfo.renderedPlaceHolder()); return FragmentAndParameters.withFragment(finalFragment) .withParameter(parameterInfo.parameterMapKey(), convertValue(condition.value())) - .withParameters(renderedLeftColumn.parameters()) .build(); } @Override public FragmentAndParameters visit(AbstractTwoValueCondition condition) { - FragmentAndParameters renderedLeftColumn = column.render(renderingContext); RenderedParameterInfo parameterInfo1 = renderingContext.calculateParameterInfo(column); RenderedParameterInfo parameterInfo2 = renderingContext.calculateParameterInfo(column); - String finalFragment = condition.overrideRenderedLeftColumn(renderedLeftColumn.fragment()) - + spaceBefore(condition.operator1()) + String finalFragment = condition.operator1() + spaceBefore(parameterInfo1.renderedPlaceHolder()) + spaceBefore(condition.operator2()) + spaceBefore(parameterInfo2.renderedPlaceHolder()); @@ -102,39 +89,32 @@ public FragmentAndParameters visit(AbstractTwoValueCondition condition) { return FragmentAndParameters.withFragment(finalFragment) .withParameter(parameterInfo1.parameterMapKey(), convertValue(condition.value1())) .withParameter(parameterInfo2.parameterMapKey(), convertValue(condition.value2())) - .withParameters(renderedLeftColumn.parameters()) .build(); } @Override public FragmentAndParameters visit(AbstractSubselectCondition condition) { - FragmentAndParameters renderedLeftColumn = column.render(renderingContext); SelectStatementProvider selectStatement = SelectRenderer.withSelectModel(condition.selectModel()) .withRenderingContext(renderingContext) .build() .render(); - String finalFragment = condition.overrideRenderedLeftColumn(renderedLeftColumn.fragment()) - + spaceBefore(condition.operator()) + String finalFragment = condition.operator() + " (" //$NON-NLS-1$ + selectStatement.getSelectStatement() + ")"; //$NON-NLS-1$ return FragmentAndParameters.withFragment(finalFragment) .withParameters(selectStatement.getParameters()) - .withParameters(renderedLeftColumn.parameters()) .build(); } @Override public FragmentAndParameters visit(AbstractColumnComparisonCondition condition) { - FragmentAndParameters renderedLeftColumn = column.render(renderingContext); FragmentAndParameters renderedRightColumn = condition.rightColumn().render(renderingContext); - String finalFragment = condition.overrideRenderedLeftColumn(renderedLeftColumn.fragment()) - + spaceBefore(condition.operator()) + String finalFragment = condition.operator() + spaceBefore(renderedRightColumn.fragment()); return FragmentAndParameters.withFragment(finalFragment) - .withParameters(renderedLeftColumn.parameters()) .withParameters(renderedRightColumn.parameters()) .build(); } From 11acc1a2708fb340fd4730f5bb173a9c18bd4164 Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Tue, 12 Mar 2024 14:29:28 -0400 Subject: [PATCH 02/19] Case expressions for Java DSL --- .../org/mybatis/dynamic/sql/SqlBuilder.java | 11 + .../common/AbstractBooleanExpressionDSL.java | 6 +- .../dynamic/sql/select/SearchedCaseDSL.java | 97 ++++++ .../dynamic/sql/select/SearchedCaseModel.java | 108 ++++++ .../dynamic/sql/select/SimpleCaseDSL.java | 76 +++++ .../dynamic/sql/select/SimpleCaseModel.java | 124 +++++++ .../select/render/SearchedCaseRenderer.java | 87 +++++ .../SearchedCaseWhenConditionRenderer.java | 42 +++ .../sql/select/render/SimpleCaseRenderer.java | 104 ++++++ .../dynamic/sql/util/messages.properties | 1 + .../animal/data/CaseExpressionTest.java | 323 ++++++++++++++++++ 11 files changed, 978 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/mybatis/dynamic/sql/select/SearchedCaseDSL.java create mode 100644 src/main/java/org/mybatis/dynamic/sql/select/SearchedCaseModel.java create mode 100644 src/main/java/org/mybatis/dynamic/sql/select/SimpleCaseDSL.java create mode 100644 src/main/java/org/mybatis/dynamic/sql/select/SimpleCaseModel.java create mode 100644 src/main/java/org/mybatis/dynamic/sql/select/render/SearchedCaseRenderer.java create mode 100644 src/main/java/org/mybatis/dynamic/sql/select/render/SearchedCaseWhenConditionRenderer.java create mode 100644 src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseRenderer.java create mode 100644 src/test/java/examples/animal/data/CaseExpressionTest.java diff --git a/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java index 0e2f372b3..f1b58fb83 100644 --- a/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java +++ b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java @@ -33,8 +33,10 @@ import org.mybatis.dynamic.sql.select.HavingDSL; import org.mybatis.dynamic.sql.select.MultiSelectDSL; import org.mybatis.dynamic.sql.select.QueryExpressionDSL.FromGatherer; +import org.mybatis.dynamic.sql.select.SearchedCaseDSL; import org.mybatis.dynamic.sql.select.SelectDSL; import org.mybatis.dynamic.sql.select.SelectModel; +import org.mybatis.dynamic.sql.select.SimpleCaseDSL; import org.mybatis.dynamic.sql.select.SimpleSortSpecification; import org.mybatis.dynamic.sql.select.aggregate.Avg; import org.mybatis.dynamic.sql.select.aggregate.Count; @@ -425,6 +427,15 @@ static AndOrCriteriaGroup and(List subCriteria) { .build(); } + // case expressions + static SimpleCaseDSL simpleCase(BindableColumn column) { + return SimpleCaseDSL.simpleCase(column); + } + + static SearchedCaseDSL searchedCase() { + return SearchedCaseDSL.searchedCase(); + } + // join support static JoinCriterion and(BindableColumn joinColumn, JoinCondition joinCondition) { return new JoinCriterion.Builder() 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 e61ec6de4..40f9cc0f7 100644 --- a/src/main/java/org/mybatis/dynamic/sql/common/AbstractBooleanExpressionDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/common/AbstractBooleanExpressionDSL.java @@ -144,9 +144,13 @@ private void addSubCriteria(String connector, List criteria) .build()); } + protected void setInitialCriterion(SqlCriterion initialCriterion) { + this.initialCriterion = initialCriterion; + } + protected void setInitialCriterion(SqlCriterion initialCriterion, StatementType statementType) { Validator.assertTrue(this.initialCriterion == null, statementType.messageNumber()); - this.initialCriterion = initialCriterion; + setInitialCriterion(initialCriterion); } // may be null! diff --git a/src/main/java/org/mybatis/dynamic/sql/select/SearchedCaseDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/SearchedCaseDSL.java new file mode 100644 index 000000000..c9eb9de4d --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/SearchedCaseDSL.java @@ -0,0 +1,97 @@ +/* + * Copyright 2016-2024 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.select; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.mybatis.dynamic.sql.AndOrCriteriaGroup; +import org.mybatis.dynamic.sql.BindableColumn; +import org.mybatis.dynamic.sql.ColumnAndConditionCriterion; +import org.mybatis.dynamic.sql.CriteriaGroup; +import org.mybatis.dynamic.sql.SqlCriterion; +import org.mybatis.dynamic.sql.VisitableCondition; +import org.mybatis.dynamic.sql.common.AbstractBooleanExpressionDSL; + +public class SearchedCaseDSL { + private final List whenConditions = new ArrayList<>(); + private String elseValue; + + public WhenDSL when(BindableColumn column, VisitableCondition condition, + AndOrCriteriaGroup... subCriteria) { + return when(column, condition, Arrays.asList(subCriteria)); + } + + public WhenDSL when(BindableColumn column, VisitableCondition condition, + List subCriteria) { + SqlCriterion sqlCriterion = ColumnAndConditionCriterion.withColumn(column) + .withCondition(condition) + .withSubCriteria(subCriteria) + .build(); + + return initialize(sqlCriterion); + } + + public WhenDSL when(SqlCriterion initialCriterion, AndOrCriteriaGroup... subCriteria) { + return when(initialCriterion, Arrays.asList(subCriteria)); + } + + public WhenDSL when(SqlCriterion initialCriterion, List subCriteria) { + SqlCriterion sqlCriterion = new CriteriaGroup.Builder() + .withInitialCriterion(initialCriterion) + .withSubCriteria(subCriteria) + .build(); + + return initialize(sqlCriterion); + } + + private WhenDSL initialize(SqlCriterion sqlCriterion) { + return new WhenDSL(sqlCriterion); + } + + public SearchedCaseDSL elseConstant(String elseValue) { + this.elseValue = elseValue; + return this; + } + + public SearchedCaseModel end() { + return new SearchedCaseModel.Builder() + .withElseValue(elseValue) + .withWhenConditions(whenConditions) + .build(); + } + + public class WhenDSL extends AbstractBooleanExpressionDSL { + private WhenDSL(SqlCriterion sqlCriterion) { + setInitialCriterion(sqlCriterion); + } + + public SearchedCaseDSL thenConstant(String value) { + whenConditions.add(new SearchedCaseModel.SearchedWhenCondition(getInitialCriterion(), subCriteria, value)); + return SearchedCaseDSL.this; + } + + @Override + protected WhenDSL getThis() { + return this; + } + } + + public static SearchedCaseDSL searchedCase() { + return new SearchedCaseDSL(); + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/SearchedCaseModel.java b/src/main/java/org/mybatis/dynamic/sql/select/SearchedCaseModel.java new file mode 100644 index 000000000..1d155c1d2 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/SearchedCaseModel.java @@ -0,0 +1,108 @@ +/* + * Copyright 2016-2024 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.select; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Stream; + +import org.mybatis.dynamic.sql.AndOrCriteriaGroup; +import org.mybatis.dynamic.sql.BasicColumn; +import org.mybatis.dynamic.sql.SqlCriterion; +import org.mybatis.dynamic.sql.common.AbstractBooleanExpressionModel; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.select.render.SearchedCaseRenderer; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; + +public class SearchedCaseModel implements BasicColumn { + private final List whenConditions; + private final String elseValue; + private final String alias; + + private SearchedCaseModel(Builder builder) { + whenConditions = builder.whenConditions; + alias = builder.alias; + elseValue = builder.elseValue; + } + + public Stream whenConditions() { + return whenConditions.stream(); + } + + public Optional elseValue() { + return Optional.ofNullable(elseValue); + } + + @Override + public Optional alias() { + return Optional.ofNullable(alias); + } + + @Override + public SearchedCaseModel as(String alias) { + return new Builder().withWhenConditions(whenConditions) + .withElseValue(elseValue) + .withAlias(alias) + .build(); + } + + @Override + public FragmentAndParameters render(RenderingContext renderingContext) { + return new SearchedCaseRenderer(this, renderingContext).render(); + } + + public static class SearchedWhenCondition extends AbstractBooleanExpressionModel { + + private final String thenValue; + + public String thenValue() { + return thenValue; + } + + protected SearchedWhenCondition(SqlCriterion initialCriterion, List subCriteria, + String thenValue) { + super(initialCriterion, subCriteria); + this.thenValue = Objects.requireNonNull(thenValue); + } + } + + public static class Builder { + private final List whenConditions = new ArrayList<>(); + private String elseValue; + private String alias; + + public Builder withWhenConditions(List whenConditions) { + this.whenConditions.addAll(whenConditions); + return this; + } + + public Builder withElseValue(String elseValue) { + this.elseValue = elseValue; + return this; + } + + public Builder withAlias(String alias) { + this.alias = alias; + return this; + } + + public SearchedCaseModel build() { + return new SearchedCaseModel(this); + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/SimpleCaseDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/SimpleCaseDSL.java new file mode 100644 index 000000000..b5faac2a4 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/SimpleCaseDSL.java @@ -0,0 +1,76 @@ +/* + * Copyright 2016-2024 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.select; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +import org.mybatis.dynamic.sql.BindableColumn; +import org.mybatis.dynamic.sql.VisitableCondition; + +public class SimpleCaseDSL { + private final BindableColumn column; + private final List> whenConditions = new ArrayList<>(); + private String elseValue; + + private SimpleCaseDSL(BindableColumn column) { + this.column = Objects.requireNonNull(column); + } + + @SafeVarargs + public final WhenFinisher when(VisitableCondition condition, + VisitableCondition... subsequentConditions) { + return when(condition, Arrays.asList(subsequentConditions)); + } + + public WhenFinisher when(VisitableCondition condition, + List> subsequentConditions) { + return new WhenFinisher(condition, subsequentConditions); + } + + public SimpleCaseDSL elseConstant(String value) { + elseValue = value; + return this; + } + + public SimpleCaseModel end() { + return new SimpleCaseModel.Builder() + .withColumn(column) + .withWhenConditions(whenConditions) + .withElseValue(elseValue) + .build(); + } + + public class WhenFinisher { + private final List> conditions = new ArrayList<>(); + + private WhenFinisher(VisitableCondition condition, List> subsequentConditions) { + conditions.add(condition); + conditions.addAll(subsequentConditions); + } + + public SimpleCaseDSL thenConstant(String value) { + whenConditions.add(new SimpleCaseModel.SimpleWhenCondition<>(conditions, value)); + return SimpleCaseDSL.this; + } + } + + public static SimpleCaseDSL simpleCase(BindableColumn column) { + return new SimpleCaseDSL<>(column); + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/SimpleCaseModel.java b/src/main/java/org/mybatis/dynamic/sql/select/SimpleCaseModel.java new file mode 100644 index 000000000..a12aef089 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/SimpleCaseModel.java @@ -0,0 +1,124 @@ +/* + * Copyright 2016-2024 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.select; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Stream; + +import org.mybatis.dynamic.sql.BasicColumn; +import org.mybatis.dynamic.sql.BindableColumn; +import org.mybatis.dynamic.sql.VisitableCondition; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.select.render.SimpleCaseRenderer; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; + +public class SimpleCaseModel implements BasicColumn { + private final BindableColumn column; + private final List> whenConditions; + private final String elseValue; + private final String alias; + + private SimpleCaseModel(Builder builder) { + column = Objects.requireNonNull(builder.column); + whenConditions = builder.whenConditions; + elseValue = builder.elseValue; + alias = builder.alias; + } + + public BindableColumn column() { + return column; + } + + public Stream> whenConditions() { + return whenConditions.stream(); + } + + public Optional elseValue() { + return Optional.ofNullable(elseValue); + } + + @Override + public Optional alias() { + return Optional.ofNullable(alias); + } + + @Override + public SimpleCaseModel as(String alias) { + return new Builder() + .withColumn(column) + .withWhenConditions(whenConditions) + .withElseValue(elseValue) + .withAlias(alias) + .build(); + } + + @Override + public FragmentAndParameters render(RenderingContext renderingContext) { + return new SimpleCaseRenderer<>(this, renderingContext).render(); + } + + public static class SimpleWhenCondition { + private final List> conditions = new ArrayList<>(); + private final String thenValue; + + public Stream> conditions() { + return conditions.stream(); + } + + public Object thenValue() { + return thenValue; + } + + protected SimpleWhenCondition(List> conditions, String thenValue) { + this.conditions.addAll(conditions); + this.thenValue = Objects.requireNonNull(thenValue); + } + } + + public static class Builder { + private BindableColumn column; + private final List> whenConditions = new ArrayList<>(); + private String elseValue; + private String alias; + + public Builder withColumn(BindableColumn column) { + this.column = column; + return this; + } + + public Builder withWhenConditions(List> whenConditions) { + this.whenConditions.addAll(whenConditions); + return this; + } + + public Builder withElseValue(String elseValue) { + this.elseValue = elseValue; + return this; + } + + public Builder withAlias(String alias) { + this.alias = alias; + return this; + } + + public SimpleCaseModel build() { + return new SimpleCaseModel<>(this); + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/SearchedCaseRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/SearchedCaseRenderer.java new file mode 100644 index 000000000..9c87317d7 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/SearchedCaseRenderer.java @@ -0,0 +1,87 @@ +/* + * Copyright 2016-2024 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.select.render; + +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.mybatis.dynamic.sql.exception.InvalidSqlException; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.select.SearchedCaseModel; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; +import org.mybatis.dynamic.sql.util.FragmentCollector; +import org.mybatis.dynamic.sql.util.Messages; + +public class SearchedCaseRenderer { + private final SearchedCaseModel searchedCaseModel; + private final RenderingContext renderingContext; + + public SearchedCaseRenderer(SearchedCaseModel searchedCaseModel, RenderingContext renderingContext) { + this.searchedCaseModel = Objects.requireNonNull(searchedCaseModel); + this.renderingContext = Objects.requireNonNull(renderingContext); + } + + public FragmentAndParameters render() { + return Stream.of( + renderCase(), + renderWhenConditions(), + renderElse(), + renderEnd() + ) + .flatMap(Function.identity()) + .collect(FragmentCollector.collect()) + .toFragmentAndParameters(Collectors.joining(" ")); //$NON-NLS-1$ + } + + private Stream renderCase() { + return Stream.of(FragmentAndParameters.fromFragment("case")); //$NON-NLS-1$ + } + + private Stream renderWhenConditions() { + return searchedCaseModel.whenConditions().flatMap(this::renderWhenCondition); + } + + private Stream renderWhenCondition(SearchedCaseModel.SearchedWhenCondition whenCondition) { + return Stream.of(renderWhen(whenCondition), renderThen(whenCondition)); + } + + private FragmentAndParameters renderWhen(SearchedCaseModel.SearchedWhenCondition whenCondition) { + SearchedCaseWhenConditionRenderer renderer = new SearchedCaseWhenConditionRenderer.Builder(whenCondition) + .withRenderingContext(renderingContext) + .build(); + + return renderer.render() + .orElseThrow(() -> new InvalidSqlException(Messages.getString("ERROR.39"))); //$NON-NLS-1$ + } + + private FragmentAndParameters renderThen(SearchedCaseModel.SearchedWhenCondition whenCondition) { + return FragmentAndParameters.fromFragment("then " + whenCondition.thenValue()); //$NON-NLS-1$ + } + + private Stream renderElse() { + return searchedCaseModel.elseValue().map(this::renderElse).map(Stream::of).orElseGet(Stream::empty); + } + + private FragmentAndParameters renderElse(Object elseValue) { + return FragmentAndParameters.fromFragment("else " + elseValue); //$NON-NLS-1$ + } + + private Stream renderEnd() { + return Stream.of(FragmentAndParameters.fromFragment("end")); //$NON-NLS-1$ + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/SearchedCaseWhenConditionRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/SearchedCaseWhenConditionRenderer.java new file mode 100644 index 000000000..2286d8e0b --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/SearchedCaseWhenConditionRenderer.java @@ -0,0 +1,42 @@ +/* + * Copyright 2016-2024 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.select.render; + +import org.mybatis.dynamic.sql.common.AbstractBooleanExpressionRenderer; +import org.mybatis.dynamic.sql.select.SearchedCaseModel.SearchedWhenCondition; + +public class SearchedCaseWhenConditionRenderer extends AbstractBooleanExpressionRenderer { + protected SearchedCaseWhenConditionRenderer(Builder builder) { + super("when", builder); + } + + public static class Builder + extends AbstractBooleanExpressionRenderer.AbstractBuilder { + + protected Builder(SearchedWhenCondition model) { + super(model); + } + + public SearchedCaseWhenConditionRenderer build() { + return new SearchedCaseWhenConditionRenderer(this); + } + + @Override + protected Builder getThis() { + return this; + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseRenderer.java new file mode 100644 index 000000000..b2edebdd0 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseRenderer.java @@ -0,0 +1,104 @@ +/* + * Copyright 2016-2024 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.select.render; + +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.mybatis.dynamic.sql.VisitableCondition; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.select.SimpleCaseModel; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; +import org.mybatis.dynamic.sql.util.FragmentCollector; +import org.mybatis.dynamic.sql.where.render.DefaultConditionVisitor; + +public class SimpleCaseRenderer { + private final SimpleCaseModel simpleCaseModel; + private final RenderingContext renderingContext; + private final DefaultConditionVisitor conditionVisitor; + + public SimpleCaseRenderer(SimpleCaseModel simpleCaseModel, RenderingContext renderingContext) { + this.simpleCaseModel = Objects.requireNonNull(simpleCaseModel); + this.renderingContext = Objects.requireNonNull(renderingContext); + conditionVisitor = new DefaultConditionVisitor.Builder() + .withColumn(simpleCaseModel.column()) + .withRenderingContext(renderingContext) + .build(); + } + + public FragmentAndParameters render() { + return Stream.of( + renderCase(), + renderWhenConditions(), + renderElse(), + renderEnd() + ) + .flatMap(Function.identity()) + .collect(FragmentCollector.collect()) + .toFragmentAndParameters(Collectors.joining(" ")); //$NON-NLS-1$ + } + + private Stream renderCase() { + return Stream.of( + FragmentAndParameters.fromFragment("case"), //$NON-NLS-1$ + simpleCaseModel.column().render(renderingContext) + ); + } + + private Stream renderWhenConditions() { + return simpleCaseModel.whenConditions().flatMap(this::renderWhenCondition); + } + + private Stream renderWhenCondition(SimpleCaseModel.SimpleWhenCondition whenCondition) { + return Stream.of( + renderWhen(), + renderConditions(whenCondition), + renderThen(whenCondition) + ); + } + + private FragmentAndParameters renderWhen() { + return FragmentAndParameters.fromFragment("when"); //$NON-NLS-1$ + } + + private FragmentAndParameters renderConditions(SimpleCaseModel.SimpleWhenCondition whenCondition) { + return whenCondition.conditions().map(this::renderCondition) + .collect(FragmentCollector.collect()) + .toFragmentAndParameters(Collectors.joining(", ")); //$NON-NLS-1$ + } + + private FragmentAndParameters renderCondition(VisitableCondition condition) { + return condition.accept(conditionVisitor); + } + + private FragmentAndParameters renderThen(SimpleCaseModel.SimpleWhenCondition whenCondition) { + return FragmentAndParameters.fromFragment("then " + whenCondition.thenValue()); //$NON-NLS-1$ + } + + private Stream renderElse() { + return simpleCaseModel.elseValue().map(this::renderElse).map(Stream::of).orElseGet(Stream::empty); + } + + private FragmentAndParameters renderElse(Object elseValue) { + return FragmentAndParameters.fromFragment("else " + elseValue); //$NON-NLS-1$ + } + + private Stream renderEnd() { + return Stream.of(FragmentAndParameters.fromFragment("end")); //$NON-NLS-1$ + } +} diff --git a/src/main/resources/org/mybatis/dynamic/sql/util/messages.properties b/src/main/resources/org/mybatis/dynamic/sql/util/messages.properties index 3c7742815..4a5c80fed 100644 --- a/src/main/resources/org/mybatis/dynamic/sql/util/messages.properties +++ b/src/main/resources/org/mybatis/dynamic/sql/util/messages.properties @@ -55,4 +55,5 @@ ERROR.35=Multi-select statements must have at least one "union" or "union all" e ERROR.36=You must either implement the "render" or "renderWithTableAlias" method in a column or function ERROR.37=The "{0}" function does not support conditions that fail to render ERROR.38=Bound values cannot be aliased +ERROR.39=When clauses in searched case expressions must render INTERNAL.ERROR=Internal Error {0} diff --git a/src/test/java/examples/animal/data/CaseExpressionTest.java b/src/test/java/examples/animal/data/CaseExpressionTest.java new file mode 100644 index 000000000..8b567a9b0 --- /dev/null +++ b/src/test/java/examples/animal/data/CaseExpressionTest.java @@ -0,0 +1,323 @@ +/* + * Copyright 2016-2024 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.animal.data; + +import static examples.animal.data.AnimalDataDynamicSqlSupport.animalData; +import static examples.animal.data.AnimalDataDynamicSqlSupport.animalName; +import static examples.animal.data.AnimalDataDynamicSqlSupport.id; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.entry; +import static org.mybatis.dynamic.sql.SqlBuilder.and; +import static org.mybatis.dynamic.sql.SqlBuilder.group; +import static org.mybatis.dynamic.sql.SqlBuilder.isEqualTo; +import static org.mybatis.dynamic.sql.SqlBuilder.isEqualToWhenPresent; +import static org.mybatis.dynamic.sql.SqlBuilder.isIn; +import static org.mybatis.dynamic.sql.SqlBuilder.or; +import static org.mybatis.dynamic.sql.SqlBuilder.searchedCase; +import static org.mybatis.dynamic.sql.SqlBuilder.select; +import static org.mybatis.dynamic.sql.SqlBuilder.simpleCase; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.sql.Connection; +import java.sql.DriverManager; +import java.util.List; +import java.util.Map; + +import org.apache.ibatis.datasource.unpooled.UnpooledDataSource; +import org.apache.ibatis.jdbc.ScriptRunner; +import org.apache.ibatis.mapping.Environment; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.SqlSessionFactory; +import org.apache.ibatis.session.SqlSessionFactoryBuilder; +import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mybatis.dynamic.sql.exception.InvalidSqlException; +import org.mybatis.dynamic.sql.render.RenderingStrategies; +import org.mybatis.dynamic.sql.select.SelectModel; +import org.mybatis.dynamic.sql.select.render.SelectStatementProvider; +import org.mybatis.dynamic.sql.util.Messages; +import org.mybatis.dynamic.sql.util.mybatis3.CommonSelectMapper; + +class CaseExpressionTest { + private static final String JDBC_URL = "jdbc:hsqldb:mem:aname"; + private static final String JDBC_DRIVER = "org.hsqldb.jdbcDriver"; + + private SqlSessionFactory sqlSessionFactory; + + @BeforeEach + void setup() throws Exception { + Class.forName(JDBC_DRIVER); + InputStream is = getClass().getResourceAsStream("/examples/animal/data/CreateAnimalData.sql"); + assert is != null; + try (Connection connection = DriverManager.getConnection(JDBC_URL, "sa", "")) { + ScriptRunner sr = new ScriptRunner(connection); + sr.setLogWriter(null); + sr.runScript(new InputStreamReader(is)); + } + + UnpooledDataSource ds = new UnpooledDataSource(JDBC_DRIVER, JDBC_URL, "sa", ""); + Environment environment = new Environment("test", new JdbcTransactionFactory(), ds); + Configuration config = new Configuration(environment); + config.addMapper(CommonSelectMapper.class); + sqlSessionFactory = new SqlSessionFactoryBuilder().build(config); + } + + @Test + void testSearchedCase() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(animalName, searchedCase() + .when(animalName, isEqualTo("Artic fox")).or(animalName, isEqualTo("Red fox")).thenConstant("'Fox'") + .when(animalName, isEqualTo("Little brown bat")).or(animalName, isEqualTo("Big brown bat")).thenConstant("'Bat'") + .elseConstant("cast('Not a Fox or a bat' as varchar(25))").end().as("AnimalType")) + .from(animalData, "a") + .where(id, isIn(2, 3, 31, 32, 38, 39)) + .orderBy(id) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expected = "select a.animal_name, case " + + "when a.animal_name = #{parameters.p1,jdbcType=VARCHAR} or a.animal_name = #{parameters.p2,jdbcType=VARCHAR} then 'Fox' " + + "when a.animal_name = #{parameters.p3,jdbcType=VARCHAR} or a.animal_name = #{parameters.p4,jdbcType=VARCHAR} then 'Bat' " + + "else cast('Not a Fox or a bat' as varchar(25)) end as AnimalType " + + "from AnimalData a where a.id in (" + + "#{parameters.p5,jdbcType=INTEGER},#{parameters.p6,jdbcType=INTEGER}," + + "#{parameters.p7,jdbcType=INTEGER},#{parameters.p8,jdbcType=INTEGER},#{parameters.p9,jdbcType=INTEGER}," + + "#{parameters.p10,jdbcType=INTEGER}) " + + "order by id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expected); + assertThat(selectStatement.getParameters()).containsOnly( + entry("p1", "Artic fox"), + entry("p2", "Red fox"), + entry("p3", "Little brown bat"), + entry("p4", "Big brown bat"), + entry("p5", 2), + entry("p6", 3), + entry("p7", 31), + entry("p8", 32), + entry("p9", 38), + entry("p10", 39) + ); + + List> records = mapper.selectManyMappedRows(selectStatement); + assertThat(records).hasSize(6); + assertThat(records.get(0)).containsOnly(entry("ANIMAL_NAME", "Little brown bat"), entry("ANIMALTYPE", "Bat")); + assertThat(records.get(1)).containsOnly(entry("ANIMAL_NAME", "Big brown bat"), entry("ANIMALTYPE", "Bat")); + assertThat(records.get(2)).containsOnly(entry("ANIMAL_NAME", "Cat"), entry("ANIMALTYPE", "Not a Fox or a bat")); + assertThat(records.get(3)).containsOnly(entry("ANIMAL_NAME", "Artic fox"), entry("ANIMALTYPE", "Fox")); + assertThat(records.get(4)).containsOnly(entry("ANIMAL_NAME", "Red fox"), entry("ANIMALTYPE", "Fox")); + assertThat(records.get(5)).containsOnly(entry("ANIMAL_NAME", "Raccoon"), entry("ANIMALTYPE", "Not a Fox or a bat")); + } + } + + @Test + void testSearchedCaseNoElse() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(animalName, searchedCase() + .when(animalName, isEqualTo("Artic fox")).or(animalName, isEqualTo("Red fox")).thenConstant("'Fox'") + .when(animalName, isEqualTo("Little brown bat")).or(animalName, isEqualTo("Big brown bat")).thenConstant("'Bat'") + .end().as("AnimalType")) + .from(animalData, "a") + .where(id, isIn(2, 3, 31, 32, 38, 39)) + .orderBy(id) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expected = "select a.animal_name, case " + + "when a.animal_name = #{parameters.p1,jdbcType=VARCHAR} or a.animal_name = #{parameters.p2,jdbcType=VARCHAR} then 'Fox' " + + "when a.animal_name = #{parameters.p3,jdbcType=VARCHAR} or a.animal_name = #{parameters.p4,jdbcType=VARCHAR} then 'Bat' " + + "end as AnimalType " + + "from AnimalData a where a.id in (" + + "#{parameters.p5,jdbcType=INTEGER},#{parameters.p6,jdbcType=INTEGER}," + + "#{parameters.p7,jdbcType=INTEGER},#{parameters.p8,jdbcType=INTEGER},#{parameters.p9,jdbcType=INTEGER}," + + "#{parameters.p10,jdbcType=INTEGER}) " + + "order by id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expected); + assertThat(selectStatement.getParameters()).containsOnly( + entry("p1", "Artic fox"), + entry("p2", "Red fox"), + entry("p3", "Little brown bat"), + entry("p4", "Big brown bat"), + entry("p5", 2), + entry("p6", 3), + entry("p7", 31), + entry("p8", 32), + entry("p9", 38), + entry("p10", 39) + ); + + List> records = mapper.selectManyMappedRows(selectStatement); + assertThat(records).hasSize(6); + assertThat(records.get(0)).containsOnly(entry("ANIMAL_NAME", "Little brown bat"), entry("ANIMALTYPE", "Bat")); + assertThat(records.get(1)).containsOnly(entry("ANIMAL_NAME", "Big brown bat"), entry("ANIMALTYPE", "Bat")); + assertThat(records.get(2)).containsOnly(entry("ANIMAL_NAME", "Cat")); + assertThat(records.get(3)).containsOnly(entry("ANIMAL_NAME", "Artic fox"), entry("ANIMALTYPE", "Fox")); + assertThat(records.get(4)).containsOnly(entry("ANIMAL_NAME", "Red fox"), entry("ANIMALTYPE", "Fox")); + assertThat(records.get(5)).containsOnly(entry("ANIMAL_NAME", "Raccoon")); + } + } + + @Test + void testSearchedCaseWithGroup() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(animalName, searchedCase() + .when(animalName, isEqualTo("Artic fox")).or(animalName, isEqualTo("Red fox")).thenConstant("'Fox'") + .when(animalName, isEqualTo("Little brown bat")).or(animalName, isEqualTo("Big brown bat")).thenConstant("'Bat'") + .when(group(animalName, isEqualTo("Cat"), and(id, isEqualTo(31))), or(id, isEqualTo(39))).thenConstant("'Fred'") + .elseConstant("cast('Not a Fox or a bat' as varchar(25))").end().as("AnimalType")) + .from(animalData, "a") + .where(id, isIn(2, 3, 4, 31, 32, 38, 39)) + .orderBy(id) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expected = "select a.animal_name, case " + + "when a.animal_name = #{parameters.p1,jdbcType=VARCHAR} or a.animal_name = #{parameters.p2,jdbcType=VARCHAR} then 'Fox' " + + "when a.animal_name = #{parameters.p3,jdbcType=VARCHAR} or a.animal_name = #{parameters.p4,jdbcType=VARCHAR} then 'Bat' " + + "when (a.animal_name = #{parameters.p5,jdbcType=VARCHAR} and a.id = #{parameters.p6,jdbcType=INTEGER}) or a.id = #{parameters.p7,jdbcType=INTEGER} then 'Fred' " + + "else cast('Not a Fox or a bat' as varchar(25)) end as AnimalType " + + "from AnimalData a where a.id in (" + + "#{parameters.p8,jdbcType=INTEGER},#{parameters.p9,jdbcType=INTEGER}," + + "#{parameters.p10,jdbcType=INTEGER},#{parameters.p11,jdbcType=INTEGER},#{parameters.p12,jdbcType=INTEGER}," + + "#{parameters.p13,jdbcType=INTEGER},#{parameters.p14,jdbcType=INTEGER}) " + + "order by id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expected); + assertThat(selectStatement.getParameters()).containsOnly( + entry("p1", "Artic fox"), + entry("p2", "Red fox"), + entry("p3", "Little brown bat"), + entry("p4", "Big brown bat"), + entry("p5", "Cat"), + entry("p6", 31), + entry("p7", 39), + entry("p8", 2), + entry("p9", 3), + entry("p10", 4), + entry("p11", 31), + entry("p12", 32), + entry("p13", 38), + entry("p14", 39) + ); + + List> records = mapper.selectManyMappedRows(selectStatement); + assertThat(records).hasSize(7); + assertThat(records.get(0)).containsOnly(entry("ANIMAL_NAME", "Little brown bat"), entry("ANIMALTYPE", "Bat")); + assertThat(records.get(1)).containsOnly(entry("ANIMAL_NAME", "Big brown bat"), entry("ANIMALTYPE", "Bat")); + assertThat(records.get(2)).containsOnly(entry("ANIMAL_NAME", "Mouse"), entry("ANIMALTYPE", "Not a Fox or a bat")); + assertThat(records.get(3)).containsOnly(entry("ANIMAL_NAME", "Cat"), entry("ANIMALTYPE", "Fred")); + assertThat(records.get(4)).containsOnly(entry("ANIMAL_NAME", "Artic fox"), entry("ANIMALTYPE", "Fox")); + assertThat(records.get(5)).containsOnly(entry("ANIMAL_NAME", "Red fox"), entry("ANIMALTYPE", "Fox")); + assertThat(records.get(6)).containsOnly(entry("ANIMAL_NAME", "Raccoon"), entry("ANIMALTYPE", "Fred")); + } + } + + @Test + void testSimpleCase() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(animalName, simpleCase(animalName) + .when(isEqualTo("Artic fox"), isEqualTo("Red fox")).thenConstant("'yes'") + .elseConstant("cast('no' as VARCHAR(3))").end().as("IsAFox")) + .from(animalData) + .where(id, isIn(31, 32, 38, 39)) + .orderBy(id) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expected = "select animal_name, " + + "case animal_name when = #{parameters.p1,jdbcType=VARCHAR}, = #{parameters.p2,jdbcType=VARCHAR} then 'yes' else cast('no' as VARCHAR(3)) end " + + "as IsAFox from AnimalData where id in " + + "(#{parameters.p3,jdbcType=INTEGER},#{parameters.p4,jdbcType=INTEGER},#{parameters.p5,jdbcType=INTEGER},#{parameters.p6,jdbcType=INTEGER}) " + + "order by id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expected); + assertThat(selectStatement.getParameters()).containsOnly( + entry("p1", "Artic fox"), + entry("p2", "Red fox"), + entry("p3", 31), + entry("p4", 32), + entry("p5", 38), + entry("p6", 39) + ); + + List> records = mapper.selectManyMappedRows(selectStatement); + assertThat(records).hasSize(4); + assertThat(records.get(0)).containsOnly(entry("ANIMAL_NAME", "Cat"), entry("ISAFOX", "no")); + assertThat(records.get(1)).containsOnly(entry("ANIMAL_NAME", "Artic fox"), entry("ISAFOX", "yes")); + assertThat(records.get(2)).containsOnly(entry("ANIMAL_NAME", "Red fox"), entry("ISAFOX", "yes")); + assertThat(records.get(3)).containsOnly(entry("ANIMAL_NAME", "Raccoon"), entry("ISAFOX", "no")); + } + } + + @Test + void testSimpleCaseNoElse() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(animalName, simpleCase(animalName) + .when(isEqualTo("Artic fox"), isEqualTo("Red fox")).thenConstant("'yes'") + .end().as("IsAFox")) + .from(animalData) + .where(id, isIn(31, 32, 38, 39)) + .orderBy(id) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expected = "select animal_name, " + + "case animal_name when = #{parameters.p1,jdbcType=VARCHAR}, = #{parameters.p2,jdbcType=VARCHAR} then 'yes' end " + + "as IsAFox from AnimalData where id in " + + "(#{parameters.p3,jdbcType=INTEGER},#{parameters.p4,jdbcType=INTEGER},#{parameters.p5,jdbcType=INTEGER},#{parameters.p6,jdbcType=INTEGER}) " + + "order by id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expected); + assertThat(selectStatement.getParameters()).containsOnly( + entry("p1", "Artic fox"), + entry("p2", "Red fox"), + entry("p3", 31), + entry("p4", 32), + entry("p5", 38), + entry("p6", 39) + ); + + List> records = mapper.selectManyMappedRows(selectStatement); + assertThat(records).hasSize(4); + assertThat(records.get(0)).containsOnly(entry("ANIMAL_NAME", "Cat")); + assertThat(records.get(1)).containsOnly(entry("ANIMAL_NAME", "Artic fox"), entry("ISAFOX", "yes")); + assertThat(records.get(2)).containsOnly(entry("ANIMAL_NAME", "Red fox"), entry("ISAFOX", "yes")); + assertThat(records.get(3)).containsOnly(entry("ANIMAL_NAME", "Raccoon")); + } + } + + @Test + void testInvalidCase() { + SelectModel model = select(animalName, searchedCase() + .when(animalName,isEqualToWhenPresent((String) null)).thenConstant("Fred").end()) + .from(animalData) + .build(); + + assertThatExceptionOfType(InvalidSqlException.class) + .isThrownBy(() -> model.render(RenderingStrategies.MYBATIS3)) + .withMessage(Messages.getString("ERROR.39")); + } +} From 0abc3fa2aed41a92840b3e99e443321eefa71d24 Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Wed, 13 Mar 2024 09:55:35 -0400 Subject: [PATCH 03/19] Checks and tests for invalid case expressions --- .../dynamic/sql/select/SearchedCaseDSL.java | 13 ++++++-- .../dynamic/sql/select/SearchedCaseModel.java | 6 ++-- .../dynamic/sql/select/SimpleCaseDSL.java | 13 ++++++-- .../dynamic/sql/select/SimpleCaseModel.java | 4 ++- .../sql/select/render/SimpleCaseRenderer.java | 2 ++ .../dynamic/sql/util/messages.properties | 3 +- .../animal/data/CaseExpressionTest.java | 30 +++++++++++++++++-- 7 files changed, 59 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/mybatis/dynamic/sql/select/SearchedCaseDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/SearchedCaseDSL.java index c9eb9de4d..af540d470 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/SearchedCaseDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/SearchedCaseDSL.java @@ -20,6 +20,7 @@ import java.util.List; import org.mybatis.dynamic.sql.AndOrCriteriaGroup; +import org.mybatis.dynamic.sql.BasicColumn; import org.mybatis.dynamic.sql.BindableColumn; import org.mybatis.dynamic.sql.ColumnAndConditionCriterion; import org.mybatis.dynamic.sql.CriteriaGroup; @@ -63,12 +64,12 @@ private WhenDSL initialize(SqlCriterion sqlCriterion) { return new WhenDSL(sqlCriterion); } - public SearchedCaseDSL elseConstant(String elseValue) { + public SearchedCaseEnder elseConstant(String elseValue) { this.elseValue = elseValue; - return this; + return new SearchedCaseEnder(); } - public SearchedCaseModel end() { + public BasicColumn end() { return new SearchedCaseModel.Builder() .withElseValue(elseValue) .withWhenConditions(whenConditions) @@ -91,6 +92,12 @@ protected WhenDSL getThis() { } } + public class SearchedCaseEnder { + public BasicColumn end() { + return SearchedCaseDSL.this.end(); + } + } + public static SearchedCaseDSL searchedCase() { return new SearchedCaseDSL(); } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/SearchedCaseModel.java b/src/main/java/org/mybatis/dynamic/sql/select/SearchedCaseModel.java index 1d155c1d2..0f25b1721 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/SearchedCaseModel.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/SearchedCaseModel.java @@ -28,6 +28,7 @@ import org.mybatis.dynamic.sql.render.RenderingContext; import org.mybatis.dynamic.sql.select.render.SearchedCaseRenderer; import org.mybatis.dynamic.sql.util.FragmentAndParameters; +import org.mybatis.dynamic.sql.util.Validator; public class SearchedCaseModel implements BasicColumn { private final List whenConditions; @@ -38,6 +39,7 @@ private SearchedCaseModel(Builder builder) { whenConditions = builder.whenConditions; alias = builder.alias; elseValue = builder.elseValue; + Validator.assertNotEmpty(whenConditions, "ERROR.40"); //$NON-NLS-1$ } public Stream whenConditions() { @@ -74,8 +76,8 @@ public String thenValue() { return thenValue; } - protected SearchedWhenCondition(SqlCriterion initialCriterion, List subCriteria, - String thenValue) { + public SearchedWhenCondition(SqlCriterion initialCriterion, List subCriteria, + String thenValue) { super(initialCriterion, subCriteria); this.thenValue = Objects.requireNonNull(thenValue); } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/SimpleCaseDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/SimpleCaseDSL.java index b5faac2a4..762722566 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/SimpleCaseDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/SimpleCaseDSL.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.Objects; +import org.mybatis.dynamic.sql.BasicColumn; import org.mybatis.dynamic.sql.BindableColumn; import org.mybatis.dynamic.sql.VisitableCondition; @@ -43,12 +44,12 @@ public WhenFinisher when(VisitableCondition condition, return new WhenFinisher(condition, subsequentConditions); } - public SimpleCaseDSL elseConstant(String value) { + public SimpleCaseEnder elseConstant(String value) { elseValue = value; - return this; + return new SimpleCaseEnder(); } - public SimpleCaseModel end() { + public BasicColumn end() { return new SimpleCaseModel.Builder() .withColumn(column) .withWhenConditions(whenConditions) @@ -70,6 +71,12 @@ public SimpleCaseDSL thenConstant(String value) { } } + public class SimpleCaseEnder { + public BasicColumn end() { + return SimpleCaseDSL.this.end(); + } + } + public static SimpleCaseDSL simpleCase(BindableColumn column) { return new SimpleCaseDSL<>(column); } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/SimpleCaseModel.java b/src/main/java/org/mybatis/dynamic/sql/select/SimpleCaseModel.java index a12aef089..944f3c6c2 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/SimpleCaseModel.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/SimpleCaseModel.java @@ -27,6 +27,7 @@ import org.mybatis.dynamic.sql.render.RenderingContext; import org.mybatis.dynamic.sql.select.render.SimpleCaseRenderer; import org.mybatis.dynamic.sql.util.FragmentAndParameters; +import org.mybatis.dynamic.sql.util.Validator; public class SimpleCaseModel implements BasicColumn { private final BindableColumn column; @@ -39,6 +40,7 @@ private SimpleCaseModel(Builder builder) { whenConditions = builder.whenConditions; elseValue = builder.elseValue; alias = builder.alias; + Validator.assertNotEmpty(whenConditions, "ERROR.40"); //$NON-NLS-1$ } public BindableColumn column() { @@ -85,7 +87,7 @@ public Object thenValue() { return thenValue; } - protected SimpleWhenCondition(List> conditions, String thenValue) { + public SimpleWhenCondition(List> conditions, String thenValue) { this.conditions.addAll(conditions); this.thenValue = Objects.requireNonNull(thenValue); } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseRenderer.java index b2edebdd0..8b614cbf7 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseRenderer.java @@ -25,6 +25,7 @@ import org.mybatis.dynamic.sql.select.SimpleCaseModel; 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 SimpleCaseRenderer { @@ -83,6 +84,7 @@ private FragmentAndParameters renderConditions(SimpleCaseModel.SimpleWhenConditi } private FragmentAndParameters renderCondition(VisitableCondition condition) { + Validator.assertTrue(condition.shouldRender(renderingContext), "ERROR.39"); //$NON-NLS-1$ return condition.accept(conditionVisitor); } diff --git a/src/main/resources/org/mybatis/dynamic/sql/util/messages.properties b/src/main/resources/org/mybatis/dynamic/sql/util/messages.properties index 4a5c80fed..c23d816aa 100644 --- a/src/main/resources/org/mybatis/dynamic/sql/util/messages.properties +++ b/src/main/resources/org/mybatis/dynamic/sql/util/messages.properties @@ -55,5 +55,6 @@ ERROR.35=Multi-select statements must have at least one "union" or "union all" e ERROR.36=You must either implement the "render" or "renderWithTableAlias" method in a column or function ERROR.37=The "{0}" function does not support conditions that fail to render ERROR.38=Bound values cannot be aliased -ERROR.39=When clauses in searched case expressions must render +ERROR.39=When clauses in case expressions must render (optional conditions are not supported) +ERROR.40=Case expressions must have at least one "when" clause INTERNAL.ERROR=Internal Error {0} diff --git a/src/test/java/examples/animal/data/CaseExpressionTest.java b/src/test/java/examples/animal/data/CaseExpressionTest.java index 8b567a9b0..c8b2ab091 100644 --- a/src/test/java/examples/animal/data/CaseExpressionTest.java +++ b/src/test/java/examples/animal/data/CaseExpressionTest.java @@ -310,9 +310,9 @@ void testSimpleCaseNoElse() { } @Test - void testInvalidCase() { + void testInvalidSearchedCaseNoConditionsRender() { SelectModel model = select(animalName, searchedCase() - .when(animalName,isEqualToWhenPresent((String) null)).thenConstant("Fred").end()) + .when(animalName, isEqualToWhenPresent((String) null)).thenConstant("Fred").end()) .from(animalData) .build(); @@ -320,4 +320,30 @@ void testInvalidCase() { .isThrownBy(() -> model.render(RenderingStrategies.MYBATIS3)) .withMessage(Messages.getString("ERROR.39")); } + + @Test + void testInvalidSimpleCaseNoConditionsRender() { + SelectModel model = select(simpleCase(animalName) + .when(isEqualToWhenPresent((String) null)).thenConstant("Fred").end()) + .from(animalData) + .build(); + + assertThatExceptionOfType(InvalidSqlException.class) + .isThrownBy(() -> model.render(RenderingStrategies.MYBATIS3)) + .withMessage(Messages.getString("ERROR.39")); + } + + @Test + void testInvalidSearchedCaseNoWhenConditions() { + assertThatExceptionOfType(InvalidSqlException.class).isThrownBy( + () -> searchedCase().end() + ).withMessage(Messages.getString("ERROR.40")); + } + + @Test + void testInvalidSimpleCaseNoWhenConditions() { + assertThatExceptionOfType(InvalidSqlException.class).isThrownBy( + () -> simpleCase(id).end() + ).withMessage(Messages.getString("ERROR.40")); + } } From faa811d6dd921029b92d948d339b46d430d1c0f7 Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Wed, 13 Mar 2024 11:10:33 -0400 Subject: [PATCH 04/19] Kotlin support for case expressions --- .../dynamic/sql/select/SearchedCaseDSL.java | 4 +- .../dynamic/sql/select/SimpleCaseDSL.java | 4 +- .../util/kotlin/GroupingCriteriaCollector.kt | 2 +- .../sql/util/kotlin/elements/CaseDSLs.kt | 80 ++++ .../sql/util/kotlin/elements/SqlElements.kt | 20 + .../dynamic/sql/util/messages.properties | 2 + .../animal/data/CaseExpressionTest.java | 57 ++- .../data/AnimalDataDynamicSqlSupport.kt | 36 ++ .../kotlin/animal/data/KCaseExpressionTest.kt | 443 ++++++++++++++++++ 9 files changed, 629 insertions(+), 19 deletions(-) create mode 100644 src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/CaseDSLs.kt create mode 100644 src/test/kotlin/examples/kotlin/animal/data/AnimalDataDynamicSqlSupport.kt create mode 100644 src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt diff --git a/src/main/java/org/mybatis/dynamic/sql/select/SearchedCaseDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/SearchedCaseDSL.java index af540d470..7d838d980 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/SearchedCaseDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/SearchedCaseDSL.java @@ -64,7 +64,7 @@ private WhenDSL initialize(SqlCriterion sqlCriterion) { return new WhenDSL(sqlCriterion); } - public SearchedCaseEnder elseConstant(String elseValue) { + public SearchedCaseEnder else_(String elseValue) { this.elseValue = elseValue; return new SearchedCaseEnder(); } @@ -81,7 +81,7 @@ private WhenDSL(SqlCriterion sqlCriterion) { setInitialCriterion(sqlCriterion); } - public SearchedCaseDSL thenConstant(String value) { + public SearchedCaseDSL then(String value) { whenConditions.add(new SearchedCaseModel.SearchedWhenCondition(getInitialCriterion(), subCriteria, value)); return SearchedCaseDSL.this; } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/SimpleCaseDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/SimpleCaseDSL.java index 762722566..fd374f7ce 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/SimpleCaseDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/SimpleCaseDSL.java @@ -44,7 +44,7 @@ public WhenFinisher when(VisitableCondition condition, return new WhenFinisher(condition, subsequentConditions); } - public SimpleCaseEnder elseConstant(String value) { + public SimpleCaseEnder else_(String value) { elseValue = value; return new SimpleCaseEnder(); } @@ -65,7 +65,7 @@ private WhenFinisher(VisitableCondition condition, List conditions.addAll(subsequentConditions); } - public SimpleCaseDSL thenConstant(String value) { + public SimpleCaseDSL then(String value) { whenConditions.add(new SimpleCaseModel.SimpleWhenCondition<>(conditions, value)); return SimpleCaseDSL.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 8111f0c40..731848fb6 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 @@ -127,7 +127,7 @@ sealed class SubCriteriaCollector { */ @Suppress("TooManyFunctions") @MyBatisDslMarker -class GroupingCriteriaCollector : SubCriteriaCollector() { +open class GroupingCriteriaCollector : SubCriteriaCollector() { internal var initialCriterion: SqlCriterion? = null private set(value) { assertNull(field, "ERROR.21") //$NON-NLS-1$ 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 new file mode 100644 index 000000000..5a9810548 --- /dev/null +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/CaseDSLs.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2016-2024 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.util.kotlin.elements + +import org.mybatis.dynamic.sql.VisitableCondition +import org.mybatis.dynamic.sql.select.SearchedCaseModel.SearchedWhenCondition +import org.mybatis.dynamic.sql.select.SimpleCaseModel.SimpleWhenCondition +import org.mybatis.dynamic.sql.util.kotlin.GroupingCriteriaCollector +import org.mybatis.dynamic.sql.util.kotlin.assertNull + +class KSearchedCaseDSL { + internal var elseValue: String? = null + private set(value) { + assertNull(field, "ERROR.42") //$NON-NLS-1$ + field = value + } + internal val whenConditions = mutableListOf() + + fun `when`(dslCompleter: SearchedCaseCriteriaCollector.() -> Unit) { + val dsl = SearchedCaseCriteriaCollector().apply(dslCompleter) + whenConditions.add(SearchedWhenCondition(dsl.initialCriterion, dsl.subCriteria, dsl.thenValue)) + } + + fun `else`(value: String) { + this.elseValue = value + } +} + +class SearchedCaseCriteriaCollector : GroupingCriteriaCollector() { + internal var thenValue: String? = null + private set(value) { + assertNull(field, "ERROR.41") //$NON-NLS-1$ + field = value + } + + fun then(value: String) { + this.thenValue = value + } +} + +class KSimpleCaseDSL { + internal var elseValue: String? = null + private set(value) { + assertNull(field, "ERROR.42") //$NON-NLS-1$ + field = value + } + internal val whenConditions = mutableListOf>() + + fun `when`(condition: VisitableCondition, vararg conditions: VisitableCondition) = + SimpleCaseThenGatherer(condition, conditions.asList()) + + fun `else`(value: String) { + this.elseValue = value + } + + inner class SimpleCaseThenGatherer(val condition: VisitableCondition, + val conditions: List>) { + fun then(value: String) { + val allConditions = buildList { + add(condition) + addAll(conditions) + } + + whenConditions.add(SimpleWhenCondition(allConditions, value)) + } + } +} 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 525c17407..f03ee0e90 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 @@ -26,6 +26,8 @@ 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.SearchedCaseModel +import org.mybatis.dynamic.sql.select.SimpleCaseModel import org.mybatis.dynamic.sql.select.aggregate.Avg import org.mybatis.dynamic.sql.select.aggregate.Count import org.mybatis.dynamic.sql.select.aggregate.CountAll @@ -96,6 +98,24 @@ fun or(receiver: GroupingCriteriaReceiver): AndOrCriteriaGroup = .build() } +// case expressions +fun case(dslCompleter: KSearchedCaseDSL.() -> Unit): BasicColumn = + KSearchedCaseDSL().apply(dslCompleter).run { + SearchedCaseModel.Builder() + .withWhenConditions(whenConditions) + .withElseValue(elseValue) + .build() + } + +fun case(column: BindableColumn, dslCompleter: KSimpleCaseDSL.() -> Unit) : BasicColumn = + KSimpleCaseDSL().apply(dslCompleter).run { + SimpleCaseModel.Builder() + .withColumn(column) + .withWhenConditions(whenConditions) + .withElseValue(elseValue) + .build() + } + // aggregate support fun count(): CountAll = SqlBuilder.count() diff --git a/src/main/resources/org/mybatis/dynamic/sql/util/messages.properties b/src/main/resources/org/mybatis/dynamic/sql/util/messages.properties index c23d816aa..20b1a79c9 100644 --- a/src/main/resources/org/mybatis/dynamic/sql/util/messages.properties +++ b/src/main/resources/org/mybatis/dynamic/sql/util/messages.properties @@ -57,4 +57,6 @@ ERROR.37=The "{0}" function does not support conditions that fail to render ERROR.38=Bound values cannot be aliased ERROR.39=When clauses in case expressions must render (optional conditions are not supported) ERROR.40=Case expressions must have at least one "when" clause +ERROR.41=You cannot call "then" in a Kotlin case expression more than once +ERROR.42=You cannot call `else` in a Kotlin case expression more than once INTERNAL.ERROR=Internal Error {0} diff --git a/src/test/java/examples/animal/data/CaseExpressionTest.java b/src/test/java/examples/animal/data/CaseExpressionTest.java index c8b2ab091..41e34cc02 100644 --- a/src/test/java/examples/animal/data/CaseExpressionTest.java +++ b/src/test/java/examples/animal/data/CaseExpressionTest.java @@ -17,6 +17,7 @@ import static examples.animal.data.AnimalDataDynamicSqlSupport.animalData; import static examples.animal.data.AnimalDataDynamicSqlSupport.animalName; +import static examples.animal.data.AnimalDataDynamicSqlSupport.brainWeight; import static examples.animal.data.AnimalDataDynamicSqlSupport.id; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -26,6 +27,7 @@ import static org.mybatis.dynamic.sql.SqlBuilder.isEqualTo; import static org.mybatis.dynamic.sql.SqlBuilder.isEqualToWhenPresent; import static org.mybatis.dynamic.sql.SqlBuilder.isIn; +import static org.mybatis.dynamic.sql.SqlBuilder.isLessThan; import static org.mybatis.dynamic.sql.SqlBuilder.or; import static org.mybatis.dynamic.sql.SqlBuilder.searchedCase; import static org.mybatis.dynamic.sql.SqlBuilder.select; @@ -85,9 +87,9 @@ void testSearchedCase() { CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); SelectStatementProvider selectStatement = select(animalName, searchedCase() - .when(animalName, isEqualTo("Artic fox")).or(animalName, isEqualTo("Red fox")).thenConstant("'Fox'") - .when(animalName, isEqualTo("Little brown bat")).or(animalName, isEqualTo("Big brown bat")).thenConstant("'Bat'") - .elseConstant("cast('Not a Fox or a bat' as varchar(25))").end().as("AnimalType")) + .when(animalName, isEqualTo("Artic fox")).or(animalName, isEqualTo("Red fox")).then("'Fox'") + .when(animalName, isEqualTo("Little brown bat")).or(animalName, isEqualTo("Big brown bat")).then("'Bat'") + .else_("cast('Not a Fox or a bat' as varchar(25))").end().as("AnimalType")) .from(animalData, "a") .where(id, isIn(2, 3, 31, 32, 38, 39)) .orderBy(id) @@ -134,8 +136,8 @@ void testSearchedCaseNoElse() { CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); SelectStatementProvider selectStatement = select(animalName, searchedCase() - .when(animalName, isEqualTo("Artic fox")).or(animalName, isEqualTo("Red fox")).thenConstant("'Fox'") - .when(animalName, isEqualTo("Little brown bat")).or(animalName, isEqualTo("Big brown bat")).thenConstant("'Bat'") + .when(animalName, isEqualTo("Artic fox")).or(animalName, isEqualTo("Red fox")).then("'Fox'") + .when(animalName, isEqualTo("Little brown bat")).or(animalName, isEqualTo("Big brown bat")).then("'Bat'") .end().as("AnimalType")) .from(animalData, "a") .where(id, isIn(2, 3, 31, 32, 38, 39)) @@ -183,10 +185,10 @@ void testSearchedCaseWithGroup() { CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); SelectStatementProvider selectStatement = select(animalName, searchedCase() - .when(animalName, isEqualTo("Artic fox")).or(animalName, isEqualTo("Red fox")).thenConstant("'Fox'") - .when(animalName, isEqualTo("Little brown bat")).or(animalName, isEqualTo("Big brown bat")).thenConstant("'Bat'") - .when(group(animalName, isEqualTo("Cat"), and(id, isEqualTo(31))), or(id, isEqualTo(39))).thenConstant("'Fred'") - .elseConstant("cast('Not a Fox or a bat' as varchar(25))").end().as("AnimalType")) + .when(animalName, isEqualTo("Artic fox")).or(animalName, isEqualTo("Red fox")).then("'Fox'") + .when(animalName, isEqualTo("Little brown bat")).or(animalName, isEqualTo("Big brown bat")).then("'Bat'") + .when(group(animalName, isEqualTo("Cat"), and(id, isEqualTo(31))), or(id, isEqualTo(39))).then("'Fred'") + .else_("cast('Not a Fox or a bat' as varchar(25))").end().as("AnimalType")) .from(animalData, "a") .where(id, isIn(2, 3, 4, 31, 32, 38, 39)) .orderBy(id) @@ -233,14 +235,41 @@ void testSearchedCaseWithGroup() { } } + @Test + void testSimpleCassLessThan() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(animalName, simpleCase(brainWeight) + .when(isLessThan(4.0)).then("'small brain'") + .else_("'large brain'").end().as("brain_size")) + .from(animalData) + .where(id, isIn(31, 32, 38, 39)) + .orderBy(id) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expected = "select animal_name, case brain_weight " + + "when < #{parameters.p1,jdbcType=DOUBLE} then 'small brain' " + + "else 'large brain' end as brain_size " + + "from AnimalData where id in (" + + "#{parameters.p2,jdbcType=INTEGER},#{parameters.p3,jdbcType=INTEGER}," + + "#{parameters.p4,jdbcType=INTEGER},#{parameters.p5,jdbcType=INTEGER}) " + + "order by id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expected); + List> records = mapper.selectManyMappedRows(selectStatement); + assertThat(records).hasSize(7); + } + } + @Test void testSimpleCase() { try (SqlSession sqlSession = sqlSessionFactory.openSession()) { CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); SelectStatementProvider selectStatement = select(animalName, simpleCase(animalName) - .when(isEqualTo("Artic fox"), isEqualTo("Red fox")).thenConstant("'yes'") - .elseConstant("cast('no' as VARCHAR(3))").end().as("IsAFox")) + .when(isEqualTo("Artic fox"), isEqualTo("Red fox")).then("'yes'") + .else_("cast('no' as VARCHAR(3))").end().as("IsAFox")) .from(animalData) .where(id, isIn(31, 32, 38, 39)) .orderBy(id) @@ -277,7 +306,7 @@ void testSimpleCaseNoElse() { CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); SelectStatementProvider selectStatement = select(animalName, simpleCase(animalName) - .when(isEqualTo("Artic fox"), isEqualTo("Red fox")).thenConstant("'yes'") + .when(isEqualTo("Artic fox"), isEqualTo("Red fox")).then("'yes'") .end().as("IsAFox")) .from(animalData) .where(id, isIn(31, 32, 38, 39)) @@ -312,7 +341,7 @@ void testSimpleCaseNoElse() { @Test void testInvalidSearchedCaseNoConditionsRender() { SelectModel model = select(animalName, searchedCase() - .when(animalName, isEqualToWhenPresent((String) null)).thenConstant("Fred").end()) + .when(animalName, isEqualToWhenPresent((String) null)).then("Fred").end()) .from(animalData) .build(); @@ -324,7 +353,7 @@ void testInvalidSearchedCaseNoConditionsRender() { @Test void testInvalidSimpleCaseNoConditionsRender() { SelectModel model = select(simpleCase(animalName) - .when(isEqualToWhenPresent((String) null)).thenConstant("Fred").end()) + .when(isEqualToWhenPresent((String) null)).then("Fred").end()) .from(animalData) .build(); diff --git a/src/test/kotlin/examples/kotlin/animal/data/AnimalDataDynamicSqlSupport.kt b/src/test/kotlin/examples/kotlin/animal/data/AnimalDataDynamicSqlSupport.kt new file mode 100644 index 000000000..e5cedd8a6 --- /dev/null +++ b/src/test/kotlin/examples/kotlin/animal/data/AnimalDataDynamicSqlSupport.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2016-2024 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.kotlin.animal.data + +import java.sql.JDBCType + +import org.mybatis.dynamic.sql.SqlTable +import org.mybatis.dynamic.sql.util.kotlin.elements.column + +object AnimalDataDynamicSqlSupport { + val animalData = AnimalData() + val id = animalData.id + val animalName = animalData.animalName + val bodyWeight = animalData.bodyWeight + val brainWeight = animalData.brainWeight + + class AnimalData : SqlTable("AnimalData") { + val id = column(name = "id", jdbcType = JDBCType.INTEGER) + val animalName = column(name = "animal_name", jdbcType = JDBCType.VARCHAR) + val bodyWeight = column(name = "body_weight", jdbcType = JDBCType.DOUBLE) + val brainWeight = column(name = "brain_weight", jdbcType = JDBCType.DOUBLE) + } +} diff --git a/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt b/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt new file mode 100644 index 000000000..8ff3d15c1 --- /dev/null +++ b/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt @@ -0,0 +1,443 @@ +/* + * Copyright 2016-2024 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.kotlin.animal.data + +import examples.kotlin.animal.data.AnimalDataDynamicSqlSupport.animalData +import examples.kotlin.animal.data.AnimalDataDynamicSqlSupport.animalName +import examples.kotlin.animal.data.AnimalDataDynamicSqlSupport.id +import examples.kotlin.mybatis3.TestUtils +import org.apache.ibatis.session.ExecutorType +import org.apache.ibatis.session.SqlSession +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatExceptionOfType +import org.assertj.core.api.Assertions.entry +import org.junit.jupiter.api.Test +import org.mybatis.dynamic.sql.exception.InvalidSqlException +import org.mybatis.dynamic.sql.util.Messages +import org.mybatis.dynamic.sql.util.kotlin.KInvalidSQLException +import org.mybatis.dynamic.sql.util.kotlin.elements.case +import org.mybatis.dynamic.sql.util.kotlin.elements.isEqualTo +import org.mybatis.dynamic.sql.util.kotlin.mybatis3.select +import org.mybatis.dynamic.sql.util.mybatis3.CommonSelectMapper + +class KCaseExpressionTest { + private fun newSession(executorType: ExecutorType = ExecutorType.REUSE): SqlSession { + // this method re-initializes the database with every test - needed because of autoincrement fields + return TestUtils.buildSqlSessionFactory { + withInitializationScript("/examples/animal/data/CreateAnimalData.sql") + withMapper(CommonSelectMapper::class) + }.openSession(executorType) + } + + @Test + fun testSearchedCase() { + newSession().use { session -> + val mapper = session.getMapper(CommonSelectMapper::class.java) + + val selectStatement = select(animalName, + case { + `when` { + animalName isEqualTo "Artic fox" + or { animalName isEqualTo "Red fox" } + then("'Fox'") + } + `when` { + animalName isEqualTo "Little brown bat" + or { animalName isEqualTo "Big brown bat" } + then("'Bat'") + } + `else`("cast('Not a Fox or a bat' as varchar(25))") + }.`as`("AnimalType") + ) { + from(animalData, "a") + where { id.isIn(2, 3, 31, 32, 38, 39) } + orderBy(id) + } + + val expected = "select a.animal_name, case " + + "when a.animal_name = #{parameters.p1,jdbcType=VARCHAR} or a.animal_name = #{parameters.p2,jdbcType=VARCHAR} then 'Fox' " + + "when a.animal_name = #{parameters.p3,jdbcType=VARCHAR} or a.animal_name = #{parameters.p4,jdbcType=VARCHAR} then 'Bat' " + + "else cast('Not a Fox or a bat' as varchar(25)) end as AnimalType " + + "from AnimalData a where a.id in (" + + "#{parameters.p5,jdbcType=INTEGER},#{parameters.p6,jdbcType=INTEGER}," + + "#{parameters.p7,jdbcType=INTEGER},#{parameters.p8,jdbcType=INTEGER},#{parameters.p9,jdbcType=INTEGER}," + + "#{parameters.p10,jdbcType=INTEGER}) " + + "order by id" + assertThat(selectStatement.selectStatement).isEqualTo(expected) + assertThat(selectStatement.parameters).containsOnly( + entry("p1", "Artic fox"), + entry("p2", "Red fox"), + entry("p3", "Little brown bat"), + entry("p4", "Big brown bat"), + entry("p5", 2), + entry("p6", 3), + entry("p7", 31), + entry("p8", 32), + entry("p9", 38), + entry("p10", 39) + ) + + val records = mapper.selectManyMappedRows(selectStatement) + assertThat(records).hasSize(6) + assertThat(records[0]).containsOnly( + entry("ANIMAL_NAME", "Little brown bat"), + entry("ANIMALTYPE", "Bat") + ) + assertThat(records[1]).containsOnly( + entry("ANIMAL_NAME", "Big brown bat"), + entry("ANIMALTYPE", "Bat") + ) + assertThat(records[2]).containsOnly( + entry("ANIMAL_NAME", "Cat"), + entry("ANIMALTYPE", "Not a Fox or a bat") + ) + assertThat(records[3]).containsOnly( + entry("ANIMAL_NAME", "Artic fox"), + entry("ANIMALTYPE", "Fox") + ) + assertThat(records[4]).containsOnly( + entry("ANIMAL_NAME", "Red fox"), + entry("ANIMALTYPE", "Fox") + ) + assertThat(records[5]).containsOnly( + entry("ANIMAL_NAME", "Raccoon"), + entry("ANIMALTYPE", "Not a Fox or a bat") + ) + } + } + + @Test + fun testSearchedCaseNoElse() { + newSession().use { session -> + val mapper = session.getMapper(CommonSelectMapper::class.java) + + val selectStatement = select( + animalName, + case { + `when` { + animalName isEqualTo "Artic fox" + or { animalName isEqualTo "Red fox" } + then("'Fox'") + } + `when` { + animalName isEqualTo "Little brown bat" + or { animalName isEqualTo "Big brown bat" } + then("'Bat'") + } + }.`as`("AnimalType") + ) { + from(animalData, "a") + where { id.isIn(2, 3, 31, 32, 38, 39) } + orderBy(id) + } + + val expected = "select a.animal_name, case " + + "when a.animal_name = #{parameters.p1,jdbcType=VARCHAR} or a.animal_name = #{parameters.p2,jdbcType=VARCHAR} then 'Fox' " + + "when a.animal_name = #{parameters.p3,jdbcType=VARCHAR} or a.animal_name = #{parameters.p4,jdbcType=VARCHAR} then 'Bat' " + + "end as AnimalType " + + "from AnimalData a where a.id in (" + + "#{parameters.p5,jdbcType=INTEGER},#{parameters.p6,jdbcType=INTEGER}," + + "#{parameters.p7,jdbcType=INTEGER},#{parameters.p8,jdbcType=INTEGER},#{parameters.p9,jdbcType=INTEGER}," + + "#{parameters.p10,jdbcType=INTEGER}) " + + "order by id" + assertThat(selectStatement.selectStatement).isEqualTo(expected) + assertThat(selectStatement.parameters) + .containsOnly( + entry("p1", "Artic fox"), + entry("p2", "Red fox"), + entry("p3", "Little brown bat"), + entry("p4", "Big brown bat"), + entry("p5", 2), + entry("p6", 3), + entry("p7", 31), + entry("p8", 32), + entry("p9", 38), + entry("p10", 39) + ) + + val records = + mapper.selectManyMappedRows(selectStatement) + assertThat(records).hasSize(6) + assertThat(records[0]).containsOnly( + entry("ANIMAL_NAME", "Little brown bat"), + entry("ANIMALTYPE", "Bat") + ) + assertThat(records[1]).containsOnly( + entry("ANIMAL_NAME", "Big brown bat"), + entry("ANIMALTYPE", "Bat") + ) + assertThat(records[2]).containsOnly(entry("ANIMAL_NAME", "Cat")) + assertThat(records[3]).containsOnly( + entry("ANIMAL_NAME", "Artic fox"), + entry("ANIMALTYPE", "Fox") + ) + assertThat(records[4]).containsOnly( + entry("ANIMAL_NAME", "Red fox"), + entry("ANIMALTYPE", "Fox") + ) + assertThat(records[5]).containsOnly(entry("ANIMAL_NAME", "Raccoon")) + } + } + + @Test + fun testSearchedCaseWithGroup() { + newSession().use { session -> + val mapper = session.getMapper(CommonSelectMapper::class.java) + + val selectStatement = select( + animalName, + case { + `when` { + animalName isEqualTo "Artic fox" + or { animalName isEqualTo "Red fox" } + then("'Fox'") + } + `when` { + animalName isEqualTo "Little brown bat" + or { animalName isEqualTo "Big brown bat" } + then("'Bat'") + } + `when` { + group { + animalName isEqualTo "Cat" + and { id isEqualTo 31 } + } + or { id isEqualTo 39 } + then("'Fred'") + } + `else`("cast('Not a Fox or a bat' as varchar(25))") + }.`as`("AnimalType") + ) { + from(animalData, "a") + where { id.isIn(2, 3, 4, 31, 32, 38, 39) } + orderBy(id) + } + + val expected = "select a.animal_name, case " + + "when a.animal_name = #{parameters.p1,jdbcType=VARCHAR} or a.animal_name = #{parameters.p2,jdbcType=VARCHAR} then 'Fox' " + + "when a.animal_name = #{parameters.p3,jdbcType=VARCHAR} or a.animal_name = #{parameters.p4,jdbcType=VARCHAR} then 'Bat' " + + "when (a.animal_name = #{parameters.p5,jdbcType=VARCHAR} and a.id = #{parameters.p6,jdbcType=INTEGER}) or a.id = #{parameters.p7,jdbcType=INTEGER} then 'Fred' " + + "else cast('Not a Fox or a bat' as varchar(25)) end as AnimalType " + + "from AnimalData a where a.id in (" + + "#{parameters.p8,jdbcType=INTEGER},#{parameters.p9,jdbcType=INTEGER}," + + "#{parameters.p10,jdbcType=INTEGER},#{parameters.p11,jdbcType=INTEGER},#{parameters.p12,jdbcType=INTEGER}," + + "#{parameters.p13,jdbcType=INTEGER},#{parameters.p14,jdbcType=INTEGER}) " + + "order by id" + assertThat(selectStatement.selectStatement).isEqualTo(expected) + assertThat(selectStatement.parameters) + .containsOnly( + entry("p1", "Artic fox"), + entry("p2", "Red fox"), + entry("p3", "Little brown bat"), + entry("p4", "Big brown bat"), + entry("p5", "Cat"), + entry("p6", 31), + entry("p7", 39), + entry("p8", 2), + entry("p9", 3), + entry("p10", 4), + entry("p11", 31), + entry("p12", 32), + entry("p13", 38), + entry("p14", 39) + ) + + val records = mapper.selectManyMappedRows(selectStatement) + assertThat(records).hasSize(7) + assertThat(records[0]).containsOnly( + entry("ANIMAL_NAME", "Little brown bat"), + entry("ANIMALTYPE", "Bat") + ) + assertThat(records[1]).containsOnly( + entry("ANIMAL_NAME", "Big brown bat"), + entry("ANIMALTYPE", "Bat") + ) + assertThat(records[2]).containsOnly( + entry("ANIMAL_NAME", "Mouse"), + entry("ANIMALTYPE", "Not a Fox or a bat") + ) + assertThat(records[3]).containsOnly( + entry("ANIMAL_NAME", "Cat"), + entry("ANIMALTYPE", "Fred") + ) + assertThat(records[4]).containsOnly( + entry("ANIMAL_NAME", "Artic fox"), + entry("ANIMALTYPE", "Fox") + ) + assertThat(records[5]).containsOnly( + entry("ANIMAL_NAME", "Red fox"), + entry("ANIMALTYPE", "Fox") + ) + assertThat(records[6]).containsOnly( + entry("ANIMAL_NAME", "Raccoon"), + entry("ANIMALTYPE", "Fred") + ) + } + } + + @Test + fun testSimpleCase() { + newSession().use { session -> + val mapper = session.getMapper(CommonSelectMapper::class.java) + + val selectStatement = select( + animalName, + case(animalName) { + `when` (isEqualTo("Artic fox"), isEqualTo("Red fox")).then("'yes'") + `else`("cast('no' as VARCHAR(3))") + }.`as`("IsAFox") + ) { + from(animalData) + where { id.isIn(31, 32, 38, 39) } + orderBy(id) + } + + val expected = "select animal_name, " + + "case animal_name when = #{parameters.p1,jdbcType=VARCHAR}, = #{parameters.p2,jdbcType=VARCHAR} then 'yes' else cast('no' as VARCHAR(3)) end " + + "as IsAFox from AnimalData where id in " + + "(#{parameters.p3,jdbcType=INTEGER},#{parameters.p4,jdbcType=INTEGER},#{parameters.p5,jdbcType=INTEGER},#{parameters.p6,jdbcType=INTEGER}) " + + "order by id" + assertThat(selectStatement.selectStatement).isEqualTo(expected) + assertThat(selectStatement.parameters) + .containsOnly( + entry("p1", "Artic fox"), + entry("p2", "Red fox"), + entry("p3", 31), + entry("p4", 32), + entry("p5", 38), + entry("p6", 39) + ) + + val records = mapper.selectManyMappedRows(selectStatement) + assertThat(records).hasSize(4) + assertThat(records[0]).containsOnly( + entry("ANIMAL_NAME", "Cat"), + entry("ISAFOX", "no") + ) + assertThat(records[1]).containsOnly( + entry("ANIMAL_NAME", "Artic fox"), + entry("ISAFOX", "yes") + ) + assertThat(records[2]).containsOnly( + entry("ANIMAL_NAME", "Red fox"), + entry("ISAFOX", "yes") + ) + assertThat(records[3]).containsOnly( + entry("ANIMAL_NAME", "Raccoon"), + entry("ISAFOX", "no") + ) + } + } + + @Test + fun testSimpleCaseNoElse() { + newSession().use { session -> + val mapper = session.getMapper(CommonSelectMapper::class.java) + + val selectStatement = select( + animalName, + case(animalName) { + `when`(isEqualTo("Artic fox"), isEqualTo("Red fox")).then("'yes'") + }.`as`("IsAFox") + ) { + from(animalData) + where { id.isIn(31, 32, 38, 39) } + orderBy(id) + } + + val expected = "select animal_name, " + + "case animal_name when = #{parameters.p1,jdbcType=VARCHAR}, = #{parameters.p2,jdbcType=VARCHAR} then 'yes' end " + + "as IsAFox from AnimalData where id in " + + "(#{parameters.p3,jdbcType=INTEGER},#{parameters.p4,jdbcType=INTEGER},#{parameters.p5,jdbcType=INTEGER},#{parameters.p6,jdbcType=INTEGER}) " + + "order by id" + assertThat(selectStatement.selectStatement).isEqualTo(expected) + assertThat(selectStatement.parameters) + .containsOnly( + entry("p1", "Artic fox"), + entry("p2", "Red fox"), + entry("p3", 31), + entry("p4", 32), + entry("p5", 38), + entry("p6", 39) + ) + + val records = mapper.selectManyMappedRows(selectStatement) + assertThat(records).hasSize(4) + assertThat(records[0]).containsOnly(entry("ANIMAL_NAME", "Cat")) + assertThat(records[1]).containsOnly( + entry("ANIMAL_NAME", "Artic fox"), + entry("ISAFOX", "yes") + ) + assertThat(records[2]).containsOnly( + entry("ANIMAL_NAME", "Red fox"), + entry("ISAFOX", "yes") + ) + assertThat(records[3]).containsOnly(entry("ANIMAL_NAME", "Raccoon")) + } + } + + @Test + fun testInvalidDoubleElseSimple() { + assertThatExceptionOfType(KInvalidSQLException::class.java).isThrownBy { + case(animalName) { + `when`(isEqualTo("Artic fox"), isEqualTo("Red fox")).then("'yes'") + `else`("Fred") + `else`("Wilma") + } + }.withMessage(Messages.getString("ERROR.42")) + } + + @Test + fun testInvalidDoubleElseSearched() { + assertThatExceptionOfType(KInvalidSQLException::class.java).isThrownBy { + case { + `when` { + id isEqualTo 22 + then("'yes'") + } + `else`("Fred") + `else`("Wilma") + } + }.withMessage(Messages.getString("ERROR.42")) + } + + @Test + fun testInvalidDoubleThenSearched() { + assertThatExceptionOfType(KInvalidSQLException::class.java).isThrownBy { + case { + `when` { + id isEqualTo 22 + then("'yes'") + then("'no'") + } + } + }.withMessage(Messages.getString("ERROR.41")) + } + + @Test + fun testInvalidSearchedMissingWhen() { + assertThatExceptionOfType(InvalidSqlException::class.java).isThrownBy { + select(case { }){ from(animalData) } + }.withMessage(Messages.getString("ERROR.40")) + } + + @Test + fun testInvalidSimpleMissingWhen() { + assertThatExceptionOfType(InvalidSqlException::class.java).isThrownBy { + select(case (id) { }){ from (animalData) } + }.withMessage(Messages.getString("ERROR.40")) + } +} From c3c971767bfb43e6d141b4bd52920817b3f7b3e7 Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Wed, 13 Mar 2024 15:29:42 -0400 Subject: [PATCH 05/19] Better support for string constants --- .../org/mybatis/dynamic/sql/SqlBuilder.java | 6 +- .../dynamic/sql/select/SearchedCaseDSL.java | 21 +- .../dynamic/sql/select/SearchedCaseModel.java | 14 +- .../dynamic/sql/select/SimpleCaseDSL.java | 16 +- .../dynamic/sql/select/SimpleCaseModel.java | 10 +- .../dynamic/sql/util/StringUtilities.java | 4 + .../sql/util/kotlin/elements/CaseDSLs.kt | 27 ++- .../animal/data/CaseExpressionTest.java | 186 ++++++++++++----- .../kotlin/animal/data/KCaseExpressionTest.kt | 193 +++++++++++++++--- 9 files changed, 377 insertions(+), 100 deletions(-) diff --git a/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java index f1b58fb83..54164e75b 100644 --- a/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java +++ b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java @@ -428,11 +428,13 @@ static AndOrCriteriaGroup and(List subCriteria) { } // case expressions - static SimpleCaseDSL simpleCase(BindableColumn column) { + @SuppressWarnings("java:S100") + static SimpleCaseDSL case_(BindableColumn column) { return SimpleCaseDSL.simpleCase(column); } - static SearchedCaseDSL searchedCase() { + @SuppressWarnings("java:S100") + static SearchedCaseDSL case_() { return SearchedCaseDSL.searchedCase(); } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/SearchedCaseDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/SearchedCaseDSL.java index 7d838d980..33d6adcb1 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/SearchedCaseDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/SearchedCaseDSL.java @@ -15,6 +15,8 @@ */ package org.mybatis.dynamic.sql.select; +import static org.mybatis.dynamic.sql.util.StringUtilities.quoteStringForSQL; + import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -30,7 +32,7 @@ public class SearchedCaseDSL { private final List whenConditions = new ArrayList<>(); - private String elseValue; + private Object elseValue; public WhenDSL when(BindableColumn column, VisitableCondition condition, AndOrCriteriaGroup... subCriteria) { @@ -64,8 +66,15 @@ private WhenDSL initialize(SqlCriterion sqlCriterion) { return new WhenDSL(sqlCriterion); } - public SearchedCaseEnder else_(String elseValue) { - this.elseValue = elseValue; + @SuppressWarnings("java:S100") + public SearchedCaseEnder else_(String value) { + this.elseValue = quoteStringForSQL(value); + return new SearchedCaseEnder(); + } + + @SuppressWarnings("java:S100") + public SearchedCaseEnder else_(Object value) { + this.elseValue = value; return new SearchedCaseEnder(); } @@ -82,6 +91,12 @@ private WhenDSL(SqlCriterion sqlCriterion) { } public SearchedCaseDSL then(String value) { + whenConditions.add(new SearchedCaseModel.SearchedWhenCondition(getInitialCriterion(), subCriteria, + quoteStringForSQL(value))); + return SearchedCaseDSL.this; + } + + public SearchedCaseDSL then(Object value) { whenConditions.add(new SearchedCaseModel.SearchedWhenCondition(getInitialCriterion(), subCriteria, value)); return SearchedCaseDSL.this; } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/SearchedCaseModel.java b/src/main/java/org/mybatis/dynamic/sql/select/SearchedCaseModel.java index 0f25b1721..061b81eac 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/SearchedCaseModel.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/SearchedCaseModel.java @@ -32,7 +32,7 @@ public class SearchedCaseModel implements BasicColumn { private final List whenConditions; - private final String elseValue; + private final Object elseValue; private final String alias; private SearchedCaseModel(Builder builder) { @@ -46,7 +46,7 @@ public Stream whenConditions() { return whenConditions.stream(); } - public Optional elseValue() { + public Optional elseValue() { return Optional.ofNullable(elseValue); } @@ -70,14 +70,14 @@ public FragmentAndParameters render(RenderingContext renderingContext) { public static class SearchedWhenCondition extends AbstractBooleanExpressionModel { - private final String thenValue; + private final Object thenValue; - public String thenValue() { + public Object thenValue() { return thenValue; } public SearchedWhenCondition(SqlCriterion initialCriterion, List subCriteria, - String thenValue) { + Object thenValue) { super(initialCriterion, subCriteria); this.thenValue = Objects.requireNonNull(thenValue); } @@ -85,7 +85,7 @@ public SearchedWhenCondition(SqlCriterion initialCriterion, List whenConditions = new ArrayList<>(); - private String elseValue; + private Object elseValue; private String alias; public Builder withWhenConditions(List whenConditions) { @@ -93,7 +93,7 @@ public Builder withWhenConditions(List whenConditions) { return this; } - public Builder withElseValue(String elseValue) { + public Builder withElseValue(Object elseValue) { this.elseValue = elseValue; return this; } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/SimpleCaseDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/SimpleCaseDSL.java index fd374f7ce..dee90fcaa 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/SimpleCaseDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/SimpleCaseDSL.java @@ -15,6 +15,8 @@ */ package org.mybatis.dynamic.sql.select; +import static org.mybatis.dynamic.sql.util.StringUtilities.quoteStringForSQL; + import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -27,7 +29,7 @@ public class SimpleCaseDSL { private final BindableColumn column; private final List> whenConditions = new ArrayList<>(); - private String elseValue; + private Object elseValue; private SimpleCaseDSL(BindableColumn column) { this.column = Objects.requireNonNull(column); @@ -44,7 +46,14 @@ public WhenFinisher when(VisitableCondition condition, return new WhenFinisher(condition, subsequentConditions); } + @SuppressWarnings("java:S100") public SimpleCaseEnder else_(String value) { + elseValue = quoteStringForSQL(value); + return new SimpleCaseEnder(); + } + + @SuppressWarnings("java:S100") + public SimpleCaseEnder else_(Object value) { elseValue = value; return new SimpleCaseEnder(); } @@ -66,6 +75,11 @@ private WhenFinisher(VisitableCondition condition, List } public SimpleCaseDSL then(String value) { + whenConditions.add(new SimpleCaseModel.SimpleWhenCondition<>(conditions, quoteStringForSQL(value))); + return SimpleCaseDSL.this; + } + + public SimpleCaseDSL then(Object value) { whenConditions.add(new SimpleCaseModel.SimpleWhenCondition<>(conditions, value)); return SimpleCaseDSL.this; } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/SimpleCaseModel.java b/src/main/java/org/mybatis/dynamic/sql/select/SimpleCaseModel.java index 944f3c6c2..737257f64 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/SimpleCaseModel.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/SimpleCaseModel.java @@ -32,7 +32,7 @@ public class SimpleCaseModel implements BasicColumn { private final BindableColumn column; private final List> whenConditions; - private final String elseValue; + private final Object elseValue; private final String alias; private SimpleCaseModel(Builder builder) { @@ -77,7 +77,7 @@ public FragmentAndParameters render(RenderingContext renderingContext) { public static class SimpleWhenCondition { private final List> conditions = new ArrayList<>(); - private final String thenValue; + private final Object thenValue; public Stream> conditions() { return conditions.stream(); @@ -87,7 +87,7 @@ public Object thenValue() { return thenValue; } - public SimpleWhenCondition(List> conditions, String thenValue) { + public SimpleWhenCondition(List> conditions, Object thenValue) { this.conditions.addAll(conditions); this.thenValue = Objects.requireNonNull(thenValue); } @@ -96,7 +96,7 @@ public SimpleWhenCondition(List> conditions, String thenVa public static class Builder { private BindableColumn column; private final List> whenConditions = new ArrayList<>(); - private String elseValue; + private Object elseValue; private String alias; public Builder withColumn(BindableColumn column) { @@ -109,7 +109,7 @@ public Builder withWhenConditions(List> whenConditions return this; } - public Builder withElseValue(String elseValue) { + public Builder withElseValue(Object elseValue) { this.elseValue = elseValue; return this; } diff --git a/src/main/java/org/mybatis/dynamic/sql/util/StringUtilities.java b/src/main/java/org/mybatis/dynamic/sql/util/StringUtilities.java index 0606c0e7c..5fe080d52 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/StringUtilities.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/StringUtilities.java @@ -52,4 +52,8 @@ static String toCamelCase(String inputString) { return sb.toString(); } + + static String quoteStringForSQL(String value) { + return "'" + value + "'"; //$NON-NLS-1$ //$NON-NLS-2$ + } } 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 5a9810548..9c592afe1 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 @@ -22,7 +22,7 @@ import org.mybatis.dynamic.sql.util.kotlin.GroupingCriteriaCollector import org.mybatis.dynamic.sql.util.kotlin.assertNull class KSearchedCaseDSL { - internal var elseValue: String? = null + internal var elseValue: Any? = null private set(value) { assertNull(field, "ERROR.42") //$NON-NLS-1$ field = value @@ -35,24 +35,32 @@ class KSearchedCaseDSL { } fun `else`(value: String) { + this.elseValue = "'$value'" + } + + fun `else`(value: Any) { this.elseValue = value } } class SearchedCaseCriteriaCollector : GroupingCriteriaCollector() { - internal var thenValue: String? = null + internal var thenValue: Any? = null private set(value) { assertNull(field, "ERROR.41") //$NON-NLS-1$ field = value } fun then(value: String) { + this.thenValue = "'$value'" + } + + fun then(value: Any) { this.thenValue = value } } class KSimpleCaseDSL { - internal var elseValue: String? = null + internal var elseValue: Any? = null private set(value) { assertNull(field, "ERROR.42") //$NON-NLS-1$ field = value @@ -63,6 +71,10 @@ class KSimpleCaseDSL { SimpleCaseThenGatherer(condition, conditions.asList()) fun `else`(value: String) { + this.elseValue = "'$value'" + } + + fun `else`(value: Any) { this.elseValue = value } @@ -74,6 +86,15 @@ class KSimpleCaseDSL { addAll(conditions) } + whenConditions.add(SimpleWhenCondition(allConditions, "'$value'")) + } + + fun then(value: Any) { + val allConditions = buildList { + add(condition) + addAll(conditions) + } + whenConditions.add(SimpleWhenCondition(allConditions, value)) } } diff --git a/src/test/java/examples/animal/data/CaseExpressionTest.java b/src/test/java/examples/animal/data/CaseExpressionTest.java index 41e34cc02..855e0eef9 100644 --- a/src/test/java/examples/animal/data/CaseExpressionTest.java +++ b/src/test/java/examples/animal/data/CaseExpressionTest.java @@ -29,9 +29,8 @@ import static org.mybatis.dynamic.sql.SqlBuilder.isIn; import static org.mybatis.dynamic.sql.SqlBuilder.isLessThan; import static org.mybatis.dynamic.sql.SqlBuilder.or; -import static org.mybatis.dynamic.sql.SqlBuilder.searchedCase; +import static org.mybatis.dynamic.sql.SqlBuilder.case_; import static org.mybatis.dynamic.sql.SqlBuilder.select; -import static org.mybatis.dynamic.sql.SqlBuilder.simpleCase; import java.io.InputStream; import java.io.InputStreamReader; @@ -50,9 +49,12 @@ import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mybatis.dynamic.sql.SqlBuilder; import org.mybatis.dynamic.sql.exception.InvalidSqlException; import org.mybatis.dynamic.sql.render.RenderingStrategies; +import org.mybatis.dynamic.sql.select.SearchedCaseDSL; import org.mybatis.dynamic.sql.select.SelectModel; +import org.mybatis.dynamic.sql.select.SimpleCaseDSL; import org.mybatis.dynamic.sql.select.render.SelectStatementProvider; import org.mybatis.dynamic.sql.util.Messages; import org.mybatis.dynamic.sql.util.mybatis3.CommonSelectMapper; @@ -82,14 +84,14 @@ void setup() throws Exception { } @Test - void testSearchedCase() { + void testSearchedCaseWithStrings() { try (SqlSession sqlSession = sqlSessionFactory.openSession()) { CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); - SelectStatementProvider selectStatement = select(animalName, searchedCase() - .when(animalName, isEqualTo("Artic fox")).or(animalName, isEqualTo("Red fox")).then("'Fox'") - .when(animalName, isEqualTo("Little brown bat")).or(animalName, isEqualTo("Big brown bat")).then("'Bat'") - .else_("cast('Not a Fox or a bat' as varchar(25))").end().as("AnimalType")) + SelectStatementProvider selectStatement = select(animalName, case_() + .when(animalName, isEqualTo("Artic fox")).or(animalName, isEqualTo("Red fox")).then("Fox") + .when(animalName, isEqualTo("Little brown bat")).or(animalName, isEqualTo("Big brown bat")).then("Bat") + .else_("Not a Fox or a bat").end().as("AnimalType")) .from(animalData, "a") .where(id, isIn(2, 3, 31, 32, 38, 39)) .orderBy(id) @@ -99,7 +101,7 @@ void testSearchedCase() { String expected = "select a.animal_name, case " + "when a.animal_name = #{parameters.p1,jdbcType=VARCHAR} or a.animal_name = #{parameters.p2,jdbcType=VARCHAR} then 'Fox' " + "when a.animal_name = #{parameters.p3,jdbcType=VARCHAR} or a.animal_name = #{parameters.p4,jdbcType=VARCHAR} then 'Bat' " + - "else cast('Not a Fox or a bat' as varchar(25)) end as AnimalType " + + "else 'Not a Fox or a bat' end as AnimalType " + "from AnimalData a where a.id in (" + "#{parameters.p5,jdbcType=INTEGER},#{parameters.p6,jdbcType=INTEGER}," + "#{parameters.p7,jdbcType=INTEGER},#{parameters.p8,jdbcType=INTEGER},#{parameters.p9,jdbcType=INTEGER}," + @@ -121,23 +123,72 @@ void testSearchedCase() { List> records = mapper.selectManyMappedRows(selectStatement); assertThat(records).hasSize(6); - assertThat(records.get(0)).containsOnly(entry("ANIMAL_NAME", "Little brown bat"), entry("ANIMALTYPE", "Bat")); - assertThat(records.get(1)).containsOnly(entry("ANIMAL_NAME", "Big brown bat"), entry("ANIMALTYPE", "Bat")); + assertThat(records.get(0)).containsOnly(entry("ANIMAL_NAME", "Little brown bat"), entry("ANIMALTYPE", "Bat ")); + assertThat(records.get(1)).containsOnly(entry("ANIMAL_NAME", "Big brown bat"), entry("ANIMALTYPE", "Bat ")); assertThat(records.get(2)).containsOnly(entry("ANIMAL_NAME", "Cat"), entry("ANIMALTYPE", "Not a Fox or a bat")); - assertThat(records.get(3)).containsOnly(entry("ANIMAL_NAME", "Artic fox"), entry("ANIMALTYPE", "Fox")); - assertThat(records.get(4)).containsOnly(entry("ANIMAL_NAME", "Red fox"), entry("ANIMALTYPE", "Fox")); + assertThat(records.get(3)).containsOnly(entry("ANIMAL_NAME", "Artic fox"), entry("ANIMALTYPE", "Fox ")); + assertThat(records.get(4)).containsOnly(entry("ANIMAL_NAME", "Red fox"), entry("ANIMALTYPE", "Fox ")); assertThat(records.get(5)).containsOnly(entry("ANIMAL_NAME", "Raccoon"), entry("ANIMALTYPE", "Not a Fox or a bat")); } } + @Test + void testSearchedCaseWithIntegers() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(animalName, case_() + .when(animalName, isEqualTo("Artic fox")).or(animalName, isEqualTo("Red fox")).then(1) + .when(animalName, isEqualTo("Little brown bat")).or(animalName, isEqualTo("Big brown bat")).then(2) + .else_(3).end().as("AnimalType")) + .from(animalData, "a") + .where(id, isIn(2, 3, 31, 32, 38, 39)) + .orderBy(id) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expected = "select a.animal_name, case " + + "when a.animal_name = #{parameters.p1,jdbcType=VARCHAR} or a.animal_name = #{parameters.p2,jdbcType=VARCHAR} then 1 " + + "when a.animal_name = #{parameters.p3,jdbcType=VARCHAR} or a.animal_name = #{parameters.p4,jdbcType=VARCHAR} then 2 " + + "else 3 end as AnimalType " + + "from AnimalData a where a.id in (" + + "#{parameters.p5,jdbcType=INTEGER},#{parameters.p6,jdbcType=INTEGER}," + + "#{parameters.p7,jdbcType=INTEGER},#{parameters.p8,jdbcType=INTEGER},#{parameters.p9,jdbcType=INTEGER}," + + "#{parameters.p10,jdbcType=INTEGER}) " + + "order by id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expected); + assertThat(selectStatement.getParameters()).containsOnly( + entry("p1", "Artic fox"), + entry("p2", "Red fox"), + entry("p3", "Little brown bat"), + entry("p4", "Big brown bat"), + entry("p5", 2), + entry("p6", 3), + entry("p7", 31), + entry("p8", 32), + entry("p9", 38), + entry("p10", 39) + ); + + List> records = mapper.selectManyMappedRows(selectStatement); + assertThat(records).hasSize(6); + assertThat(records.get(0)).containsOnly(entry("ANIMAL_NAME", "Little brown bat"), entry("ANIMALTYPE", 2)); + assertThat(records.get(1)).containsOnly(entry("ANIMAL_NAME", "Big brown bat"), entry("ANIMALTYPE", 2)); + assertThat(records.get(2)).containsOnly(entry("ANIMAL_NAME", "Cat"), entry("ANIMALTYPE", 3)); + assertThat(records.get(3)).containsOnly(entry("ANIMAL_NAME", "Artic fox"), entry("ANIMALTYPE", 1)); + assertThat(records.get(4)).containsOnly(entry("ANIMAL_NAME", "Red fox"), entry("ANIMALTYPE", 1)); + assertThat(records.get(5)).containsOnly(entry("ANIMAL_NAME", "Raccoon"), entry("ANIMALTYPE", 3)); + } + } + @Test void testSearchedCaseNoElse() { try (SqlSession sqlSession = sqlSessionFactory.openSession()) { CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); - SelectStatementProvider selectStatement = select(animalName, searchedCase() - .when(animalName, isEqualTo("Artic fox")).or(animalName, isEqualTo("Red fox")).then("'Fox'") - .when(animalName, isEqualTo("Little brown bat")).or(animalName, isEqualTo("Big brown bat")).then("'Bat'") + SelectStatementProvider selectStatement = select(animalName, case_() + .when(animalName, isEqualTo("Artic fox")).or(animalName, isEqualTo("Red fox")).then("Fox") + .when(animalName, isEqualTo("Little brown bat")).or(animalName, isEqualTo("Big brown bat")).then("Bat") .end().as("AnimalType")) .from(animalData, "a") .where(id, isIn(2, 3, 31, 32, 38, 39)) @@ -184,11 +235,11 @@ void testSearchedCaseWithGroup() { try (SqlSession sqlSession = sqlSessionFactory.openSession()) { CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); - SelectStatementProvider selectStatement = select(animalName, searchedCase() - .when(animalName, isEqualTo("Artic fox")).or(animalName, isEqualTo("Red fox")).then("'Fox'") - .when(animalName, isEqualTo("Little brown bat")).or(animalName, isEqualTo("Big brown bat")).then("'Bat'") - .when(group(animalName, isEqualTo("Cat"), and(id, isEqualTo(31))), or(id, isEqualTo(39))).then("'Fred'") - .else_("cast('Not a Fox or a bat' as varchar(25))").end().as("AnimalType")) + SelectStatementProvider selectStatement = select(animalName, case_() + .when(animalName, isEqualTo("Artic fox")).or(animalName, isEqualTo("Red fox")).then("Fox") + .when(animalName, isEqualTo("Little brown bat")).or(animalName, isEqualTo("Big brown bat")).then("Bat") + .when(group(animalName, isEqualTo("Cat"), and(id, isEqualTo(31))), or(id, isEqualTo(39))).then("Fred") + .else_("Not a Fox or a bat").end().as("AnimalType")) .from(animalData, "a") .where(id, isIn(2, 3, 4, 31, 32, 38, 39)) .orderBy(id) @@ -199,7 +250,7 @@ void testSearchedCaseWithGroup() { "when a.animal_name = #{parameters.p1,jdbcType=VARCHAR} or a.animal_name = #{parameters.p2,jdbcType=VARCHAR} then 'Fox' " + "when a.animal_name = #{parameters.p3,jdbcType=VARCHAR} or a.animal_name = #{parameters.p4,jdbcType=VARCHAR} then 'Bat' " + "when (a.animal_name = #{parameters.p5,jdbcType=VARCHAR} and a.id = #{parameters.p6,jdbcType=INTEGER}) or a.id = #{parameters.p7,jdbcType=INTEGER} then 'Fred' " + - "else cast('Not a Fox or a bat' as varchar(25)) end as AnimalType " + + "else 'Not a Fox or a bat' end as AnimalType " + "from AnimalData a where a.id in (" + "#{parameters.p8,jdbcType=INTEGER},#{parameters.p9,jdbcType=INTEGER}," + "#{parameters.p10,jdbcType=INTEGER},#{parameters.p11,jdbcType=INTEGER},#{parameters.p12,jdbcType=INTEGER}," + @@ -225,13 +276,13 @@ void testSearchedCaseWithGroup() { List> records = mapper.selectManyMappedRows(selectStatement); assertThat(records).hasSize(7); - assertThat(records.get(0)).containsOnly(entry("ANIMAL_NAME", "Little brown bat"), entry("ANIMALTYPE", "Bat")); - assertThat(records.get(1)).containsOnly(entry("ANIMAL_NAME", "Big brown bat"), entry("ANIMALTYPE", "Bat")); + assertThat(records.get(0)).containsOnly(entry("ANIMAL_NAME", "Little brown bat"), entry("ANIMALTYPE", "Bat ")); + assertThat(records.get(1)).containsOnly(entry("ANIMAL_NAME", "Big brown bat"), entry("ANIMALTYPE", "Bat ")); assertThat(records.get(2)).containsOnly(entry("ANIMAL_NAME", "Mouse"), entry("ANIMALTYPE", "Not a Fox or a bat")); - assertThat(records.get(3)).containsOnly(entry("ANIMAL_NAME", "Cat"), entry("ANIMALTYPE", "Fred")); - assertThat(records.get(4)).containsOnly(entry("ANIMAL_NAME", "Artic fox"), entry("ANIMALTYPE", "Fox")); - assertThat(records.get(5)).containsOnly(entry("ANIMAL_NAME", "Red fox"), entry("ANIMALTYPE", "Fox")); - assertThat(records.get(6)).containsOnly(entry("ANIMAL_NAME", "Raccoon"), entry("ANIMALTYPE", "Fred")); + assertThat(records.get(3)).containsOnly(entry("ANIMAL_NAME", "Cat"), entry("ANIMALTYPE", "Fred ")); + assertThat(records.get(4)).containsOnly(entry("ANIMAL_NAME", "Artic fox"), entry("ANIMALTYPE", "Fox ")); + assertThat(records.get(5)).containsOnly(entry("ANIMAL_NAME", "Red fox"), entry("ANIMALTYPE", "Fox ")); + assertThat(records.get(6)).containsOnly(entry("ANIMAL_NAME", "Raccoon"), entry("ANIMALTYPE", "Fred ")); } } @@ -240,9 +291,9 @@ void testSimpleCassLessThan() { try (SqlSession sqlSession = sqlSessionFactory.openSession()) { CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); - SelectStatementProvider selectStatement = select(animalName, simpleCase(brainWeight) - .when(isLessThan(4.0)).then("'small brain'") - .else_("'large brain'").end().as("brain_size")) + SelectStatementProvider selectStatement = select(animalName, SqlBuilder.case_(brainWeight) + .when(isLessThan(4.0)).then("small brain") + .else_("large brain").end().as("brain_size")) .from(animalData) .where(id, isIn(31, 32, 38, 39)) .orderBy(id) @@ -258,18 +309,18 @@ void testSimpleCassLessThan() { "order by id"; assertThat(selectStatement.getSelectStatement()).isEqualTo(expected); List> records = mapper.selectManyMappedRows(selectStatement); - assertThat(records).hasSize(7); + assertThat(records).hasSize(4); } } @Test - void testSimpleCase() { + void testSimpleCaseWithStrings() { try (SqlSession sqlSession = sqlSessionFactory.openSession()) { CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); - SelectStatementProvider selectStatement = select(animalName, simpleCase(animalName) - .when(isEqualTo("Artic fox"), isEqualTo("Red fox")).then("'yes'") - .else_("cast('no' as VARCHAR(3))").end().as("IsAFox")) + SelectStatementProvider selectStatement = select(animalName, SqlBuilder.case_(animalName) + .when(isEqualTo("Artic fox"), isEqualTo("Red fox")).then("yes") + .else_("no").end().as("IsAFox")) .from(animalData) .where(id, isIn(31, 32, 38, 39)) .orderBy(id) @@ -277,7 +328,7 @@ void testSimpleCase() { .render(RenderingStrategies.MYBATIS3); String expected = "select animal_name, " + - "case animal_name when = #{parameters.p1,jdbcType=VARCHAR}, = #{parameters.p2,jdbcType=VARCHAR} then 'yes' else cast('no' as VARCHAR(3)) end " + + "case animal_name when = #{parameters.p1,jdbcType=VARCHAR}, = #{parameters.p2,jdbcType=VARCHAR} then 'yes' else 'no' end " + "as IsAFox from AnimalData where id in " + "(#{parameters.p3,jdbcType=INTEGER},#{parameters.p4,jdbcType=INTEGER},#{parameters.p5,jdbcType=INTEGER},#{parameters.p6,jdbcType=INTEGER}) " + "order by id"; @@ -293,10 +344,48 @@ void testSimpleCase() { List> records = mapper.selectManyMappedRows(selectStatement); assertThat(records).hasSize(4); - assertThat(records.get(0)).containsOnly(entry("ANIMAL_NAME", "Cat"), entry("ISAFOX", "no")); + assertThat(records.get(0)).containsOnly(entry("ANIMAL_NAME", "Cat"), entry("ISAFOX", "no ")); assertThat(records.get(1)).containsOnly(entry("ANIMAL_NAME", "Artic fox"), entry("ISAFOX", "yes")); assertThat(records.get(2)).containsOnly(entry("ANIMAL_NAME", "Red fox"), entry("ISAFOX", "yes")); - assertThat(records.get(3)).containsOnly(entry("ANIMAL_NAME", "Raccoon"), entry("ISAFOX", "no")); + assertThat(records.get(3)).containsOnly(entry("ANIMAL_NAME", "Raccoon"), entry("ISAFOX", "no ")); + } + } + + @Test + void testSimpleCaseWithBooleans() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(animalName, SqlBuilder.case_(animalName) + .when(isEqualTo("Artic fox"), isEqualTo("Red fox")).then(true) + .else_(false).end().as("IsAFox")) + .from(animalData) + .where(id, isIn(31, 32, 38, 39)) + .orderBy(id) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expected = "select animal_name, " + + "case animal_name when = #{parameters.p1,jdbcType=VARCHAR}, = #{parameters.p2,jdbcType=VARCHAR} then true else false end " + + "as IsAFox from AnimalData where id in " + + "(#{parameters.p3,jdbcType=INTEGER},#{parameters.p4,jdbcType=INTEGER},#{parameters.p5,jdbcType=INTEGER},#{parameters.p6,jdbcType=INTEGER}) " + + "order by id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expected); + assertThat(selectStatement.getParameters()).containsOnly( + entry("p1", "Artic fox"), + entry("p2", "Red fox"), + entry("p3", 31), + entry("p4", 32), + entry("p5", 38), + entry("p6", 39) + ); + + List> records = mapper.selectManyMappedRows(selectStatement); + assertThat(records).hasSize(4); + assertThat(records.get(0)).containsOnly(entry("ANIMAL_NAME", "Cat"), entry("ISAFOX", false)); + assertThat(records.get(1)).containsOnly(entry("ANIMAL_NAME", "Artic fox"), entry("ISAFOX", true)); + assertThat(records.get(2)).containsOnly(entry("ANIMAL_NAME", "Red fox"), entry("ISAFOX", true)); + assertThat(records.get(3)).containsOnly(entry("ANIMAL_NAME", "Raccoon"), entry("ISAFOX", false)); } } @@ -305,8 +394,8 @@ void testSimpleCaseNoElse() { try (SqlSession sqlSession = sqlSessionFactory.openSession()) { CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); - SelectStatementProvider selectStatement = select(animalName, simpleCase(animalName) - .when(isEqualTo("Artic fox"), isEqualTo("Red fox")).then("'yes'") + SelectStatementProvider selectStatement = select(animalName, SqlBuilder.case_(animalName) + .when(isEqualTo("Artic fox"), isEqualTo("Red fox")).then("yes") .end().as("IsAFox")) .from(animalData) .where(id, isIn(31, 32, 38, 39)) @@ -340,7 +429,7 @@ void testSimpleCaseNoElse() { @Test void testInvalidSearchedCaseNoConditionsRender() { - SelectModel model = select(animalName, searchedCase() + SelectModel model = select(animalName, case_() .when(animalName, isEqualToWhenPresent((String) null)).then("Fred").end()) .from(animalData) .build(); @@ -352,7 +441,7 @@ void testInvalidSearchedCaseNoConditionsRender() { @Test void testInvalidSimpleCaseNoConditionsRender() { - SelectModel model = select(simpleCase(animalName) + SelectModel model = select(SqlBuilder.case_(animalName) .when(isEqualToWhenPresent((String) null)).then("Fred").end()) .from(animalData) .build(); @@ -364,15 +453,16 @@ void testInvalidSimpleCaseNoConditionsRender() { @Test void testInvalidSearchedCaseNoWhenConditions() { - assertThatExceptionOfType(InvalidSqlException.class).isThrownBy( - () -> searchedCase().end() - ).withMessage(Messages.getString("ERROR.40")); + SearchedCaseDSL dsl = case_(); + + assertThatExceptionOfType(InvalidSqlException.class).isThrownBy(dsl::end) + .withMessage(Messages.getString("ERROR.40")); } @Test void testInvalidSimpleCaseNoWhenConditions() { - assertThatExceptionOfType(InvalidSqlException.class).isThrownBy( - () -> simpleCase(id).end() - ).withMessage(Messages.getString("ERROR.40")); + SimpleCaseDSL dsl = case_(id); + assertThatExceptionOfType(InvalidSqlException.class).isThrownBy(dsl::end) + .withMessage(Messages.getString("ERROR.40")); } } diff --git a/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt b/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt index 8ff3d15c1..9693a1691 100644 --- a/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt +++ b/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt @@ -43,7 +43,7 @@ class KCaseExpressionTest { } @Test - fun testSearchedCase() { + fun testSearchedCaseWithStrings() { newSession().use { session -> val mapper = session.getMapper(CommonSelectMapper::class.java) @@ -52,14 +52,14 @@ class KCaseExpressionTest { `when` { animalName isEqualTo "Artic fox" or { animalName isEqualTo "Red fox" } - then("'Fox'") + then("Fox") } `when` { animalName isEqualTo "Little brown bat" or { animalName isEqualTo "Big brown bat" } - then("'Bat'") + then("Bat") } - `else`("cast('Not a Fox or a bat' as varchar(25))") + `else`("Not a Fox or a bat") }.`as`("AnimalType") ) { from(animalData, "a") @@ -70,7 +70,7 @@ class KCaseExpressionTest { val expected = "select a.animal_name, case " + "when a.animal_name = #{parameters.p1,jdbcType=VARCHAR} or a.animal_name = #{parameters.p2,jdbcType=VARCHAR} then 'Fox' " + "when a.animal_name = #{parameters.p3,jdbcType=VARCHAR} or a.animal_name = #{parameters.p4,jdbcType=VARCHAR} then 'Bat' " + - "else cast('Not a Fox or a bat' as varchar(25)) end as AnimalType " + + "else 'Not a Fox or a bat' end as AnimalType " + "from AnimalData a where a.id in (" + "#{parameters.p5,jdbcType=INTEGER},#{parameters.p6,jdbcType=INTEGER}," + "#{parameters.p7,jdbcType=INTEGER},#{parameters.p8,jdbcType=INTEGER},#{parameters.p9,jdbcType=INTEGER}," + @@ -94,11 +94,11 @@ class KCaseExpressionTest { assertThat(records).hasSize(6) assertThat(records[0]).containsOnly( entry("ANIMAL_NAME", "Little brown bat"), - entry("ANIMALTYPE", "Bat") + entry("ANIMALTYPE", "Bat ") ) assertThat(records[1]).containsOnly( entry("ANIMAL_NAME", "Big brown bat"), - entry("ANIMALTYPE", "Bat") + entry("ANIMALTYPE", "Bat ") ) assertThat(records[2]).containsOnly( entry("ANIMAL_NAME", "Cat"), @@ -106,11 +106,11 @@ class KCaseExpressionTest { ) assertThat(records[3]).containsOnly( entry("ANIMAL_NAME", "Artic fox"), - entry("ANIMALTYPE", "Fox") + entry("ANIMALTYPE", "Fox ") ) assertThat(records[4]).containsOnly( entry("ANIMAL_NAME", "Red fox"), - entry("ANIMALTYPE", "Fox") + entry("ANIMALTYPE", "Fox ") ) assertThat(records[5]).containsOnly( entry("ANIMAL_NAME", "Raccoon"), @@ -119,6 +119,83 @@ class KCaseExpressionTest { } } + @Test + fun testSearchedCaseWithIntegers() { + newSession().use { session -> + val mapper = session.getMapper(CommonSelectMapper::class.java) + + val selectStatement = select(animalName, + case { + `when` { + animalName isEqualTo "Artic fox" + or { animalName isEqualTo "Red fox" } + then(1) + } + `when` { + animalName isEqualTo "Little brown bat" + or { animalName isEqualTo "Big brown bat" } + then(2) + } + `else`(3) + }.`as`("AnimalType") + ) { + from(animalData, "a") + where { id.isIn(2, 3, 31, 32, 38, 39) } + orderBy(id) + } + + val expected = "select a.animal_name, case " + + "when a.animal_name = #{parameters.p1,jdbcType=VARCHAR} or a.animal_name = #{parameters.p2,jdbcType=VARCHAR} then 1 " + + "when a.animal_name = #{parameters.p3,jdbcType=VARCHAR} or a.animal_name = #{parameters.p4,jdbcType=VARCHAR} then 2 " + + "else 3 end as AnimalType " + + "from AnimalData a where a.id in (" + + "#{parameters.p5,jdbcType=INTEGER},#{parameters.p6,jdbcType=INTEGER}," + + "#{parameters.p7,jdbcType=INTEGER},#{parameters.p8,jdbcType=INTEGER},#{parameters.p9,jdbcType=INTEGER}," + + "#{parameters.p10,jdbcType=INTEGER}) " + + "order by id" + assertThat(selectStatement.selectStatement).isEqualTo(expected) + assertThat(selectStatement.parameters).containsOnly( + entry("p1", "Artic fox"), + entry("p2", "Red fox"), + entry("p3", "Little brown bat"), + entry("p4", "Big brown bat"), + entry("p5", 2), + entry("p6", 3), + entry("p7", 31), + entry("p8", 32), + entry("p9", 38), + entry("p10", 39) + ) + + val records = mapper.selectManyMappedRows(selectStatement) + assertThat(records).hasSize(6) + assertThat(records[0]).containsOnly( + entry("ANIMAL_NAME", "Little brown bat"), + entry("ANIMALTYPE", 2) + ) + assertThat(records[1]).containsOnly( + entry("ANIMAL_NAME", "Big brown bat"), + entry("ANIMALTYPE", 2) + ) + assertThat(records[2]).containsOnly( + entry("ANIMAL_NAME", "Cat"), + entry("ANIMALTYPE", 3) + ) + assertThat(records[3]).containsOnly( + entry("ANIMAL_NAME", "Artic fox"), + entry("ANIMALTYPE", 1) + ) + assertThat(records[4]).containsOnly( + entry("ANIMAL_NAME", "Red fox"), + entry("ANIMALTYPE", 1) + ) + assertThat(records[5]).containsOnly( + entry("ANIMAL_NAME", "Raccoon"), + entry("ANIMALTYPE", 3) + ) + } + } + @Test fun testSearchedCaseNoElse() { newSession().use { session -> @@ -130,12 +207,12 @@ class KCaseExpressionTest { `when` { animalName isEqualTo "Artic fox" or { animalName isEqualTo "Red fox" } - then("'Fox'") + then("Fox") } `when` { animalName isEqualTo "Little brown bat" or { animalName isEqualTo "Big brown bat" } - then("'Bat'") + then("Bat") } }.`as`("AnimalType") ) { @@ -203,12 +280,12 @@ class KCaseExpressionTest { `when` { animalName isEqualTo "Artic fox" or { animalName isEqualTo "Red fox" } - then("'Fox'") + then("Fox") } `when` { animalName isEqualTo "Little brown bat" or { animalName isEqualTo "Big brown bat" } - then("'Bat'") + then("Bat") } `when` { group { @@ -216,9 +293,9 @@ class KCaseExpressionTest { and { id isEqualTo 31 } } or { id isEqualTo 39 } - then("'Fred'") + then("Fred") } - `else`("cast('Not a Fox or a bat' as varchar(25))") + `else`("Not a Fox or a bat") }.`as`("AnimalType") ) { from(animalData, "a") @@ -230,7 +307,7 @@ class KCaseExpressionTest { "when a.animal_name = #{parameters.p1,jdbcType=VARCHAR} or a.animal_name = #{parameters.p2,jdbcType=VARCHAR} then 'Fox' " + "when a.animal_name = #{parameters.p3,jdbcType=VARCHAR} or a.animal_name = #{parameters.p4,jdbcType=VARCHAR} then 'Bat' " + "when (a.animal_name = #{parameters.p5,jdbcType=VARCHAR} and a.id = #{parameters.p6,jdbcType=INTEGER}) or a.id = #{parameters.p7,jdbcType=INTEGER} then 'Fred' " + - "else cast('Not a Fox or a bat' as varchar(25)) end as AnimalType " + + "else 'Not a Fox or a bat' end as AnimalType " + "from AnimalData a where a.id in (" + "#{parameters.p8,jdbcType=INTEGER},#{parameters.p9,jdbcType=INTEGER}," + "#{parameters.p10,jdbcType=INTEGER},#{parameters.p11,jdbcType=INTEGER},#{parameters.p12,jdbcType=INTEGER}," + @@ -259,11 +336,11 @@ class KCaseExpressionTest { assertThat(records).hasSize(7) assertThat(records[0]).containsOnly( entry("ANIMAL_NAME", "Little brown bat"), - entry("ANIMALTYPE", "Bat") + entry("ANIMALTYPE", "Bat ") ) assertThat(records[1]).containsOnly( entry("ANIMAL_NAME", "Big brown bat"), - entry("ANIMALTYPE", "Bat") + entry("ANIMALTYPE", "Bat ") ) assertThat(records[2]).containsOnly( entry("ANIMAL_NAME", "Mouse"), @@ -271,33 +348,33 @@ class KCaseExpressionTest { ) assertThat(records[3]).containsOnly( entry("ANIMAL_NAME", "Cat"), - entry("ANIMALTYPE", "Fred") + entry("ANIMALTYPE", "Fred ") ) assertThat(records[4]).containsOnly( entry("ANIMAL_NAME", "Artic fox"), - entry("ANIMALTYPE", "Fox") + entry("ANIMALTYPE", "Fox ") ) assertThat(records[5]).containsOnly( entry("ANIMAL_NAME", "Red fox"), - entry("ANIMALTYPE", "Fox") + entry("ANIMALTYPE", "Fox ") ) assertThat(records[6]).containsOnly( entry("ANIMAL_NAME", "Raccoon"), - entry("ANIMALTYPE", "Fred") + entry("ANIMALTYPE", "Fred ") ) } } @Test - fun testSimpleCase() { + fun testSimpleCaseWithStrings() { newSession().use { session -> val mapper = session.getMapper(CommonSelectMapper::class.java) val selectStatement = select( animalName, case(animalName) { - `when` (isEqualTo("Artic fox"), isEqualTo("Red fox")).then("'yes'") - `else`("cast('no' as VARCHAR(3))") + `when` (isEqualTo("Artic fox"), isEqualTo("Red fox")).then("yes") + `else`("no") }.`as`("IsAFox") ) { from(animalData) @@ -306,7 +383,7 @@ class KCaseExpressionTest { } val expected = "select animal_name, " + - "case animal_name when = #{parameters.p1,jdbcType=VARCHAR}, = #{parameters.p2,jdbcType=VARCHAR} then 'yes' else cast('no' as VARCHAR(3)) end " + + "case animal_name when = #{parameters.p1,jdbcType=VARCHAR}, = #{parameters.p2,jdbcType=VARCHAR} then 'yes' else 'no' end " + "as IsAFox from AnimalData where id in " + "(#{parameters.p3,jdbcType=INTEGER},#{parameters.p4,jdbcType=INTEGER},#{parameters.p5,jdbcType=INTEGER},#{parameters.p6,jdbcType=INTEGER}) " + "order by id" @@ -325,7 +402,7 @@ class KCaseExpressionTest { assertThat(records).hasSize(4) assertThat(records[0]).containsOnly( entry("ANIMAL_NAME", "Cat"), - entry("ISAFOX", "no") + entry("ISAFOX", "no ") ) assertThat(records[1]).containsOnly( entry("ANIMAL_NAME", "Artic fox"), @@ -337,7 +414,61 @@ class KCaseExpressionTest { ) assertThat(records[3]).containsOnly( entry("ANIMAL_NAME", "Raccoon"), - entry("ISAFOX", "no") + entry("ISAFOX", "no ") + ) + } + } + + @Test + fun testSimpleCaseWithBooleans() { + newSession().use { session -> + val mapper = session.getMapper(CommonSelectMapper::class.java) + + val selectStatement = select( + animalName, + case(animalName) { + `when` (isEqualTo("Artic fox"), isEqualTo("Red fox")).then(true) + `else`(false) + }.`as`("IsAFox") + ) { + from(animalData) + where { id.isIn(31, 32, 38, 39) } + orderBy(id) + } + + val expected = "select animal_name, " + + "case animal_name when = #{parameters.p1,jdbcType=VARCHAR}, = #{parameters.p2,jdbcType=VARCHAR} then true else false end " + + "as IsAFox from AnimalData where id in " + + "(#{parameters.p3,jdbcType=INTEGER},#{parameters.p4,jdbcType=INTEGER},#{parameters.p5,jdbcType=INTEGER},#{parameters.p6,jdbcType=INTEGER}) " + + "order by id" + assertThat(selectStatement.selectStatement).isEqualTo(expected) + assertThat(selectStatement.parameters) + .containsOnly( + entry("p1", "Artic fox"), + entry("p2", "Red fox"), + entry("p3", 31), + entry("p4", 32), + entry("p5", 38), + entry("p6", 39) + ) + + val records = mapper.selectManyMappedRows(selectStatement) + assertThat(records).hasSize(4) + assertThat(records[0]).containsOnly( + entry("ANIMAL_NAME", "Cat"), + entry("ISAFOX", false) + ) + assertThat(records[1]).containsOnly( + entry("ANIMAL_NAME", "Artic fox"), + entry("ISAFOX", true) + ) + assertThat(records[2]).containsOnly( + entry("ANIMAL_NAME", "Red fox"), + entry("ISAFOX", true) + ) + assertThat(records[3]).containsOnly( + entry("ANIMAL_NAME", "Raccoon"), + entry("ISAFOX", false) ) } } @@ -350,7 +481,7 @@ class KCaseExpressionTest { val selectStatement = select( animalName, case(animalName) { - `when`(isEqualTo("Artic fox"), isEqualTo("Red fox")).then("'yes'") + `when`(isEqualTo("Artic fox"), isEqualTo("Red fox")).then("yes") }.`as`("IsAFox") ) { from(animalData) @@ -430,14 +561,14 @@ class KCaseExpressionTest { @Test fun testInvalidSearchedMissingWhen() { assertThatExceptionOfType(InvalidSqlException::class.java).isThrownBy { - select(case { }){ from(animalData) } + select(case { `else`("Fred") }){ from(animalData) } }.withMessage(Messages.getString("ERROR.40")) } @Test fun testInvalidSimpleMissingWhen() { assertThatExceptionOfType(InvalidSqlException::class.java).isThrownBy { - select(case (id) { }){ from (animalData) } + select(case (id) { `else`("Fred") }){ from (animalData) } }.withMessage(Messages.getString("ERROR.40")) } } From f712cb363a30494e5f27b406b9a90bff3ae71ddd Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Wed, 13 Mar 2024 17:25:40 -0400 Subject: [PATCH 06/19] Add support for simple case with basic values --- .../org/mybatis/dynamic/sql/SqlBuilder.java | 4 +- .../caseexpression/BasicWhenCondition.java | 38 ++++++ .../ConditionBasedWhenCondition.java | 40 +++++++ .../{ => caseexpression}/SearchedCaseDSL.java | 2 +- .../SearchedCaseModel.java | 2 +- .../{ => caseexpression}/SimpleCaseDSL.java | 50 ++++++-- .../{ => caseexpression}/SimpleCaseModel.java | 29 +---- .../SimpleCaseWhenCondition.java | 32 ++++++ .../SimpleCaseWhenConditionVisitor.java | 22 ++++ .../select/render/SearchedCaseRenderer.java | 2 +- .../SearchedCaseWhenConditionRenderer.java | 2 +- .../sql/select/render/SimpleCaseRenderer.java | 29 ++--- .../SimpleCaseWhenConditionRenderer.java | 72 ++++++++++++ .../sql/util/kotlin/elements/CaseDSLs.kt | 51 ++++++--- .../sql/util/kotlin/elements/SqlElements.kt | 4 +- .../animal/data/CaseExpressionTest.java | 80 ++++++++++++- .../kotlin/animal/data/KCaseExpressionTest.kt | 108 ++++++++++++++++++ 17 files changed, 488 insertions(+), 79 deletions(-) create mode 100644 src/main/java/org/mybatis/dynamic/sql/select/caseexpression/BasicWhenCondition.java create mode 100644 src/main/java/org/mybatis/dynamic/sql/select/caseexpression/ConditionBasedWhenCondition.java rename src/main/java/org/mybatis/dynamic/sql/select/{ => caseexpression}/SearchedCaseDSL.java (98%) rename src/main/java/org/mybatis/dynamic/sql/select/{ => caseexpression}/SearchedCaseModel.java (98%) rename src/main/java/org/mybatis/dynamic/sql/select/{ => caseexpression}/SimpleCaseDSL.java (57%) rename src/main/java/org/mybatis/dynamic/sql/select/{ => caseexpression}/SimpleCaseModel.java (76%) create mode 100644 src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseWhenCondition.java create mode 100644 src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseWhenConditionVisitor.java create mode 100644 src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseWhenConditionRenderer.java diff --git a/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java index 54164e75b..b80eb7757 100644 --- a/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java +++ b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java @@ -28,15 +28,15 @@ import org.mybatis.dynamic.sql.insert.InsertDSL; import org.mybatis.dynamic.sql.insert.InsertSelectDSL; import org.mybatis.dynamic.sql.insert.MultiRowInsertDSL; +import org.mybatis.dynamic.sql.select.caseexpression.SearchedCaseDSL; +import org.mybatis.dynamic.sql.select.caseexpression.SimpleCaseDSL; import org.mybatis.dynamic.sql.select.ColumnSortSpecification; import org.mybatis.dynamic.sql.select.CountDSL; import org.mybatis.dynamic.sql.select.HavingDSL; import org.mybatis.dynamic.sql.select.MultiSelectDSL; import org.mybatis.dynamic.sql.select.QueryExpressionDSL.FromGatherer; -import org.mybatis.dynamic.sql.select.SearchedCaseDSL; import org.mybatis.dynamic.sql.select.SelectDSL; import org.mybatis.dynamic.sql.select.SelectModel; -import org.mybatis.dynamic.sql.select.SimpleCaseDSL; import org.mybatis.dynamic.sql.select.SimpleSortSpecification; import org.mybatis.dynamic.sql.select.aggregate.Avg; import org.mybatis.dynamic.sql.select.aggregate.Count; diff --git a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/BasicWhenCondition.java b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/BasicWhenCondition.java new file mode 100644 index 000000000..56417cd95 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/BasicWhenCondition.java @@ -0,0 +1,38 @@ +/* + * Copyright 2016-2024 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.select.caseexpression; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +public class BasicWhenCondition extends SimpleCaseWhenCondition { + private final List conditions = new ArrayList<>(); + + public BasicWhenCondition(List conditions, Object thenValue) { + super(thenValue); + this.conditions.addAll(conditions); + } + + public Stream conditions() { + return conditions.stream(); + } + + @Override + public R accept(SimpleCaseWhenConditionVisitor visitor) { + return visitor.visit(this); + } +} 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 new file mode 100644 index 000000000..28ca6aa45 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/ConditionBasedWhenCondition.java @@ -0,0 +1,40 @@ +/* + * Copyright 2016-2024 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.select.caseexpression; + +import org.mybatis.dynamic.sql.VisitableCondition; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +public class ConditionBasedWhenCondition extends SimpleCaseWhenCondition { + private final List> conditions = new ArrayList<>(); + + public ConditionBasedWhenCondition(List> conditions, Object thenValue) { + super(thenValue); + this.conditions.addAll(conditions); + } + + public Stream> conditions() { + return conditions.stream(); + } + + @Override + public R accept(SimpleCaseWhenConditionVisitor visitor) { + return visitor.visit(this); + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/SearchedCaseDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SearchedCaseDSL.java similarity index 98% rename from src/main/java/org/mybatis/dynamic/sql/select/SearchedCaseDSL.java rename to src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SearchedCaseDSL.java index 33d6adcb1..4adfa2287 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/SearchedCaseDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SearchedCaseDSL.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.mybatis.dynamic.sql.select; +package org.mybatis.dynamic.sql.select.caseexpression; import static org.mybatis.dynamic.sql.util.StringUtilities.quoteStringForSQL; diff --git a/src/main/java/org/mybatis/dynamic/sql/select/SearchedCaseModel.java b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SearchedCaseModel.java similarity index 98% rename from src/main/java/org/mybatis/dynamic/sql/select/SearchedCaseModel.java rename to src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SearchedCaseModel.java index 061b81eac..6c3e8e17e 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/SearchedCaseModel.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SearchedCaseModel.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.mybatis.dynamic.sql.select; +package org.mybatis.dynamic.sql.select.caseexpression; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/org/mybatis/dynamic/sql/select/SimpleCaseDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseDSL.java similarity index 57% rename from src/main/java/org/mybatis/dynamic/sql/select/SimpleCaseDSL.java rename to src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseDSL.java index dee90fcaa..ec8d37811 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/SimpleCaseDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseDSL.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.mybatis.dynamic.sql.select; +package org.mybatis.dynamic.sql.select.caseexpression; import static org.mybatis.dynamic.sql.util.StringUtilities.quoteStringForSQL; @@ -28,7 +28,7 @@ public class SimpleCaseDSL { private final BindableColumn column; - private final List> whenConditions = new ArrayList<>(); + private final List> whenConditions = new ArrayList<>(); private Object elseValue; private SimpleCaseDSL(BindableColumn column) { @@ -36,14 +36,23 @@ private SimpleCaseDSL(BindableColumn column) { } @SafeVarargs - public final WhenFinisher when(VisitableCondition condition, - VisitableCondition... subsequentConditions) { + public final ConditionBasedWhenFinisher when(VisitableCondition condition, + VisitableCondition... subsequentConditions) { return when(condition, Arrays.asList(subsequentConditions)); } - public WhenFinisher when(VisitableCondition condition, - List> subsequentConditions) { - return new WhenFinisher(condition, subsequentConditions); + public ConditionBasedWhenFinisher when(VisitableCondition condition, + List> subsequentConditions) { + return new ConditionBasedWhenFinisher(condition, subsequentConditions); + } + + @SafeVarargs + public final BasicWhenFinisher when(T condition, T... subsequentConditions) { + return when(condition, Arrays.asList(subsequentConditions)); + } + + public BasicWhenFinisher when(T condition, List subsequentConditions) { + return new BasicWhenFinisher(condition, subsequentConditions); } @SuppressWarnings("java:S100") @@ -66,21 +75,40 @@ public BasicColumn end() { .build(); } - public class WhenFinisher { + public class ConditionBasedWhenFinisher { private final List> conditions = new ArrayList<>(); - private WhenFinisher(VisitableCondition condition, List> subsequentConditions) { + private ConditionBasedWhenFinisher(VisitableCondition condition, List> subsequentConditions) { conditions.add(condition); conditions.addAll(subsequentConditions); } public SimpleCaseDSL then(String value) { - whenConditions.add(new SimpleCaseModel.SimpleWhenCondition<>(conditions, quoteStringForSQL(value))); + whenConditions.add(new ConditionBasedWhenCondition<>(conditions, quoteStringForSQL(value))); + return SimpleCaseDSL.this; + } + + public SimpleCaseDSL then(Object value) { + whenConditions.add(new ConditionBasedWhenCondition<>(conditions, value)); + return SimpleCaseDSL.this; + } + } + + public class BasicWhenFinisher { + private final List values = new ArrayList<>(); + + private BasicWhenFinisher(T value, List subsequentValues) { + values.add(value); + values.addAll(subsequentValues); + } + + public SimpleCaseDSL then(String value) { + whenConditions.add(new BasicWhenCondition<>(values, quoteStringForSQL(value))); return SimpleCaseDSL.this; } public SimpleCaseDSL then(Object value) { - whenConditions.add(new SimpleCaseModel.SimpleWhenCondition<>(conditions, value)); + whenConditions.add(new BasicWhenCondition<>(values, value)); return SimpleCaseDSL.this; } } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/SimpleCaseModel.java b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseModel.java similarity index 76% rename from src/main/java/org/mybatis/dynamic/sql/select/SimpleCaseModel.java rename to src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseModel.java index 737257f64..3393c6783 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/SimpleCaseModel.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseModel.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.mybatis.dynamic.sql.select; +package org.mybatis.dynamic.sql.select.caseexpression; import java.util.ArrayList; import java.util.List; @@ -23,7 +23,6 @@ import org.mybatis.dynamic.sql.BasicColumn; import org.mybatis.dynamic.sql.BindableColumn; -import org.mybatis.dynamic.sql.VisitableCondition; import org.mybatis.dynamic.sql.render.RenderingContext; import org.mybatis.dynamic.sql.select.render.SimpleCaseRenderer; import org.mybatis.dynamic.sql.util.FragmentAndParameters; @@ -31,7 +30,7 @@ public class SimpleCaseModel implements BasicColumn { private final BindableColumn column; - private final List> whenConditions; + private final List> whenConditions; private final Object elseValue; private final String alias; @@ -47,7 +46,7 @@ public BindableColumn column() { return column; } - public Stream> whenConditions() { + public Stream> whenConditions() { return whenConditions.stream(); } @@ -75,27 +74,9 @@ public FragmentAndParameters render(RenderingContext renderingContext) { return new SimpleCaseRenderer<>(this, renderingContext).render(); } - public static class SimpleWhenCondition { - private final List> conditions = new ArrayList<>(); - private final Object thenValue; - - public Stream> conditions() { - return conditions.stream(); - } - - public Object thenValue() { - return thenValue; - } - - public SimpleWhenCondition(List> conditions, Object thenValue) { - this.conditions.addAll(conditions); - this.thenValue = Objects.requireNonNull(thenValue); - } - } - public static class Builder { private BindableColumn column; - private final List> whenConditions = new ArrayList<>(); + private final List> whenConditions = new ArrayList<>(); private Object elseValue; private String alias; @@ -104,7 +85,7 @@ public Builder withColumn(BindableColumn column) { return this; } - public Builder withWhenConditions(List> whenConditions) { + public Builder withWhenConditions(List> whenConditions) { this.whenConditions.addAll(whenConditions); return this; } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseWhenCondition.java b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseWhenCondition.java new file mode 100644 index 000000000..a803887c8 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseWhenCondition.java @@ -0,0 +1,32 @@ +/* + * Copyright 2016-2024 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.select.caseexpression; + +import java.util.Objects; + +public abstract class SimpleCaseWhenCondition { + private final Object thenValue; + + protected SimpleCaseWhenCondition(Object thenValue) { + this.thenValue = Objects.requireNonNull(thenValue); + } + + public Object thenValue() { + return thenValue; + } + + public abstract R accept(SimpleCaseWhenConditionVisitor visitor); +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseWhenConditionVisitor.java b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseWhenConditionVisitor.java new file mode 100644 index 000000000..890343265 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseWhenConditionVisitor.java @@ -0,0 +1,22 @@ +/* + * Copyright 2016-2024 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.select.caseexpression; + +public interface SimpleCaseWhenConditionVisitor { + R visit(ConditionBasedWhenCondition whenCondition); + + R visit(BasicWhenCondition whenCondition); +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/SearchedCaseRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/SearchedCaseRenderer.java index 9c87317d7..8b1403a3b 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/render/SearchedCaseRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/SearchedCaseRenderer.java @@ -22,7 +22,7 @@ import org.mybatis.dynamic.sql.exception.InvalidSqlException; import org.mybatis.dynamic.sql.render.RenderingContext; -import org.mybatis.dynamic.sql.select.SearchedCaseModel; +import org.mybatis.dynamic.sql.select.caseexpression.SearchedCaseModel; import org.mybatis.dynamic.sql.util.FragmentAndParameters; import org.mybatis.dynamic.sql.util.FragmentCollector; import org.mybatis.dynamic.sql.util.Messages; diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/SearchedCaseWhenConditionRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/SearchedCaseWhenConditionRenderer.java index 2286d8e0b..cb487b2d4 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/render/SearchedCaseWhenConditionRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/SearchedCaseWhenConditionRenderer.java @@ -16,7 +16,7 @@ package org.mybatis.dynamic.sql.select.render; import org.mybatis.dynamic.sql.common.AbstractBooleanExpressionRenderer; -import org.mybatis.dynamic.sql.select.SearchedCaseModel.SearchedWhenCondition; +import org.mybatis.dynamic.sql.select.caseexpression.SearchedCaseModel.SearchedWhenCondition; public class SearchedCaseWhenConditionRenderer extends AbstractBooleanExpressionRenderer { protected SearchedCaseWhenConditionRenderer(Builder builder) { diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseRenderer.java index 8b614cbf7..e4843b2ac 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseRenderer.java @@ -20,26 +20,21 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import org.mybatis.dynamic.sql.VisitableCondition; import org.mybatis.dynamic.sql.render.RenderingContext; -import org.mybatis.dynamic.sql.select.SimpleCaseModel; +import org.mybatis.dynamic.sql.select.caseexpression.SimpleCaseModel; +import org.mybatis.dynamic.sql.select.caseexpression.SimpleCaseWhenCondition; 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 SimpleCaseRenderer { private final SimpleCaseModel simpleCaseModel; private final RenderingContext renderingContext; - private final DefaultConditionVisitor conditionVisitor; + private final SimpleCaseWhenConditionRenderer whenConditionRenderer; public SimpleCaseRenderer(SimpleCaseModel simpleCaseModel, RenderingContext renderingContext) { this.simpleCaseModel = Objects.requireNonNull(simpleCaseModel); this.renderingContext = Objects.requireNonNull(renderingContext); - conditionVisitor = new DefaultConditionVisitor.Builder() - .withColumn(simpleCaseModel.column()) - .withRenderingContext(renderingContext) - .build(); + whenConditionRenderer = new SimpleCaseWhenConditionRenderer<>(renderingContext, simpleCaseModel.column()); } public FragmentAndParameters render() { @@ -65,7 +60,7 @@ private Stream renderWhenConditions() { return simpleCaseModel.whenConditions().flatMap(this::renderWhenCondition); } - private Stream renderWhenCondition(SimpleCaseModel.SimpleWhenCondition whenCondition) { + private Stream renderWhenCondition(SimpleCaseWhenCondition whenCondition) { return Stream.of( renderWhen(), renderConditions(whenCondition), @@ -77,18 +72,10 @@ private FragmentAndParameters renderWhen() { return FragmentAndParameters.fromFragment("when"); //$NON-NLS-1$ } - private FragmentAndParameters renderConditions(SimpleCaseModel.SimpleWhenCondition whenCondition) { - return whenCondition.conditions().map(this::renderCondition) - .collect(FragmentCollector.collect()) - .toFragmentAndParameters(Collectors.joining(", ")); //$NON-NLS-1$ - } - - private FragmentAndParameters renderCondition(VisitableCondition condition) { - Validator.assertTrue(condition.shouldRender(renderingContext), "ERROR.39"); //$NON-NLS-1$ - return condition.accept(conditionVisitor); + private FragmentAndParameters renderConditions(SimpleCaseWhenCondition whenCondition) { + return whenCondition.accept(whenConditionRenderer); } - - private FragmentAndParameters renderThen(SimpleCaseModel.SimpleWhenCondition whenCondition) { + private FragmentAndParameters renderThen(SimpleCaseWhenCondition whenCondition) { return FragmentAndParameters.fromFragment("then " + whenCondition.thenValue()); //$NON-NLS-1$ } 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 new file mode 100644 index 000000000..d81bc9bd2 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseWhenConditionRenderer.java @@ -0,0 +1,72 @@ +/* + * Copyright 2016-2024 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.select.render; + +import java.util.Objects; +import java.util.stream.Collectors; + +import org.mybatis.dynamic.sql.BindableColumn; +import org.mybatis.dynamic.sql.VisitableCondition; +import org.mybatis.dynamic.sql.render.RenderedParameterInfo; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.select.caseexpression.ConditionBasedWhenCondition; +import org.mybatis.dynamic.sql.select.caseexpression.BasicWhenCondition; +import org.mybatis.dynamic.sql.select.caseexpression.SimpleCaseWhenConditionVisitor; +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 + public FragmentAndParameters visit(ConditionBasedWhenCondition whenCondition) { + return whenCondition.conditions().map(this::renderCondition) + .collect(FragmentCollector.collect()) + .toFragmentAndParameters(Collectors.joining(", ")); //$NON-NLS-1$ + } + + private FragmentAndParameters renderCondition(VisitableCondition condition) { + Validator.assertTrue(condition.shouldRender(renderingContext), "ERROR.39"); //$NON-NLS-1$ + return condition.accept(conditionVisitor); + } + + @Override + public FragmentAndParameters visit(BasicWhenCondition whenCondition) { + return whenCondition.conditions().map(this::renderBasicValue) + .collect(FragmentCollector.collect()) + .toFragmentAndParameters(Collectors.joining(", ")); //$NON-NLS-1$ + } + + private FragmentAndParameters renderBasicValue(T value) { + RenderedParameterInfo rpi = renderingContext.calculateParameterInfo(column); + return FragmentAndParameters.withFragment(rpi.renderedPlaceHolder()) + .withParameter(rpi.parameterMapKey(), value) + .build(); + } +} 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 9c592afe1..898b2e514 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,8 +16,10 @@ package org.mybatis.dynamic.sql.util.kotlin.elements import org.mybatis.dynamic.sql.VisitableCondition -import org.mybatis.dynamic.sql.select.SearchedCaseModel.SearchedWhenCondition -import org.mybatis.dynamic.sql.select.SimpleCaseModel.SimpleWhenCondition +import org.mybatis.dynamic.sql.select.caseexpression.BasicWhenCondition +import org.mybatis.dynamic.sql.select.caseexpression.ConditionBasedWhenCondition +import org.mybatis.dynamic.sql.select.caseexpression.SearchedCaseModel.SearchedWhenCondition +import org.mybatis.dynamic.sql.select.caseexpression.SimpleCaseWhenCondition import org.mybatis.dynamic.sql.util.kotlin.GroupingCriteriaCollector import org.mybatis.dynamic.sql.util.kotlin.assertNull @@ -65,10 +67,13 @@ class KSimpleCaseDSL { assertNull(field, "ERROR.42") //$NON-NLS-1$ field = value } - internal val whenConditions = mutableListOf>() + internal val whenConditions = mutableListOf>() - fun `when`(condition: VisitableCondition, vararg conditions: VisitableCondition) = - SimpleCaseThenGatherer(condition, conditions.asList()) + fun `when`(firstCondition: VisitableCondition, vararg subsequentConditions: VisitableCondition) = + ConditionBasedThenGatherer(firstCondition, subsequentConditions.asList()) + + fun `when`(firstValue: T, vararg subsequentValues: T) = + BasicThenGatherer(firstValue, subsequentValues.asList()) fun `else`(value: String) { this.elseValue = "'$value'" @@ -78,24 +83,44 @@ class KSimpleCaseDSL { this.elseValue = value } - inner class SimpleCaseThenGatherer(val condition: VisitableCondition, - val conditions: List>) { + inner class ConditionBasedThenGatherer(private val firstCondition: VisitableCondition, + private val subsequentConditions: List>) { fun then(value: String) { val allConditions = buildList { - add(condition) - addAll(conditions) + add(firstCondition) + addAll(subsequentConditions) } - whenConditions.add(SimpleWhenCondition(allConditions, "'$value'")) + whenConditions.add(ConditionBasedWhenCondition(allConditions, "'$value'")) } fun then(value: Any) { val allConditions = buildList { - add(condition) - addAll(conditions) + add(firstCondition) + addAll(subsequentConditions) + } + + whenConditions.add(ConditionBasedWhenCondition(allConditions, value)) + } + } + + inner class BasicThenGatherer(private val firstValue: T, private val subsequentValues: List) { + fun then(value: String) { + val allValues = buildList { + add(firstValue) + addAll(subsequentValues) + } + + whenConditions.add(BasicWhenCondition(allValues, "'$value'")) + } + + fun then(value: Any) { + val allValues = buildList { + add(firstValue) + addAll(subsequentValues) } - whenConditions.add(SimpleWhenCondition(allConditions, value)) + whenConditions.add(BasicWhenCondition(allValues, value)) } } } 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 f03ee0e90..7cc681a52 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 @@ -26,8 +26,8 @@ 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.SearchedCaseModel -import org.mybatis.dynamic.sql.select.SimpleCaseModel +import org.mybatis.dynamic.sql.select.caseexpression.SearchedCaseModel +import org.mybatis.dynamic.sql.select.caseexpression.SimpleCaseModel import org.mybatis.dynamic.sql.select.aggregate.Avg import org.mybatis.dynamic.sql.select.aggregate.Count import org.mybatis.dynamic.sql.select.aggregate.CountAll diff --git a/src/test/java/examples/animal/data/CaseExpressionTest.java b/src/test/java/examples/animal/data/CaseExpressionTest.java index 855e0eef9..7dfcd77e0 100644 --- a/src/test/java/examples/animal/data/CaseExpressionTest.java +++ b/src/test/java/examples/animal/data/CaseExpressionTest.java @@ -52,9 +52,9 @@ import org.mybatis.dynamic.sql.SqlBuilder; import org.mybatis.dynamic.sql.exception.InvalidSqlException; import org.mybatis.dynamic.sql.render.RenderingStrategies; -import org.mybatis.dynamic.sql.select.SearchedCaseDSL; +import org.mybatis.dynamic.sql.select.caseexpression.SearchedCaseDSL; import org.mybatis.dynamic.sql.select.SelectModel; -import org.mybatis.dynamic.sql.select.SimpleCaseDSL; +import org.mybatis.dynamic.sql.select.caseexpression.SimpleCaseDSL; import org.mybatis.dynamic.sql.select.render.SelectStatementProvider; import org.mybatis.dynamic.sql.util.Messages; import org.mybatis.dynamic.sql.util.mybatis3.CommonSelectMapper; @@ -351,6 +351,44 @@ void testSimpleCaseWithStrings() { } } + @Test + void testSimpleCaseBasicWithStrings() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(animalName, SqlBuilder.case_(animalName) + .when("Artic fox", "Red fox").then("yes") + .else_("no").end().as("IsAFox")) + .from(animalData) + .where(id, isIn(31, 32, 38, 39)) + .orderBy(id) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expected = "select animal_name, " + + "case animal_name when #{parameters.p1,jdbcType=VARCHAR}, #{parameters.p2,jdbcType=VARCHAR} then 'yes' else 'no' end " + + "as IsAFox from AnimalData where id in " + + "(#{parameters.p3,jdbcType=INTEGER},#{parameters.p4,jdbcType=INTEGER},#{parameters.p5,jdbcType=INTEGER},#{parameters.p6,jdbcType=INTEGER}) " + + "order by id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expected); + assertThat(selectStatement.getParameters()).containsOnly( + entry("p1", "Artic fox"), + entry("p2", "Red fox"), + entry("p3", 31), + entry("p4", 32), + entry("p5", 38), + entry("p6", 39) + ); + + List> records = mapper.selectManyMappedRows(selectStatement); + assertThat(records).hasSize(4); + assertThat(records.get(0)).containsOnly(entry("ANIMAL_NAME", "Cat"), entry("ISAFOX", "no ")); + assertThat(records.get(1)).containsOnly(entry("ANIMAL_NAME", "Artic fox"), entry("ISAFOX", "yes")); + assertThat(records.get(2)).containsOnly(entry("ANIMAL_NAME", "Red fox"), entry("ISAFOX", "yes")); + assertThat(records.get(3)).containsOnly(entry("ANIMAL_NAME", "Raccoon"), entry("ISAFOX", "no ")); + } + } + @Test void testSimpleCaseWithBooleans() { try (SqlSession sqlSession = sqlSessionFactory.openSession()) { @@ -389,6 +427,44 @@ void testSimpleCaseWithBooleans() { } } + @Test + void testSimpleCaseBasicWithBooleans() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(animalName, SqlBuilder.case_(animalName) + .when("Artic fox", "Red fox").then(true) + .else_(false).end().as("IsAFox")) + .from(animalData) + .where(id, isIn(31, 32, 38, 39)) + .orderBy(id) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expected = "select animal_name, " + + "case animal_name when #{parameters.p1,jdbcType=VARCHAR}, #{parameters.p2,jdbcType=VARCHAR} then true else false end " + + "as IsAFox from AnimalData where id in " + + "(#{parameters.p3,jdbcType=INTEGER},#{parameters.p4,jdbcType=INTEGER},#{parameters.p5,jdbcType=INTEGER},#{parameters.p6,jdbcType=INTEGER}) " + + "order by id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expected); + assertThat(selectStatement.getParameters()).containsOnly( + entry("p1", "Artic fox"), + entry("p2", "Red fox"), + entry("p3", 31), + entry("p4", 32), + entry("p5", 38), + entry("p6", 39) + ); + + List> records = mapper.selectManyMappedRows(selectStatement); + assertThat(records).hasSize(4); + assertThat(records.get(0)).containsOnly(entry("ANIMAL_NAME", "Cat"), entry("ISAFOX", false)); + assertThat(records.get(1)).containsOnly(entry("ANIMAL_NAME", "Artic fox"), entry("ISAFOX", true)); + assertThat(records.get(2)).containsOnly(entry("ANIMAL_NAME", "Red fox"), entry("ISAFOX", true)); + assertThat(records.get(3)).containsOnly(entry("ANIMAL_NAME", "Raccoon"), entry("ISAFOX", false)); + } + } + @Test void testSimpleCaseNoElse() { try (SqlSession sqlSession = sqlSessionFactory.openSession()) { diff --git a/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt b/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt index 9693a1691..04f5e07bd 100644 --- a/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt +++ b/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt @@ -419,6 +419,60 @@ class KCaseExpressionTest { } } + @Test + fun testSimpleCaseBasicWithStrings() { + newSession().use { session -> + val mapper = session.getMapper(CommonSelectMapper::class.java) + + val selectStatement = select( + animalName, + case(animalName) { + `when` ("Artic fox", "Red fox").then("yes") + `else`("no") + }.`as`("IsAFox") + ) { + from(animalData) + where { id.isIn(31, 32, 38, 39) } + orderBy(id) + } + + val expected = "select animal_name, " + + "case animal_name when #{parameters.p1,jdbcType=VARCHAR}, #{parameters.p2,jdbcType=VARCHAR} then 'yes' else 'no' end " + + "as IsAFox from AnimalData where id in " + + "(#{parameters.p3,jdbcType=INTEGER},#{parameters.p4,jdbcType=INTEGER},#{parameters.p5,jdbcType=INTEGER},#{parameters.p6,jdbcType=INTEGER}) " + + "order by id" + assertThat(selectStatement.selectStatement).isEqualTo(expected) + assertThat(selectStatement.parameters) + .containsOnly( + entry("p1", "Artic fox"), + entry("p2", "Red fox"), + entry("p3", 31), + entry("p4", 32), + entry("p5", 38), + entry("p6", 39) + ) + + val records = mapper.selectManyMappedRows(selectStatement) + assertThat(records).hasSize(4) + assertThat(records[0]).containsOnly( + entry("ANIMAL_NAME", "Cat"), + entry("ISAFOX", "no ") + ) + assertThat(records[1]).containsOnly( + entry("ANIMAL_NAME", "Artic fox"), + entry("ISAFOX", "yes") + ) + assertThat(records[2]).containsOnly( + entry("ANIMAL_NAME", "Red fox"), + entry("ISAFOX", "yes") + ) + assertThat(records[3]).containsOnly( + entry("ANIMAL_NAME", "Raccoon"), + entry("ISAFOX", "no ") + ) + } + } + @Test fun testSimpleCaseWithBooleans() { newSession().use { session -> @@ -473,6 +527,60 @@ class KCaseExpressionTest { } } + @Test + fun testSimpleCaseBasicWithBooleans() { + newSession().use { session -> + val mapper = session.getMapper(CommonSelectMapper::class.java) + + val selectStatement = select( + animalName, + case(animalName) { + `when` ("Artic fox", "Red fox").then(true) + `else`(false) + }.`as`("IsAFox") + ) { + from(animalData) + where { id.isIn(31, 32, 38, 39) } + orderBy(id) + } + + val expected = "select animal_name, " + + "case animal_name when #{parameters.p1,jdbcType=VARCHAR}, #{parameters.p2,jdbcType=VARCHAR} then true else false end " + + "as IsAFox from AnimalData where id in " + + "(#{parameters.p3,jdbcType=INTEGER},#{parameters.p4,jdbcType=INTEGER},#{parameters.p5,jdbcType=INTEGER},#{parameters.p6,jdbcType=INTEGER}) " + + "order by id" + assertThat(selectStatement.selectStatement).isEqualTo(expected) + assertThat(selectStatement.parameters) + .containsOnly( + entry("p1", "Artic fox"), + entry("p2", "Red fox"), + entry("p3", 31), + entry("p4", 32), + entry("p5", 38), + entry("p6", 39) + ) + + val records = mapper.selectManyMappedRows(selectStatement) + assertThat(records).hasSize(4) + assertThat(records[0]).containsOnly( + entry("ANIMAL_NAME", "Cat"), + entry("ISAFOX", false) + ) + assertThat(records[1]).containsOnly( + entry("ANIMAL_NAME", "Artic fox"), + entry("ISAFOX", true) + ) + assertThat(records[2]).containsOnly( + entry("ANIMAL_NAME", "Red fox"), + entry("ISAFOX", true) + ) + assertThat(records[3]).containsOnly( + entry("ANIMAL_NAME", "Raccoon"), + entry("ISAFOX", false) + ) + } + } + @Test fun testSimpleCaseNoElse() { newSession().use { session -> From aa58bc97fc2e6e04a209ac261e08ff8608648963 Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Wed, 13 Mar 2024 17:35:54 -0400 Subject: [PATCH 07/19] Checkstyle --- .../org/mybatis/dynamic/sql/SqlBuilder.java | 26 +++++++++---------- .../ConditionBasedWhenCondition.java | 4 +-- .../select/caseexpression/SimpleCaseDSL.java | 3 ++- .../sql/select/render/SimpleCaseRenderer.java | 1 + .../SimpleCaseWhenConditionRenderer.java | 12 ++++----- 5 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java index b80eb7757..d9f8d02c1 100644 --- a/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java +++ b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java @@ -28,8 +28,6 @@ import org.mybatis.dynamic.sql.insert.InsertDSL; import org.mybatis.dynamic.sql.insert.InsertSelectDSL; import org.mybatis.dynamic.sql.insert.MultiRowInsertDSL; -import org.mybatis.dynamic.sql.select.caseexpression.SearchedCaseDSL; -import org.mybatis.dynamic.sql.select.caseexpression.SimpleCaseDSL; import org.mybatis.dynamic.sql.select.ColumnSortSpecification; import org.mybatis.dynamic.sql.select.CountDSL; import org.mybatis.dynamic.sql.select.HavingDSL; @@ -45,6 +43,8 @@ import org.mybatis.dynamic.sql.select.aggregate.Max; import org.mybatis.dynamic.sql.select.aggregate.Min; import org.mybatis.dynamic.sql.select.aggregate.Sum; +import org.mybatis.dynamic.sql.select.caseexpression.SearchedCaseDSL; +import org.mybatis.dynamic.sql.select.caseexpression.SimpleCaseDSL; import org.mybatis.dynamic.sql.select.function.Add; import org.mybatis.dynamic.sql.select.function.Concat; import org.mybatis.dynamic.sql.select.function.Concatenate; @@ -427,17 +427,6 @@ static AndOrCriteriaGroup and(List subCriteria) { .build(); } - // case expressions - @SuppressWarnings("java:S100") - static SimpleCaseDSL case_(BindableColumn column) { - return SimpleCaseDSL.simpleCase(column); - } - - @SuppressWarnings("java:S100") - static SearchedCaseDSL case_() { - return SearchedCaseDSL.searchedCase(); - } - // join support static JoinCriterion and(BindableColumn joinColumn, JoinCondition joinCondition) { return new JoinCriterion.Builder() @@ -455,6 +444,17 @@ static JoinCriterion on(BindableColumn joinColumn, JoinCondition jo .build(); } + // case expressions + @SuppressWarnings("java:S100") + static SimpleCaseDSL case_(BindableColumn column) { + return SimpleCaseDSL.simpleCase(column); + } + + @SuppressWarnings("java:S100") + static SearchedCaseDSL case_() { + return SearchedCaseDSL.searchedCase(); + } + static EqualTo equalTo(BindableColumn column) { return new EqualTo<>(column); } 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 28ca6aa45..029100744 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 @@ -15,12 +15,12 @@ */ package org.mybatis.dynamic.sql.select.caseexpression; -import org.mybatis.dynamic.sql.VisitableCondition; - import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; +import org.mybatis.dynamic.sql.VisitableCondition; + public class ConditionBasedWhenCondition extends SimpleCaseWhenCondition { private final List> conditions = new ArrayList<>(); 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 ec8d37811..c5e61443a 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 @@ -78,7 +78,8 @@ public BasicColumn end() { public class ConditionBasedWhenFinisher { private final List> conditions = new ArrayList<>(); - private ConditionBasedWhenFinisher(VisitableCondition condition, List> subsequentConditions) { + private ConditionBasedWhenFinisher(VisitableCondition condition, + List> subsequentConditions) { conditions.add(condition); conditions.addAll(subsequentConditions); } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseRenderer.java index e4843b2ac..263bf732c 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseRenderer.java @@ -75,6 +75,7 @@ private FragmentAndParameters renderWhen() { private FragmentAndParameters renderConditions(SimpleCaseWhenCondition whenCondition) { return whenCondition.accept(whenConditionRenderer); } + private FragmentAndParameters renderThen(SimpleCaseWhenCondition whenCondition) { return FragmentAndParameters.fromFragment("then " + whenCondition.thenValue()); //$NON-NLS-1$ } 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 d81bc9bd2..4bb8d8aff 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 @@ -22,8 +22,8 @@ import org.mybatis.dynamic.sql.VisitableCondition; import org.mybatis.dynamic.sql.render.RenderedParameterInfo; import org.mybatis.dynamic.sql.render.RenderingContext; -import org.mybatis.dynamic.sql.select.caseexpression.ConditionBasedWhenCondition; import org.mybatis.dynamic.sql.select.caseexpression.BasicWhenCondition; +import org.mybatis.dynamic.sql.select.caseexpression.ConditionBasedWhenCondition; import org.mybatis.dynamic.sql.select.caseexpression.SimpleCaseWhenConditionVisitor; import org.mybatis.dynamic.sql.util.FragmentAndParameters; import org.mybatis.dynamic.sql.util.FragmentCollector; @@ -51,11 +51,6 @@ public FragmentAndParameters visit(ConditionBasedWhenCondition whenCondition) .toFragmentAndParameters(Collectors.joining(", ")); //$NON-NLS-1$ } - private FragmentAndParameters renderCondition(VisitableCondition condition) { - Validator.assertTrue(condition.shouldRender(renderingContext), "ERROR.39"); //$NON-NLS-1$ - return condition.accept(conditionVisitor); - } - @Override public FragmentAndParameters visit(BasicWhenCondition whenCondition) { return whenCondition.conditions().map(this::renderBasicValue) @@ -63,6 +58,11 @@ public FragmentAndParameters visit(BasicWhenCondition whenCondition) { .toFragmentAndParameters(Collectors.joining(", ")); //$NON-NLS-1$ } + private FragmentAndParameters renderCondition(VisitableCondition condition) { + Validator.assertTrue(condition.shouldRender(renderingContext), "ERROR.39"); //$NON-NLS-1$ + return condition.accept(conditionVisitor); + } + private FragmentAndParameters renderBasicValue(T value) { RenderedParameterInfo rpi = renderingContext.calculateParameterInfo(column); return FragmentAndParameters.withFragment(rpi.renderedPlaceHolder()) From f4ca50aa319cc652fd0920defa069a4df792bd17 Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Thu, 14 Mar 2024 09:12:29 -0400 Subject: [PATCH 08/19] Polishing --- .../sql/util/kotlin/elements/CaseDSLs.kt | 66 ++++++++----------- .../kotlin/animal/data/KCaseExpressionTest.kt | 29 +++++--- 2 files changed, 49 insertions(+), 46 deletions(-) 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 898b2e514..868502ced 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 @@ -53,11 +53,11 @@ class SearchedCaseCriteriaCollector : GroupingCriteriaCollector() { } fun then(value: String) { - this.thenValue = "'$value'" + thenValue = "'$value'" } fun then(value: Any) { - this.thenValue = value + thenValue = value } } @@ -69,58 +69,48 @@ class KSimpleCaseDSL { } internal val whenConditions = mutableListOf>() - fun `when`(firstCondition: VisitableCondition, vararg subsequentConditions: VisitableCondition) = - ConditionBasedThenGatherer(firstCondition, subsequentConditions.asList()) - - fun `when`(firstValue: T, vararg subsequentValues: T) = - BasicThenGatherer(firstValue, subsequentValues.asList()) - - fun `else`(value: String) { - this.elseValue = "'$value'" - } - - fun `else`(value: Any) { - this.elseValue = value - } - - inner class ConditionBasedThenGatherer(private val firstCondition: VisitableCondition, - private val subsequentConditions: List>) { - fun then(value: String) { + fun `when`(firstCondition: VisitableCondition, vararg subsequentConditions: VisitableCondition, + completer: SimpleCaseThenGatherer.() -> Unit) = + SimpleCaseThenGatherer().apply(completer).run { val allConditions = buildList { add(firstCondition) addAll(subsequentConditions) } - whenConditions.add(ConditionBasedWhenCondition(allConditions, "'$value'")) + whenConditions.add(ConditionBasedWhenCondition(allConditions, thenValue)) } - fun then(value: Any) { + fun `when`(firstValue: T, vararg subsequentValues: T, completer: SimpleCaseThenGatherer.() -> Unit) = + SimpleCaseThenGatherer().apply(completer).run { val allConditions = buildList { - add(firstCondition) - addAll(subsequentConditions) + add(firstValue) + addAll(subsequentValues) } - whenConditions.add(ConditionBasedWhenCondition(allConditions, value)) + whenConditions.add(BasicWhenCondition(allConditions, thenValue)) } + + fun `else`(value: String) { + this.elseValue = "'$value'" } - inner class BasicThenGatherer(private val firstValue: T, private val subsequentValues: List) { - fun then(value: String) { - val allValues = buildList { - add(firstValue) - addAll(subsequentValues) - } + fun `else`(value: Any) { + this.elseValue = value + } +} - whenConditions.add(BasicWhenCondition(allValues, "'$value'")) +class SimpleCaseThenGatherer { + internal var thenValue: Any? = null + private set(value) { + assertNull(field, "ERROR.41") //$NON-NLS-1$ + field = value } - fun then(value: Any) { - val allValues = buildList { - add(firstValue) - addAll(subsequentValues) - } + fun then(value: String) { + thenValue = "'$value'" + } - whenConditions.add(BasicWhenCondition(allValues, value)) - } + fun then(value: Any) { + thenValue = value } } diff --git a/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt b/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt index 04f5e07bd..e47a5ab84 100644 --- a/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt +++ b/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt @@ -373,7 +373,7 @@ class KCaseExpressionTest { val selectStatement = select( animalName, case(animalName) { - `when` (isEqualTo("Artic fox"), isEqualTo("Red fox")).then("yes") + `when` (isEqualTo("Artic fox"), isEqualTo("Red fox")) { then("yes") } `else`("no") }.`as`("IsAFox") ) { @@ -427,7 +427,7 @@ class KCaseExpressionTest { val selectStatement = select( animalName, case(animalName) { - `when` ("Artic fox", "Red fox").then("yes") + `when` ("Artic fox", "Red fox") { then("yes") } `else`("no") }.`as`("IsAFox") ) { @@ -481,7 +481,7 @@ class KCaseExpressionTest { val selectStatement = select( animalName, case(animalName) { - `when` (isEqualTo("Artic fox"), isEqualTo("Red fox")).then(true) + `when` (isEqualTo("Artic fox"), isEqualTo("Red fox")) { then(true) } `else`(false) }.`as`("IsAFox") ) { @@ -535,7 +535,7 @@ class KCaseExpressionTest { val selectStatement = select( animalName, case(animalName) { - `when` ("Artic fox", "Red fox").then(true) + `when` ("Artic fox", "Red fox") { then(true) } `else`(false) }.`as`("IsAFox") ) { @@ -589,7 +589,7 @@ class KCaseExpressionTest { val selectStatement = select( animalName, case(animalName) { - `when`(isEqualTo("Artic fox"), isEqualTo("Red fox")).then("yes") + `when`(isEqualTo("Artic fox"), isEqualTo("Red fox")) { then("yes") } }.`as`("IsAFox") ) { from(animalData) @@ -632,13 +632,26 @@ class KCaseExpressionTest { fun testInvalidDoubleElseSimple() { assertThatExceptionOfType(KInvalidSQLException::class.java).isThrownBy { case(animalName) { - `when`(isEqualTo("Artic fox"), isEqualTo("Red fox")).then("'yes'") + `when`(isEqualTo("Artic fox"), isEqualTo("Red fox")) { then("'yes'") } `else`("Fred") `else`("Wilma") } }.withMessage(Messages.getString("ERROR.42")) } + @Test + fun testInvalidDoubleThenSimple() { + assertThatExceptionOfType(KInvalidSQLException::class.java).isThrownBy { + case(animalName) { + `when`(isEqualTo("Artic fox"), isEqualTo("Red fox")) { + then("'yes'") + then("no") + } + `else`("Fred") + } + }.withMessage(Messages.getString("ERROR.41")) + } + @Test fun testInvalidDoubleElseSearched() { assertThatExceptionOfType(KInvalidSQLException::class.java).isThrownBy { @@ -669,14 +682,14 @@ class KCaseExpressionTest { @Test fun testInvalidSearchedMissingWhen() { assertThatExceptionOfType(InvalidSqlException::class.java).isThrownBy { - select(case { `else`("Fred") }){ from(animalData) } + select(case { `else`("Fred") }) { from(animalData) } }.withMessage(Messages.getString("ERROR.40")) } @Test fun testInvalidSimpleMissingWhen() { assertThatExceptionOfType(InvalidSqlException::class.java).isThrownBy { - select(case (id) { `else`("Fred") }){ from (animalData) } + select(case (id) { `else`("Fred") }) { from (animalData) } }.withMessage(Messages.getString("ERROR.40")) } } From b95978933643c62bad80f5afa25a1a706d66c501 Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Thu, 14 Mar 2024 14:36:56 -0400 Subject: [PATCH 09/19] Add CAST function, then render casts properly --- .../org/mybatis/dynamic/sql/SqlBuilder.java | 20 ++++ .../mybatis/dynamic/sql/StringConstant.java | 3 +- .../caseexpression/BasicWhenCondition.java | 4 +- .../ConditionBasedWhenCondition.java | 3 +- .../sql/select/caseexpression/ElseDSL.java | 51 ++++++++++ .../caseexpression/SearchedCaseDSL.java | 29 ++---- .../caseexpression/SearchedCaseModel.java | 14 +-- .../select/caseexpression/SimpleCaseDSL.java | 41 +++----- .../caseexpression/SimpleCaseModel.java | 8 +- .../SimpleCaseWhenCondition.java | 8 +- .../sql/select/caseexpression/ThenDSL.java | 45 +++++++++ .../dynamic/sql/select/function/Cast.java | 82 ++++++++++++++++ .../select/render/SearchedCaseRenderer.java | 46 +++++---- .../sql/select/render/SimpleCaseRenderer.java | 51 +++++----- .../sql/util/kotlin/elements/CaseDSLs.kt | 91 ++++++++++++------ .../sql/util/kotlin/elements/SqlElements.kt | 6 ++ .../animal/data/CaseExpressionTest.java | 95 +++++++++++++++++++ .../kotlin/animal/data/KCaseExpressionTest.kt | 83 ++++++++++++++++ 18 files changed, 541 insertions(+), 139 deletions(-) create mode 100644 src/main/java/org/mybatis/dynamic/sql/select/caseexpression/ElseDSL.java create mode 100644 src/main/java/org/mybatis/dynamic/sql/select/caseexpression/ThenDSL.java create mode 100644 src/main/java/org/mybatis/dynamic/sql/select/function/Cast.java diff --git a/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java index d9f8d02c1..e107cdd83 100644 --- a/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java +++ b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java @@ -46,6 +46,7 @@ import org.mybatis.dynamic.sql.select.caseexpression.SearchedCaseDSL; import org.mybatis.dynamic.sql.select.caseexpression.SimpleCaseDSL; import org.mybatis.dynamic.sql.select.function.Add; +import org.mybatis.dynamic.sql.select.function.Cast; import org.mybatis.dynamic.sql.select.function.Concat; import org.mybatis.dynamic.sql.select.function.Concatenate; import org.mybatis.dynamic.sql.select.function.Divide; @@ -530,6 +531,10 @@ static Subtract subtract(BindableColumn firstColumn, BasicColumn secon return Subtract.of(firstColumn, secondColumn, subsequentColumns); } + static CastFinisher cast(BasicColumn column) { + return new CastFinisher(column); + } + /** * Concatenate function that renders as "(x || y || z)". This will not work on some * databases like MySql. In that case, use {@link SqlBuilder#concat(BindableColumn, BasicColumn...)} @@ -981,4 +986,19 @@ public GeneralInsertDSL.SetClauseFinisher set(SqlColumn column) { .set(column); } } + + class CastFinisher { + private final BasicColumn column; + + public CastFinisher(BasicColumn column) { + this.column = column; + } + + public Cast as(String targetType) { + return new Cast.Builder() + .withColumn(column) + .withTargetType(targetType) + .build(); + } + } } diff --git a/src/main/java/org/mybatis/dynamic/sql/StringConstant.java b/src/main/java/org/mybatis/dynamic/sql/StringConstant.java index f5793c937..522ac41eb 100644 --- a/src/main/java/org/mybatis/dynamic/sql/StringConstant.java +++ b/src/main/java/org/mybatis/dynamic/sql/StringConstant.java @@ -20,6 +20,7 @@ import org.mybatis.dynamic.sql.render.RenderingContext; import org.mybatis.dynamic.sql.util.FragmentAndParameters; +import org.mybatis.dynamic.sql.util.StringUtilities; public class StringConstant implements BindableColumn { @@ -42,7 +43,7 @@ public Optional alias() { @Override public FragmentAndParameters render(RenderingContext renderingContext) { - return FragmentAndParameters.fromFragment("'" + value + "'"); //$NON-NLS-1$ //$NON-NLS-2$ + return FragmentAndParameters.fromFragment(StringUtilities.quoteStringForSQL(value)); } @Override diff --git a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/BasicWhenCondition.java b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/BasicWhenCondition.java index 56417cd95..8fe46e5fd 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/BasicWhenCondition.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/BasicWhenCondition.java @@ -19,10 +19,12 @@ import java.util.List; import java.util.stream.Stream; +import org.mybatis.dynamic.sql.BasicColumn; + public class BasicWhenCondition extends SimpleCaseWhenCondition { private final List conditions = new ArrayList<>(); - public BasicWhenCondition(List conditions, Object thenValue) { + public BasicWhenCondition(List conditions, BasicColumn thenValue) { super(thenValue); this.conditions.addAll(conditions); } 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 029100744..16af8f891 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 @@ -19,12 +19,13 @@ import java.util.List; import java.util.stream.Stream; +import org.mybatis.dynamic.sql.BasicColumn; import org.mybatis.dynamic.sql.VisitableCondition; public class ConditionBasedWhenCondition extends SimpleCaseWhenCondition { private final List> conditions = new ArrayList<>(); - public ConditionBasedWhenCondition(List> conditions, Object thenValue) { + public ConditionBasedWhenCondition(List> conditions, BasicColumn thenValue) { super(thenValue); this.conditions.addAll(conditions); } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/ElseDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/ElseDSL.java new file mode 100644 index 000000000..abe51d763 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/ElseDSL.java @@ -0,0 +1,51 @@ +/* + * Copyright 2016-2024 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.select.caseexpression; + +import org.mybatis.dynamic.sql.BasicColumn; +import org.mybatis.dynamic.sql.Constant; +import org.mybatis.dynamic.sql.StringConstant; + +public interface ElseDSL { + + @SuppressWarnings("java:S100") + default T else_(String value) { + return else_(StringConstant.of(value)); + } + + @SuppressWarnings("java:S100") + default T else_(Boolean value) { + return else_(Constant.of(value.toString())); + } + + @SuppressWarnings("java:S100") + default T else_(Integer value) { + return else_(Constant.of(value.toString())); + } + + @SuppressWarnings("java:S100") + default T else_(Long value) { + return else_(Constant.of(value.toString())); + } + + @SuppressWarnings("java:S100") + default T else_(Double value) { + return else_(Constant.of(value.toString())); + } + + @SuppressWarnings("java:S100") + T else_(BasicColumn column); +} 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 4adfa2287..4e070b98d 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 @@ -15,8 +15,6 @@ */ package org.mybatis.dynamic.sql.select.caseexpression; -import static org.mybatis.dynamic.sql.util.StringUtilities.quoteStringForSQL; - import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -30,9 +28,9 @@ import org.mybatis.dynamic.sql.VisitableCondition; import org.mybatis.dynamic.sql.common.AbstractBooleanExpressionDSL; -public class SearchedCaseDSL { +public class SearchedCaseDSL implements ElseDSL { private final List whenConditions = new ArrayList<>(); - private Object elseValue; + private BasicColumn elseValue; public WhenDSL when(BindableColumn column, VisitableCondition condition, AndOrCriteriaGroup... subCriteria) { @@ -67,14 +65,9 @@ private WhenDSL initialize(SqlCriterion sqlCriterion) { } @SuppressWarnings("java:S100") - public SearchedCaseEnder else_(String value) { - this.elseValue = quoteStringForSQL(value); - return new SearchedCaseEnder(); - } - - @SuppressWarnings("java:S100") - public SearchedCaseEnder else_(Object value) { - this.elseValue = value; + @Override + public SearchedCaseEnder else_(BasicColumn column) { + elseValue = column; return new SearchedCaseEnder(); } @@ -85,19 +78,15 @@ public BasicColumn end() { .build(); } - public class WhenDSL extends AbstractBooleanExpressionDSL { + public class WhenDSL extends AbstractBooleanExpressionDSL implements ThenDSL { private WhenDSL(SqlCriterion sqlCriterion) { setInitialCriterion(sqlCriterion); } - public SearchedCaseDSL then(String value) { + @Override + public SearchedCaseDSL then(BasicColumn column) { whenConditions.add(new SearchedCaseModel.SearchedWhenCondition(getInitialCriterion(), subCriteria, - quoteStringForSQL(value))); - return SearchedCaseDSL.this; - } - - public SearchedCaseDSL then(Object value) { - whenConditions.add(new SearchedCaseModel.SearchedWhenCondition(getInitialCriterion(), subCriteria, value)); + column)); return SearchedCaseDSL.this; } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SearchedCaseModel.java b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SearchedCaseModel.java index 6c3e8e17e..50ca830ad 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SearchedCaseModel.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SearchedCaseModel.java @@ -32,7 +32,7 @@ public class SearchedCaseModel implements BasicColumn { private final List whenConditions; - private final Object elseValue; + private final BasicColumn elseValue; private final String alias; private SearchedCaseModel(Builder builder) { @@ -46,7 +46,7 @@ public Stream whenConditions() { return whenConditions.stream(); } - public Optional elseValue() { + public Optional elseValue() { return Optional.ofNullable(elseValue); } @@ -70,14 +70,14 @@ public FragmentAndParameters render(RenderingContext renderingContext) { public static class SearchedWhenCondition extends AbstractBooleanExpressionModel { - private final Object thenValue; + private final BasicColumn thenValue; - public Object thenValue() { + public BasicColumn thenValue() { return thenValue; } public SearchedWhenCondition(SqlCriterion initialCriterion, List subCriteria, - Object thenValue) { + BasicColumn thenValue) { super(initialCriterion, subCriteria); this.thenValue = Objects.requireNonNull(thenValue); } @@ -85,7 +85,7 @@ public SearchedWhenCondition(SqlCriterion initialCriterion, List whenConditions = new ArrayList<>(); - private Object elseValue; + private BasicColumn elseValue; private String alias; public Builder withWhenConditions(List whenConditions) { @@ -93,7 +93,7 @@ public Builder withWhenConditions(List whenConditions) { return this; } - public Builder withElseValue(Object elseValue) { + public Builder withElseValue(BasicColumn elseValue) { this.elseValue = elseValue; return this; } 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 c5e61443a..83e46473a 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 @@ -15,8 +15,6 @@ */ package org.mybatis.dynamic.sql.select.caseexpression; -import static org.mybatis.dynamic.sql.util.StringUtilities.quoteStringForSQL; - import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -26,10 +24,10 @@ import org.mybatis.dynamic.sql.BindableColumn; import org.mybatis.dynamic.sql.VisitableCondition; -public class SimpleCaseDSL { +public class SimpleCaseDSL implements ElseDSL.SimpleCaseEnder> { private final BindableColumn column; private final List> whenConditions = new ArrayList<>(); - private Object elseValue; + private BasicColumn elseValue; private SimpleCaseDSL(BindableColumn column) { this.column = Objects.requireNonNull(column); @@ -56,14 +54,9 @@ public BasicWhenFinisher when(T condition, List subsequentConditions) { } @SuppressWarnings("java:S100") - public SimpleCaseEnder else_(String value) { - elseValue = quoteStringForSQL(value); - return new SimpleCaseEnder(); - } - - @SuppressWarnings("java:S100") - public SimpleCaseEnder else_(Object value) { - elseValue = value; + @Override + public SimpleCaseEnder else_(BasicColumn column) { + elseValue = column; return new SimpleCaseEnder(); } @@ -75,7 +68,7 @@ public BasicColumn end() { .build(); } - public class ConditionBasedWhenFinisher { + public class ConditionBasedWhenFinisher implements ThenDSL> { private final List> conditions = new ArrayList<>(); private ConditionBasedWhenFinisher(VisitableCondition condition, @@ -84,18 +77,14 @@ private ConditionBasedWhenFinisher(VisitableCondition condition, conditions.addAll(subsequentConditions); } - public SimpleCaseDSL then(String value) { - whenConditions.add(new ConditionBasedWhenCondition<>(conditions, quoteStringForSQL(value))); - return SimpleCaseDSL.this; - } - - public SimpleCaseDSL then(Object value) { - whenConditions.add(new ConditionBasedWhenCondition<>(conditions, value)); + @Override + public SimpleCaseDSL then(BasicColumn column) { + whenConditions.add(new ConditionBasedWhenCondition<>(conditions, column)); return SimpleCaseDSL.this; } } - public class BasicWhenFinisher { + public class BasicWhenFinisher implements ThenDSL> { private final List values = new ArrayList<>(); private BasicWhenFinisher(T value, List subsequentValues) { @@ -103,13 +92,9 @@ private BasicWhenFinisher(T value, List subsequentValues) { values.addAll(subsequentValues); } - public SimpleCaseDSL then(String value) { - whenConditions.add(new BasicWhenCondition<>(values, quoteStringForSQL(value))); - return SimpleCaseDSL.this; - } - - public SimpleCaseDSL then(Object value) { - whenConditions.add(new BasicWhenCondition<>(values, value)); + @Override + public SimpleCaseDSL then(BasicColumn column) { + whenConditions.add(new BasicWhenCondition<>(values, column)); return SimpleCaseDSL.this; } } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseModel.java b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseModel.java index 3393c6783..4b71407ae 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseModel.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseModel.java @@ -31,7 +31,7 @@ public class SimpleCaseModel implements BasicColumn { private final BindableColumn column; private final List> whenConditions; - private final Object elseValue; + private final BasicColumn elseValue; private final String alias; private SimpleCaseModel(Builder builder) { @@ -50,7 +50,7 @@ public Stream> whenConditions() { return whenConditions.stream(); } - public Optional elseValue() { + public Optional elseValue() { return Optional.ofNullable(elseValue); } @@ -77,7 +77,7 @@ public FragmentAndParameters render(RenderingContext renderingContext) { public static class Builder { private BindableColumn column; private final List> whenConditions = new ArrayList<>(); - private Object elseValue; + private BasicColumn elseValue; private String alias; public Builder withColumn(BindableColumn column) { @@ -90,7 +90,7 @@ public Builder withWhenConditions(List> whenCondit return this; } - public Builder withElseValue(Object elseValue) { + public Builder withElseValue(BasicColumn elseValue) { this.elseValue = elseValue; return this; } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseWhenCondition.java b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseWhenCondition.java index a803887c8..5466f2f3f 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseWhenCondition.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseWhenCondition.java @@ -17,14 +17,16 @@ import java.util.Objects; +import org.mybatis.dynamic.sql.BasicColumn; + public abstract class SimpleCaseWhenCondition { - private final Object thenValue; + private final BasicColumn thenValue; - protected SimpleCaseWhenCondition(Object thenValue) { + protected SimpleCaseWhenCondition(BasicColumn thenValue) { this.thenValue = Objects.requireNonNull(thenValue); } - public Object thenValue() { + public BasicColumn thenValue() { return thenValue; } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/ThenDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/ThenDSL.java new file mode 100644 index 000000000..e88fb13c3 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/ThenDSL.java @@ -0,0 +1,45 @@ +/* + * Copyright 2016-2024 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.select.caseexpression; + +import org.mybatis.dynamic.sql.BasicColumn; +import org.mybatis.dynamic.sql.Constant; +import org.mybatis.dynamic.sql.StringConstant; + +public interface ThenDSL { + + default T then(String value) { + return then(StringConstant.of(value)); + } + + default T then(Boolean value) { + return then(Constant.of(value.toString())); + } + + default T then(Integer value) { + return then(Constant.of(value.toString())); + } + + default T then(Long value) { + return then(Constant.of(value.toString())); + } + + default T then(Double value) { + return then(Constant.of(value.toString())); + } + + T then(BasicColumn column); +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/function/Cast.java b/src/main/java/org/mybatis/dynamic/sql/select/function/Cast.java new file mode 100644 index 000000000..11c31e352 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/function/Cast.java @@ -0,0 +1,82 @@ +/* + * Copyright 2016-2024 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.select.function; + +import java.util.Objects; +import java.util.Optional; + +import org.mybatis.dynamic.sql.BasicColumn; +import org.mybatis.dynamic.sql.render.RenderingContext; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; + +public class Cast implements BasicColumn { + private final BasicColumn column; + private final String targetType; + private final String alias; + + private Cast(Builder builder) { + column = Objects.requireNonNull(builder.column); + targetType = Objects.requireNonNull(builder.targetType); + alias = builder.alias; + } + + @Override + public Optional alias() { + return Optional.ofNullable(alias); + } + + @Override + public Cast as(String alias) { + return new Builder().withColumn(column) + .withTargetType(targetType) + .withAlias(alias) + .build(); + } + + @Override + public FragmentAndParameters render(RenderingContext renderingContext) { + return column.render(renderingContext).mapFragment(this::applyCast); + } + + private String applyCast(String in) { + return "cast(" + in + " as " + targetType + ")"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } + + public static class Builder { + private BasicColumn column; + private String targetType; + private String alias; + + public Builder withColumn(BasicColumn column) { + this.column = column; + return this; + } + + public Builder withTargetType(String targetType) { + this.targetType = targetType; + return this; + } + + public Builder withAlias(String alias) { + this.alias = alias; + return this; + } + + public Cast build() { + return new Cast(this); + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/SearchedCaseRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/SearchedCaseRenderer.java index 8b1403a3b..91846cee2 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/render/SearchedCaseRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/SearchedCaseRenderer.java @@ -16,10 +16,11 @@ package org.mybatis.dynamic.sql.select.render; import java.util.Objects; -import java.util.function.Function; +import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.mybatis.dynamic.sql.BasicColumn; import org.mybatis.dynamic.sql.exception.InvalidSqlException; import org.mybatis.dynamic.sql.render.RenderingContext; import org.mybatis.dynamic.sql.select.caseexpression.SearchedCaseModel; @@ -37,27 +38,30 @@ public SearchedCaseRenderer(SearchedCaseModel searchedCaseModel, RenderingContex } public FragmentAndParameters render() { - return Stream.of( - renderCase(), - renderWhenConditions(), - renderElse(), - renderEnd() - ) - .flatMap(Function.identity()) + FragmentAndParameters caseFragment = renderCase(); + FragmentAndParameters whenFragment = renderWhenConditions(); + Optional elseFragment = renderElse(); + FragmentAndParameters endFragment = renderEnd(); + + return elseFragment.map(ef -> Stream.of(caseFragment, whenFragment, ef, endFragment)) + .orElseGet(() -> Stream.of(caseFragment, whenFragment, endFragment)) .collect(FragmentCollector.collect()) .toFragmentAndParameters(Collectors.joining(" ")); //$NON-NLS-1$ } - private Stream renderCase() { - return Stream.of(FragmentAndParameters.fromFragment("case")); //$NON-NLS-1$ + private FragmentAndParameters renderCase() { + return FragmentAndParameters.fromFragment("case"); //$NON-NLS-1$ } - private Stream renderWhenConditions() { - return searchedCaseModel.whenConditions().flatMap(this::renderWhenCondition); + private FragmentAndParameters renderWhenConditions() { + return searchedCaseModel.whenConditions().map(this::renderWhenCondition) + .collect(FragmentCollector.collect()) + .toFragmentAndParameters(Collectors.joining(" ")); //$NON-NLS-1$ } - private Stream renderWhenCondition(SearchedCaseModel.SearchedWhenCondition whenCondition) { - return Stream.of(renderWhen(whenCondition), renderThen(whenCondition)); + private FragmentAndParameters renderWhenCondition(SearchedCaseModel.SearchedWhenCondition whenCondition) { + return Stream.of(renderWhen(whenCondition), renderThen(whenCondition)).collect(FragmentCollector.collect()) + .toFragmentAndParameters(Collectors.joining(" ")); //$NON-NLS-1$ } private FragmentAndParameters renderWhen(SearchedCaseModel.SearchedWhenCondition whenCondition) { @@ -70,18 +74,18 @@ private FragmentAndParameters renderWhen(SearchedCaseModel.SearchedWhenCondition } private FragmentAndParameters renderThen(SearchedCaseModel.SearchedWhenCondition whenCondition) { - return FragmentAndParameters.fromFragment("then " + whenCondition.thenValue()); //$NON-NLS-1$ + return whenCondition.thenValue().render(renderingContext).mapFragment(f -> "then " + f); } - private Stream renderElse() { - return searchedCaseModel.elseValue().map(this::renderElse).map(Stream::of).orElseGet(Stream::empty); + private Optional renderElse() { + return searchedCaseModel.elseValue().map(this::renderElse); } - private FragmentAndParameters renderElse(Object elseValue) { - return FragmentAndParameters.fromFragment("else " + elseValue); //$NON-NLS-1$ + private FragmentAndParameters renderElse(BasicColumn elseValue) { + return elseValue.render(renderingContext).mapFragment(f -> "else " + f); //$NON-NLS-1$ } - private Stream renderEnd() { - return Stream.of(FragmentAndParameters.fromFragment("end")); //$NON-NLS-1$ + private FragmentAndParameters renderEnd() { + return FragmentAndParameters.fromFragment("end"); //$NON-NLS-1$ } } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseRenderer.java index 263bf732c..a401f2aaf 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseRenderer.java @@ -16,10 +16,11 @@ package org.mybatis.dynamic.sql.select.render; import java.util.Objects; -import java.util.function.Function; +import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.mybatis.dynamic.sql.BasicColumn; import org.mybatis.dynamic.sql.render.RenderingContext; import org.mybatis.dynamic.sql.select.caseexpression.SimpleCaseModel; import org.mybatis.dynamic.sql.select.caseexpression.SimpleCaseWhenCondition; @@ -38,34 +39,35 @@ public SimpleCaseRenderer(SimpleCaseModel simpleCaseModel, RenderingContext r } public FragmentAndParameters render() { - return Stream.of( - renderCase(), - renderWhenConditions(), - renderElse(), - renderEnd() - ) - .flatMap(Function.identity()) + FragmentAndParameters caseFragment = renderCase(); + FragmentAndParameters whenFragment = renderWhenConditions(); + Optional elseFragment = renderElse(); + FragmentAndParameters endFragment = renderEnd(); + + return elseFragment.map(ef -> Stream.of(caseFragment, whenFragment, ef, endFragment)) + .orElseGet(() -> Stream.of(caseFragment, whenFragment, endFragment)) .collect(FragmentCollector.collect()) .toFragmentAndParameters(Collectors.joining(" ")); //$NON-NLS-1$ } - private Stream renderCase() { - return Stream.of( - FragmentAndParameters.fromFragment("case"), //$NON-NLS-1$ - simpleCaseModel.column().render(renderingContext) - ); + private FragmentAndParameters renderCase() { + return simpleCaseModel.column().render(renderingContext) + .mapFragment(f -> "case " + f); //$NON-NLS-1$ } - private Stream renderWhenConditions() { - return simpleCaseModel.whenConditions().flatMap(this::renderWhenCondition); + private FragmentAndParameters renderWhenConditions() { + return simpleCaseModel.whenConditions().map(this::renderWhenCondition) + .collect(FragmentCollector.collect()) + .toFragmentAndParameters(Collectors.joining(" ")); //$NON-NLS-1$ } - private Stream renderWhenCondition(SimpleCaseWhenCondition whenCondition) { + private FragmentAndParameters renderWhenCondition(SimpleCaseWhenCondition whenCondition) { return Stream.of( renderWhen(), renderConditions(whenCondition), renderThen(whenCondition) - ); + ).collect(FragmentCollector.collect()) + .toFragmentAndParameters(Collectors.joining(" ")); //$NON-NLS-1$ } private FragmentAndParameters renderWhen() { @@ -77,18 +79,19 @@ private FragmentAndParameters renderConditions(SimpleCaseWhenCondition whenCo } private FragmentAndParameters renderThen(SimpleCaseWhenCondition whenCondition) { - return FragmentAndParameters.fromFragment("then " + whenCondition.thenValue()); //$NON-NLS-1$ + return whenCondition.thenValue().render(renderingContext) + .mapFragment(f -> "then " + f); //$NON-NLS-1$ } - private Stream renderElse() { - return simpleCaseModel.elseValue().map(this::renderElse).map(Stream::of).orElseGet(Stream::empty); + private Optional renderElse() { + return simpleCaseModel.elseValue().map(this::renderElse); } - private FragmentAndParameters renderElse(Object elseValue) { - return FragmentAndParameters.fromFragment("else " + elseValue); //$NON-NLS-1$ + private FragmentAndParameters renderElse(BasicColumn elseValue) { + return elseValue.render(renderingContext).mapFragment(f -> "else " + f); //$NON-NLS-1$ } - private Stream renderEnd() { - return Stream.of(FragmentAndParameters.fromFragment("end")); //$NON-NLS-1$ + private FragmentAndParameters renderEnd() { + return FragmentAndParameters.fromFragment("end"); //$NON-NLS-1$ } } 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 868502ced..e3f244f8e 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 @@ -15,6 +15,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.select.caseexpression.BasicWhenCondition import org.mybatis.dynamic.sql.select.caseexpression.ConditionBasedWhenCondition @@ -23,8 +24,8 @@ import org.mybatis.dynamic.sql.select.caseexpression.SimpleCaseWhenCondition import org.mybatis.dynamic.sql.util.kotlin.GroupingCriteriaCollector import org.mybatis.dynamic.sql.util.kotlin.assertNull -class KSearchedCaseDSL { - internal var elseValue: Any? = null +class KSearchedCaseDSL : KElseDSL { + internal var elseValue: BasicColumn? = null private set(value) { assertNull(field, "ERROR.42") //$NON-NLS-1$ field = value @@ -36,33 +37,25 @@ class KSearchedCaseDSL { whenConditions.add(SearchedWhenCondition(dsl.initialCriterion, dsl.subCriteria, dsl.thenValue)) } - fun `else`(value: String) { - this.elseValue = "'$value'" - } - - fun `else`(value: Any) { - this.elseValue = value + override fun `else`(column: BasicColumn) { + this.elseValue = column } } -class SearchedCaseCriteriaCollector : GroupingCriteriaCollector() { - internal var thenValue: Any? = null +class SearchedCaseCriteriaCollector : GroupingCriteriaCollector(), KThenDSL { + internal var thenValue: BasicColumn? = null private set(value) { assertNull(field, "ERROR.41") //$NON-NLS-1$ field = value } - fun then(value: String) { - thenValue = "'$value'" - } - - fun then(value: Any) { - thenValue = value + override fun then(column: BasicColumn) { + thenValue = column } } -class KSimpleCaseDSL { - internal var elseValue: Any? = null +class KSimpleCaseDSL : KElseDSL { + internal var elseValue: BasicColumn? = null private set(value) { assertNull(field, "ERROR.42") //$NON-NLS-1$ field = value @@ -90,27 +83,67 @@ class KSimpleCaseDSL { whenConditions.add(BasicWhenCondition(allConditions, thenValue)) } - fun `else`(value: String) { - this.elseValue = "'$value'" - } - - fun `else`(value: Any) { - this.elseValue = value + override fun `else`(column: BasicColumn) { + this.elseValue = column } } -class SimpleCaseThenGatherer { - internal var thenValue: Any? = null +class SimpleCaseThenGatherer: KThenDSL { + internal var thenValue: BasicColumn? = null private set(value) { assertNull(field, "ERROR.41") //$NON-NLS-1$ field = value } + override fun then(column: BasicColumn) { + thenValue = column + } +} + +interface KThenDSL { fun then(value: String) { - thenValue = "'$value'" + then(stringConstant(value)) + } + + fun then(value: Boolean) { + then(constant(value.toString())) + } + + fun then(value: Int) { + then(constant(value.toString())) } - fun then(value: Any) { - thenValue = value + fun then(value: Long) { + then(constant(value.toString())) } + + fun then(value: Double) { + then(constant(value.toString())) + } + + fun then(column: BasicColumn) +} + +interface KElseDSL { + fun `else`(value: String) { + `else`(stringConstant(value)) + } + + fun `else`(value: Boolean) { + `else`(constant(value.toString())) + } + + fun `else`(value: Int) { + `else`(constant(value.toString())) + } + + fun `else`(value: Long) { + `else`(constant(value.toString())) + } + + fun `else`(value: Double) { + `else`(constant(value.toString())) + } + + fun `else`(column: BasicColumn) } 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 7cc681a52..706d23634 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 @@ -23,6 +23,7 @@ import org.mybatis.dynamic.sql.BoundValue import org.mybatis.dynamic.sql.Constant import org.mybatis.dynamic.sql.SortSpecification import org.mybatis.dynamic.sql.SqlBuilder +import org.mybatis.dynamic.sql.SqlBuilder.CastFinisher import org.mybatis.dynamic.sql.SqlColumn import org.mybatis.dynamic.sql.StringConstant import org.mybatis.dynamic.sql.VisitableCondition @@ -36,6 +37,7 @@ import org.mybatis.dynamic.sql.select.aggregate.Max import org.mybatis.dynamic.sql.select.aggregate.Min import org.mybatis.dynamic.sql.select.aggregate.Sum import org.mybatis.dynamic.sql.select.function.Add +import org.mybatis.dynamic.sql.select.function.Cast import org.mybatis.dynamic.sql.select.function.Concat import org.mybatis.dynamic.sql.select.function.Concatenate import org.mybatis.dynamic.sql.select.function.Divide @@ -165,6 +167,10 @@ fun subtract( vararg subsequentColumns: BasicColumn ): Subtract = Subtract.of(firstColumn, secondColumn, subsequentColumns.asList()) +fun cast(column: BasicColumn) = SqlBuilder.cast(column) + +infix fun CastFinisher.`as`(targetType: String): Cast = this.`as`(targetType) + fun concat( firstColumn: BindableColumn, vararg subsequentColumns: BasicColumn diff --git a/src/test/java/examples/animal/data/CaseExpressionTest.java b/src/test/java/examples/animal/data/CaseExpressionTest.java index 7dfcd77e0..c39fcd97d 100644 --- a/src/test/java/examples/animal/data/CaseExpressionTest.java +++ b/src/test/java/examples/animal/data/CaseExpressionTest.java @@ -23,6 +23,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.entry; import static org.mybatis.dynamic.sql.SqlBuilder.and; +import static org.mybatis.dynamic.sql.SqlBuilder.cast; import static org.mybatis.dynamic.sql.SqlBuilder.group; import static org.mybatis.dynamic.sql.SqlBuilder.isEqualTo; import static org.mybatis.dynamic.sql.SqlBuilder.isEqualToWhenPresent; @@ -31,6 +32,7 @@ import static org.mybatis.dynamic.sql.SqlBuilder.or; import static org.mybatis.dynamic.sql.SqlBuilder.case_; import static org.mybatis.dynamic.sql.SqlBuilder.select; +import static org.mybatis.dynamic.sql.SqlBuilder.value; import java.io.InputStream; import java.io.InputStreamReader; @@ -181,6 +183,58 @@ void testSearchedCaseWithIntegers() { } } + @Test + void testSearchedCaseWithBoundValues() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(animalName, case_() + .when(animalName, isEqualTo("Artic fox")).or(animalName, isEqualTo("Red fox")).then(value("Fox")) + .when(animalName, isEqualTo("Little brown bat")).or(animalName, isEqualTo("Big brown bat")).then(value("Bat")) + .else_(cast(value("Not a Fox or a bat")).as("VARCHAR(30)")).end().as("AnimalType")) + .from(animalData, "a") + .where(id, isIn(2, 3, 31, 32, 38, 39)) + .orderBy(id) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expected = "select a.animal_name, case " + + "when a.animal_name = #{parameters.p1,jdbcType=VARCHAR} or a.animal_name = #{parameters.p2,jdbcType=VARCHAR} then #{parameters.p3} " + + "when a.animal_name = #{parameters.p4,jdbcType=VARCHAR} or a.animal_name = #{parameters.p5,jdbcType=VARCHAR} then #{parameters.p6} " + + "else cast(#{parameters.p7} as VARCHAR(30)) end as AnimalType " + + "from AnimalData a where a.id in (" + + "#{parameters.p8,jdbcType=INTEGER},#{parameters.p9,jdbcType=INTEGER}," + + "#{parameters.p10,jdbcType=INTEGER},#{parameters.p11,jdbcType=INTEGER},#{parameters.p12,jdbcType=INTEGER}," + + "#{parameters.p13,jdbcType=INTEGER}) " + + "order by id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expected); + assertThat(selectStatement.getParameters()).containsOnly( + entry("p1", "Artic fox"), + entry("p2", "Red fox"), + entry("p3", "Fox"), + entry("p4", "Little brown bat"), + entry("p5", "Big brown bat"), + entry("p6", "Bat"), + entry("p7", "Not a Fox or a bat"), + entry("p8", 2), + entry("p9", 3), + entry("p10", 31), + entry("p11", 32), + entry("p12", 38), + entry("p13", 39) + ); + + List> records = mapper.selectManyMappedRows(selectStatement); + assertThat(records).hasSize(6); + assertThat(records.get(0)).containsOnly(entry("ANIMAL_NAME", "Little brown bat"), entry("ANIMALTYPE", "Bat")); + assertThat(records.get(1)).containsOnly(entry("ANIMAL_NAME", "Big brown bat"), entry("ANIMALTYPE", "Bat")); + assertThat(records.get(2)).containsOnly(entry("ANIMAL_NAME", "Cat"), entry("ANIMALTYPE", "Not a Fox or a bat")); + assertThat(records.get(3)).containsOnly(entry("ANIMAL_NAME", "Artic fox"), entry("ANIMALTYPE", "Fox")); + assertThat(records.get(4)).containsOnly(entry("ANIMAL_NAME", "Red fox"), entry("ANIMALTYPE", "Fox")); + assertThat(records.get(5)).containsOnly(entry("ANIMAL_NAME", "Raccoon"), entry("ANIMALTYPE", "Not a Fox or a bat")); + } + } + @Test void testSearchedCaseNoElse() { try (SqlSession sqlSession = sqlSessionFactory.openSession()) { @@ -427,6 +481,47 @@ void testSimpleCaseWithBooleans() { } } + @Test + void testSimpleCaseWithBoundValues() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(animalName, SqlBuilder.case_(animalName) + .when(isEqualTo("Artic fox"), isEqualTo("Red fox")).then(value("yes")) + .else_(cast(value("no")).as("VARCHAR(30)")).end().as("IsAFox")) + .from(animalData) + .where(id, isIn(31, 32, 38, 39)) + .orderBy(id) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expected = "select animal_name, " + + "case animal_name when = #{parameters.p1,jdbcType=VARCHAR}, = #{parameters.p2,jdbcType=VARCHAR} then #{parameters.p3} " + + "else cast(#{parameters.p4} as VARCHAR(30)) end " + + "as IsAFox from AnimalData where id in " + + "(#{parameters.p5,jdbcType=INTEGER},#{parameters.p6,jdbcType=INTEGER},#{parameters.p7,jdbcType=INTEGER},#{parameters.p8,jdbcType=INTEGER}) " + + "order by id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expected); + assertThat(selectStatement.getParameters()).containsOnly( + entry("p1", "Artic fox"), + entry("p2", "Red fox"), + entry("p3", "yes"), + entry("p4", "no"), + entry("p5", 31), + entry("p6", 32), + entry("p7", 38), + entry("p8", 39) + ); + + List> records = mapper.selectManyMappedRows(selectStatement); + assertThat(records).hasSize(4); + assertThat(records.get(0)).containsOnly(entry("ANIMAL_NAME", "Cat"), entry("ISAFOX", "no")); + assertThat(records.get(1)).containsOnly(entry("ANIMAL_NAME", "Artic fox"), entry("ISAFOX", "yes")); + assertThat(records.get(2)).containsOnly(entry("ANIMAL_NAME", "Red fox"), entry("ISAFOX", "yes")); + assertThat(records.get(3)).containsOnly(entry("ANIMAL_NAME", "Raccoon"), entry("ISAFOX", "no")); + } + } + @Test void testSimpleCaseBasicWithBooleans() { try (SqlSession sqlSession = sqlSessionFactory.openSession()) { diff --git a/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt b/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt index e47a5ab84..da12a2d1e 100644 --- a/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt +++ b/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt @@ -28,8 +28,11 @@ import org.junit.jupiter.api.Test import org.mybatis.dynamic.sql.exception.InvalidSqlException import org.mybatis.dynamic.sql.util.Messages import org.mybatis.dynamic.sql.util.kotlin.KInvalidSQLException +import org.mybatis.dynamic.sql.util.kotlin.elements.`as` import org.mybatis.dynamic.sql.util.kotlin.elements.case +import org.mybatis.dynamic.sql.util.kotlin.elements.cast import org.mybatis.dynamic.sql.util.kotlin.elements.isEqualTo +import org.mybatis.dynamic.sql.util.kotlin.elements.value import org.mybatis.dynamic.sql.util.kotlin.mybatis3.select import org.mybatis.dynamic.sql.util.mybatis3.CommonSelectMapper @@ -196,6 +199,86 @@ class KCaseExpressionTest { } } + @Test + fun testSearchedCaseWithBoundValues() { + newSession().use { session -> + val mapper = session.getMapper(CommonSelectMapper::class.java) + + val selectStatement = select(animalName, + case { + `when` { + animalName isEqualTo "Artic fox" + or { animalName isEqualTo "Red fox" } + then(value("Fox")) + } + `when` { + animalName isEqualTo "Little brown bat" + or { animalName isEqualTo "Big brown bat" } + then(value("Bat")) + } + `else`(cast(value("Not a Fox or a bat")) `as` "VARCHAR(30)") + }.`as`("AnimalType") + ) { + from(animalData, "a") + where { id.isIn(2, 3, 31, 32, 38, 39) } + orderBy(id) + } + + val expected = "select a.animal_name, case " + + "when a.animal_name = #{parameters.p1,jdbcType=VARCHAR} or a.animal_name = #{parameters.p2,jdbcType=VARCHAR} then #{parameters.p3} " + + "when a.animal_name = #{parameters.p4,jdbcType=VARCHAR} or a.animal_name = #{parameters.p5,jdbcType=VARCHAR} then #{parameters.p6} " + + "else cast(#{parameters.p7} as VARCHAR(30)) end as AnimalType " + + "from AnimalData a where a.id in (" + + "#{parameters.p8,jdbcType=INTEGER},#{parameters.p9,jdbcType=INTEGER}," + + "#{parameters.p10,jdbcType=INTEGER},#{parameters.p11,jdbcType=INTEGER},#{parameters.p12,jdbcType=INTEGER}," + + "#{parameters.p13,jdbcType=INTEGER}) " + + "order by id" + assertThat(selectStatement.selectStatement).isEqualTo(expected) + assertThat(selectStatement.parameters).containsOnly( + entry("p1", "Artic fox"), + entry("p2", "Red fox"), + entry("p3", "Fox"), + entry("p4", "Little brown bat"), + entry("p5", "Big brown bat"), + entry("p6", "Bat"), + entry("p7", "Not a Fox or a bat"), + entry("p8", 2), + entry("p9", 3), + entry("p10", 31), + entry("p11", 32), + entry("p12", 38), + entry("p13", 39) + ) + + val records = mapper.selectManyMappedRows(selectStatement) + assertThat(records).hasSize(6) + assertThat(records[0]).containsOnly( + entry("ANIMAL_NAME", "Little brown bat"), + entry("ANIMALTYPE", "Bat") + ) + assertThat(records[1]).containsOnly( + entry("ANIMAL_NAME", "Big brown bat"), + entry("ANIMALTYPE", "Bat") + ) + assertThat(records[2]).containsOnly( + entry("ANIMAL_NAME", "Cat"), + entry("ANIMALTYPE", "Not a Fox or a bat") + ) + assertThat(records[3]).containsOnly( + entry("ANIMAL_NAME", "Artic fox"), + entry("ANIMALTYPE", "Fox") + ) + assertThat(records[4]).containsOnly( + entry("ANIMAL_NAME", "Red fox"), + entry("ANIMALTYPE", "Fox") + ) + assertThat(records[5]).containsOnly( + entry("ANIMAL_NAME", "Raccoon"), + entry("ANIMALTYPE", "Not a Fox or a bat") + ) + } + } + @Test fun testSearchedCaseNoElse() { newSession().use { session -> From 777cef935fb08a421d67142db2dc6225515f057f Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Thu, 14 Mar 2024 17:03:16 -0400 Subject: [PATCH 10/19] Coverage --- .../org/mybatis/dynamic/sql/SqlBuilder.java | 4 + .../mybatis/dynamic/sql/StringConstant.java | 4 +- .../dynamic/sql/util/StringUtilities.java | 4 - .../sql/util/kotlin/elements/SqlElements.kt | 4 +- .../animal/data/CaseExpressionTest.java | 131 ++++++++++++++++-- .../kotlin/animal/data/KCaseExpressionTest.kt | 130 +++++++++++++++++ 6 files changed, 261 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java index e107cdd83..1307b27c5 100644 --- a/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java +++ b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java @@ -531,6 +531,10 @@ static Subtract subtract(BindableColumn firstColumn, BasicColumn secon return Subtract.of(firstColumn, secondColumn, subsequentColumns); } + static CastFinisher cast(String value) { + return cast(stringConstant(value)); + } + static CastFinisher cast(BasicColumn column) { return new CastFinisher(column); } diff --git a/src/main/java/org/mybatis/dynamic/sql/StringConstant.java b/src/main/java/org/mybatis/dynamic/sql/StringConstant.java index 522ac41eb..ca38ed1da 100644 --- a/src/main/java/org/mybatis/dynamic/sql/StringConstant.java +++ b/src/main/java/org/mybatis/dynamic/sql/StringConstant.java @@ -20,7 +20,6 @@ import org.mybatis.dynamic.sql.render.RenderingContext; import org.mybatis.dynamic.sql.util.FragmentAndParameters; -import org.mybatis.dynamic.sql.util.StringUtilities; public class StringConstant implements BindableColumn { @@ -43,7 +42,8 @@ public Optional alias() { @Override public FragmentAndParameters render(RenderingContext renderingContext) { - return FragmentAndParameters.fromFragment(StringUtilities.quoteStringForSQL(value)); + String escaped = value.replace("'", "''"); //$NON-NLS-1$ //$NON-NLS-2$ + return FragmentAndParameters.fromFragment("'" + escaped + "'"); //$NON-NLS-1$ //$NON-NLS-2$ } @Override diff --git a/src/main/java/org/mybatis/dynamic/sql/util/StringUtilities.java b/src/main/java/org/mybatis/dynamic/sql/util/StringUtilities.java index 5fe080d52..0606c0e7c 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/StringUtilities.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/StringUtilities.java @@ -52,8 +52,4 @@ static String toCamelCase(String inputString) { return sb.toString(); } - - static String quoteStringForSQL(String value) { - return "'" + value + "'"; //$NON-NLS-1$ //$NON-NLS-2$ - } } 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 706d23634..9e5f8818c 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 @@ -167,7 +167,9 @@ fun subtract( vararg subsequentColumns: BasicColumn ): Subtract = Subtract.of(firstColumn, secondColumn, subsequentColumns.asList()) -fun cast(column: BasicColumn) = SqlBuilder.cast(column) +fun cast(value: String): CastFinisher = SqlBuilder.cast(value) + +fun cast(column: BasicColumn): CastFinisher = SqlBuilder.cast(column) infix fun CastFinisher.`as`(targetType: String): Cast = this.`as`(targetType) diff --git a/src/test/java/examples/animal/data/CaseExpressionTest.java b/src/test/java/examples/animal/data/CaseExpressionTest.java index c39fcd97d..322d9504b 100644 --- a/src/test/java/examples/animal/data/CaseExpressionTest.java +++ b/src/test/java/examples/animal/data/CaseExpressionTest.java @@ -22,8 +22,10 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.entry; +import static org.mybatis.dynamic.sql.SqlBuilder.add; import static org.mybatis.dynamic.sql.SqlBuilder.and; import static org.mybatis.dynamic.sql.SqlBuilder.cast; +import static org.mybatis.dynamic.sql.SqlBuilder.constant; import static org.mybatis.dynamic.sql.SqlBuilder.group; import static org.mybatis.dynamic.sql.SqlBuilder.isEqualTo; import static org.mybatis.dynamic.sql.SqlBuilder.isEqualToWhenPresent; @@ -36,6 +38,7 @@ import java.io.InputStream; import java.io.InputStreamReader; +import java.math.BigDecimal; import java.sql.Connection; import java.sql.DriverManager; import java.util.List; @@ -51,7 +54,6 @@ import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mybatis.dynamic.sql.SqlBuilder; import org.mybatis.dynamic.sql.exception.InvalidSqlException; import org.mybatis.dynamic.sql.render.RenderingStrategies; import org.mybatis.dynamic.sql.select.caseexpression.SearchedCaseDSL; @@ -345,7 +347,7 @@ void testSimpleCassLessThan() { try (SqlSession sqlSession = sqlSessionFactory.openSession()) { CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); - SelectStatementProvider selectStatement = select(animalName, SqlBuilder.case_(brainWeight) + SelectStatementProvider selectStatement = select(animalName, case_(brainWeight) .when(isLessThan(4.0)).then("small brain") .else_("large brain").end().as("brain_size")) .from(animalData) @@ -372,7 +374,7 @@ void testSimpleCaseWithStrings() { try (SqlSession sqlSession = sqlSessionFactory.openSession()) { CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); - SelectStatementProvider selectStatement = select(animalName, SqlBuilder.case_(animalName) + SelectStatementProvider selectStatement = select(animalName, case_(animalName) .when(isEqualTo("Artic fox"), isEqualTo("Red fox")).then("yes") .else_("no").end().as("IsAFox")) .from(animalData) @@ -410,7 +412,7 @@ void testSimpleCaseBasicWithStrings() { try (SqlSession sqlSession = sqlSessionFactory.openSession()) { CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); - SelectStatementProvider selectStatement = select(animalName, SqlBuilder.case_(animalName) + SelectStatementProvider selectStatement = select(animalName, case_(animalName) .when("Artic fox", "Red fox").then("yes") .else_("no").end().as("IsAFox")) .from(animalData) @@ -448,7 +450,7 @@ void testSimpleCaseWithBooleans() { try (SqlSession sqlSession = sqlSessionFactory.openSession()) { CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); - SelectStatementProvider selectStatement = select(animalName, SqlBuilder.case_(animalName) + SelectStatementProvider selectStatement = select(animalName, case_(animalName) .when(isEqualTo("Artic fox"), isEqualTo("Red fox")).then(true) .else_(false).end().as("IsAFox")) .from(animalData) @@ -486,7 +488,7 @@ void testSimpleCaseWithBoundValues() { try (SqlSession sqlSession = sqlSessionFactory.openSession()) { CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); - SelectStatementProvider selectStatement = select(animalName, SqlBuilder.case_(animalName) + SelectStatementProvider selectStatement = select(animalName, case_(animalName) .when(isEqualTo("Artic fox"), isEqualTo("Red fox")).then(value("yes")) .else_(cast(value("no")).as("VARCHAR(30)")).end().as("IsAFox")) .from(animalData) @@ -527,7 +529,7 @@ void testSimpleCaseBasicWithBooleans() { try (SqlSession sqlSession = sqlSessionFactory.openSession()) { CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); - SelectStatementProvider selectStatement = select(animalName, SqlBuilder.case_(animalName) + SelectStatementProvider selectStatement = select(animalName, case_(animalName) .when("Artic fox", "Red fox").then(true) .else_(false).end().as("IsAFox")) .from(animalData) @@ -565,7 +567,7 @@ void testSimpleCaseNoElse() { try (SqlSession sqlSession = sqlSessionFactory.openSession()) { CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); - SelectStatementProvider selectStatement = select(animalName, SqlBuilder.case_(animalName) + SelectStatementProvider selectStatement = select(animalName, case_(animalName) .when(isEqualTo("Artic fox"), isEqualTo("Red fox")).then("yes") .end().as("IsAFox")) .from(animalData) @@ -598,6 +600,117 @@ void testSimpleCaseNoElse() { } } + @Test + void testSimpleCaseLongs() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(animalName, case_(animalName) + .when(isEqualTo("Artic fox"), isEqualTo("Red fox")).then(1L) + .else_(2L) + .end().as("IsAFox")) + .from(animalData) + .where(id, isIn(31, 32, 38, 39)) + .orderBy(id) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expected = "select animal_name, " + + "case animal_name when = #{parameters.p1,jdbcType=VARCHAR}, = #{parameters.p2,jdbcType=VARCHAR} then 1 else 2 end " + + "as IsAFox from AnimalData where id in " + + "(#{parameters.p3,jdbcType=INTEGER},#{parameters.p4,jdbcType=INTEGER},#{parameters.p5,jdbcType=INTEGER},#{parameters.p6,jdbcType=INTEGER}) " + + "order by id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expected); + assertThat(selectStatement.getParameters()).containsOnly( + entry("p1", "Artic fox"), + entry("p2", "Red fox"), + entry("p3", 31), + entry("p4", 32), + entry("p5", 38), + entry("p6", 39) + ); + + List> records = mapper.selectManyMappedRows(selectStatement); + assertThat(records).hasSize(4); + assertThat(records.get(0)).containsOnly(entry("ANIMAL_NAME", "Cat"), entry("ISAFOX", 2)); + assertThat(records.get(1)).containsOnly(entry("ANIMAL_NAME", "Artic fox"), entry("ISAFOX", 1)); + assertThat(records.get(2)).containsOnly(entry("ANIMAL_NAME", "Red fox"), entry("ISAFOX", 1)); + assertThat(records.get(3)).containsOnly(entry("ANIMAL_NAME", "Raccoon"), entry("ISAFOX", 2)); + } + } + + @Test + void testSimpleCaseDoubles() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(animalName, case_(animalName) + .when(isEqualTo("Artic fox"), isEqualTo("Red fox")).then(1.1) + .else_(2.2) + .end().as("IsAFox")) + .from(animalData) + .where(id, isIn(31, 32, 38, 39)) + .orderBy(id) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expected = "select animal_name, " + + "case animal_name when = #{parameters.p1,jdbcType=VARCHAR}, = #{parameters.p2,jdbcType=VARCHAR} then 1.1 else 2.2 end " + + "as IsAFox from AnimalData where id in " + + "(#{parameters.p3,jdbcType=INTEGER},#{parameters.p4,jdbcType=INTEGER},#{parameters.p5,jdbcType=INTEGER},#{parameters.p6,jdbcType=INTEGER}) " + + "order by id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expected); + assertThat(selectStatement.getParameters()).containsOnly( + entry("p1", "Artic fox"), + entry("p2", "Red fox"), + entry("p3", 31), + entry("p4", 32), + entry("p5", 38), + entry("p6", 39) + ); + + List> records = mapper.selectManyMappedRows(selectStatement); + assertThat(records).hasSize(4); + assertThat(records.get(0)).containsOnly(entry("ANIMAL_NAME", "Cat"), entry("ISAFOX", new BigDecimal("2.2"))); + assertThat(records.get(1)).containsOnly(entry("ANIMAL_NAME", "Artic fox"), entry("ISAFOX", new BigDecimal("1.1"))); + assertThat(records.get(2)).containsOnly(entry("ANIMAL_NAME", "Red fox"), entry("ISAFOX", new BigDecimal("1.1"))); + assertThat(records.get(3)).containsOnly(entry("ANIMAL_NAME", "Raccoon"), entry("ISAFOX", new BigDecimal("2.2"))); + } + } + + @Test + void testAliasCast() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(animalName, cast(add(id, constant("20"))).as("INTEGER").as("BIG_ID")) + .from(animalData) + .where(id, isIn(31, 32, 38, 39)) + .orderBy(id) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expected = "select animal_name, cast((id + 20) as INTEGER) as BIG_ID " + + "from AnimalData where id in " + + "(#{parameters.p1,jdbcType=INTEGER},#{parameters.p2,jdbcType=INTEGER},#{parameters.p3,jdbcType=INTEGER},#{parameters.p4,jdbcType=INTEGER}) " + + "order by id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expected); + assertThat(selectStatement.getParameters()).containsOnly( + entry("p1", 31), + entry("p2", 32), + entry("p3", 38), + entry("p4", 39) + ); + + List> records = mapper.selectManyMappedRows(selectStatement); + assertThat(records).hasSize(4); + assertThat(records.get(0)).containsOnly(entry("ANIMAL_NAME", "Cat"), entry("BIG_ID", 51)); + assertThat(records.get(1)).containsOnly(entry("ANIMAL_NAME", "Artic fox"), entry("BIG_ID", 52)); + assertThat(records.get(2)).containsOnly(entry("ANIMAL_NAME", "Red fox"), entry("BIG_ID", 58)); + assertThat(records.get(3)).containsOnly(entry("ANIMAL_NAME", "Raccoon"), entry("BIG_ID", 59)); + } + } + @Test void testInvalidSearchedCaseNoConditionsRender() { SelectModel model = select(animalName, case_() @@ -612,7 +725,7 @@ void testInvalidSearchedCaseNoConditionsRender() { @Test void testInvalidSimpleCaseNoConditionsRender() { - SelectModel model = select(SqlBuilder.case_(animalName) + SelectModel model = select(case_(animalName) .when(isEqualToWhenPresent((String) null)).then("Fred").end()) .from(animalData) .build(); diff --git a/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt b/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt index da12a2d1e..c212b094a 100644 --- a/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt +++ b/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt @@ -35,6 +35,7 @@ import org.mybatis.dynamic.sql.util.kotlin.elements.isEqualTo import org.mybatis.dynamic.sql.util.kotlin.elements.value import org.mybatis.dynamic.sql.util.kotlin.mybatis3.select import org.mybatis.dynamic.sql.util.mybatis3.CommonSelectMapper +import java.math.BigDecimal class KCaseExpressionTest { private fun newSession(executorType: ExecutorType = ExecutorType.REUSE): SqlSession { @@ -711,6 +712,135 @@ class KCaseExpressionTest { } } + @Test + fun testCastString() { + newSession().use { session -> + val mapper = session.getMapper(CommonSelectMapper::class.java) + + val selectStatement = select( + animalName, + case(animalName) { + `when`(isEqualTo("Artic fox"), isEqualTo("Red fox")) { then(cast("It's a fox") `as` "VARCHAR(30)") } + `else`("It's not a fox") + }.`as`("IsAFox") + ) { + from(animalData) + where { id.isIn(31, 32, 38, 39) } + orderBy(id) + } + + val expected = "select animal_name, " + + "case animal_name when = #{parameters.p1,jdbcType=VARCHAR}, = #{parameters.p2,jdbcType=VARCHAR} then cast('It''s a fox' as VARCHAR(30)) " + + "else 'It''s not a fox' end " + + "as IsAFox from AnimalData where id in " + + "(#{parameters.p3,jdbcType=INTEGER},#{parameters.p4,jdbcType=INTEGER},#{parameters.p5,jdbcType=INTEGER},#{parameters.p6,jdbcType=INTEGER}) " + + "order by id" + assertThat(selectStatement.selectStatement).isEqualTo(expected) + assertThat(selectStatement.parameters) + .containsOnly( + entry("p1", "Artic fox"), + entry("p2", "Red fox"), + entry("p3", 31), + entry("p4", 32), + entry("p5", 38), + entry("p6", 39) + ) + + val records = mapper.selectManyMappedRows(selectStatement) + assertThat(records).hasSize(4) + assertThat(records[0]).containsOnly(entry("ANIMAL_NAME", "Cat"), entry("ISAFOX", "It's not a fox")) + assertThat(records[1]).containsOnly(entry("ANIMAL_NAME", "Artic fox"), entry("ISAFOX", "It's a fox")) + assertThat(records[2]).containsOnly(entry("ANIMAL_NAME", "Red fox"), entry("ISAFOX", "It's a fox")) + assertThat(records[3]).containsOnly(entry("ANIMAL_NAME", "Raccoon"), entry("ISAFOX", "It's not a fox")) + } + } + + @Test + fun testCaseLongs() { + newSession().use { session -> + val mapper = session.getMapper(CommonSelectMapper::class.java) + + val selectStatement = select( + animalName, + case(animalName) { + `when`(isEqualTo("Artic fox"), isEqualTo("Red fox")) { then( 1L) } + `else`(2L) + }.`as`("IsAFox") + ) { + from(animalData) + where { id.isIn(31, 32, 38, 39) } + orderBy(id) + } + + val expected = "select animal_name, " + + "case animal_name when = #{parameters.p1,jdbcType=VARCHAR}, = #{parameters.p2,jdbcType=VARCHAR} then 1 " + + "else 2 end " + + "as IsAFox from AnimalData where id in " + + "(#{parameters.p3,jdbcType=INTEGER},#{parameters.p4,jdbcType=INTEGER},#{parameters.p5,jdbcType=INTEGER},#{parameters.p6,jdbcType=INTEGER}) " + + "order by id" + assertThat(selectStatement.selectStatement).isEqualTo(expected) + assertThat(selectStatement.parameters) + .containsOnly( + entry("p1", "Artic fox"), + entry("p2", "Red fox"), + entry("p3", 31), + entry("p4", 32), + entry("p5", 38), + entry("p6", 39) + ) + + val records = mapper.selectManyMappedRows(selectStatement) + assertThat(records).hasSize(4) + assertThat(records[0]).containsOnly(entry("ANIMAL_NAME", "Cat"), entry("ISAFOX", 2)) + assertThat(records[1]).containsOnly(entry("ANIMAL_NAME", "Artic fox"), entry("ISAFOX", 1)) + assertThat(records[2]).containsOnly(entry("ANIMAL_NAME", "Red fox"), entry("ISAFOX", 1)) + assertThat(records[3]).containsOnly(entry("ANIMAL_NAME", "Raccoon"), entry("ISAFOX", 2)) + } + } + + @Test + fun testCaseDoubles() { + newSession().use { session -> + val mapper = session.getMapper(CommonSelectMapper::class.java) + + val selectStatement = select( + animalName, + case(animalName) { + `when`(isEqualTo("Artic fox"), isEqualTo("Red fox")) { then( 1.1) } + `else`(2.2) + }.`as`("IsAFox") + ) { + from(animalData) + where { id.isIn(31, 32, 38, 39) } + orderBy(id) + } + + val expected = "select animal_name, " + + "case animal_name when = #{parameters.p1,jdbcType=VARCHAR}, = #{parameters.p2,jdbcType=VARCHAR} then 1.1 " + + "else 2.2 end " + + "as IsAFox from AnimalData where id in " + + "(#{parameters.p3,jdbcType=INTEGER},#{parameters.p4,jdbcType=INTEGER},#{parameters.p5,jdbcType=INTEGER},#{parameters.p6,jdbcType=INTEGER}) " + + "order by id" + assertThat(selectStatement.selectStatement).isEqualTo(expected) + assertThat(selectStatement.parameters) + .containsOnly( + entry("p1", "Artic fox"), + entry("p2", "Red fox"), + entry("p3", 31), + entry("p4", 32), + entry("p5", 38), + entry("p6", 39) + ) + + val records = mapper.selectManyMappedRows(selectStatement) + assertThat(records).hasSize(4) + assertThat(records[0]).containsOnly(entry("ANIMAL_NAME", "Cat"), entry("ISAFOX", BigDecimal("2.2"))) + assertThat(records[1]).containsOnly(entry("ANIMAL_NAME", "Artic fox"), entry("ISAFOX", BigDecimal("1.1"))) + assertThat(records[2]).containsOnly(entry("ANIMAL_NAME", "Red fox"), entry("ISAFOX", BigDecimal("1.1"))) + assertThat(records[3]).containsOnly(entry("ANIMAL_NAME", "Raccoon"), entry("ISAFOX", BigDecimal("2.2"))) + } + } + @Test fun testInvalidDoubleElseSimple() { assertThatExceptionOfType(KInvalidSQLException::class.java).isThrownBy { From 9ebb2db5227cbc07cf99da58606ae28cc1b316d4 Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Fri, 15 Mar 2024 10:22:40 -0400 Subject: [PATCH 11/19] More idiomatic DSL for Kotlin casts --- .../sql/util/kotlin/elements/CastDSL.kt | 37 +++++++++++++++++++ .../sql/util/kotlin/elements/SqlElements.kt | 9 ++--- .../dynamic/sql/util/messages.properties | 1 + .../kotlin/animal/data/KCaseExpressionTest.kt | 5 +-- 4 files changed, 43 insertions(+), 9 deletions(-) create mode 100644 src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/CastDSL.kt diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/CastDSL.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/CastDSL.kt new file mode 100644 index 000000000..98f195ece --- /dev/null +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/CastDSL.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2016-2024 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.util.kotlin.elements + +import org.mybatis.dynamic.sql.BasicColumn +import org.mybatis.dynamic.sql.SqlBuilder +import org.mybatis.dynamic.sql.select.function.Cast +import org.mybatis.dynamic.sql.util.kotlin.assertNull + +class CastDSL { + internal var cast: Cast? = null + private set(value) { + assertNull(field, "ERROR.43") //$NON-NLS-1$ + field = value + } + + infix fun String.`as`(targetType: String) { + cast = SqlBuilder.cast(this).`as`(targetType) + } + + infix fun BasicColumn.`as`(targetType: String) { + cast = SqlBuilder.cast(this).`as`(targetType) + } +} 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 9e5f8818c..64298c3f5 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 @@ -23,7 +23,6 @@ import org.mybatis.dynamic.sql.BoundValue import org.mybatis.dynamic.sql.Constant import org.mybatis.dynamic.sql.SortSpecification import org.mybatis.dynamic.sql.SqlBuilder -import org.mybatis.dynamic.sql.SqlBuilder.CastFinisher import org.mybatis.dynamic.sql.SqlColumn import org.mybatis.dynamic.sql.StringConstant import org.mybatis.dynamic.sql.VisitableCondition @@ -50,6 +49,7 @@ import org.mybatis.dynamic.sql.select.function.Upper import org.mybatis.dynamic.sql.util.kotlin.GroupingCriteriaCollector import org.mybatis.dynamic.sql.util.kotlin.GroupingCriteriaReceiver import org.mybatis.dynamic.sql.util.kotlin.KotlinSubQueryBuilder +import org.mybatis.dynamic.sql.util.kotlin.invalidIfNull import org.mybatis.dynamic.sql.where.condition.IsBetween import org.mybatis.dynamic.sql.where.condition.IsEqualTo import org.mybatis.dynamic.sql.where.condition.IsEqualToColumn @@ -167,11 +167,8 @@ fun subtract( vararg subsequentColumns: BasicColumn ): Subtract = Subtract.of(firstColumn, secondColumn, subsequentColumns.asList()) -fun cast(value: String): CastFinisher = SqlBuilder.cast(value) - -fun cast(column: BasicColumn): CastFinisher = SqlBuilder.cast(column) - -infix fun CastFinisher.`as`(targetType: String): Cast = this.`as`(targetType) +fun cast(receiver: CastDSL.() -> Unit): Cast = + invalidIfNull(CastDSL().apply(receiver).cast, "ERROR.43") fun concat( firstColumn: BindableColumn, diff --git a/src/main/resources/org/mybatis/dynamic/sql/util/messages.properties b/src/main/resources/org/mybatis/dynamic/sql/util/messages.properties index 20b1a79c9..3921f6f7f 100644 --- a/src/main/resources/org/mybatis/dynamic/sql/util/messages.properties +++ b/src/main/resources/org/mybatis/dynamic/sql/util/messages.properties @@ -59,4 +59,5 @@ ERROR.39=When clauses in case expressions must render (optional conditions are n ERROR.40=Case expressions must have at least one "when" clause ERROR.41=You cannot call "then" in a Kotlin case expression more than once ERROR.42=You cannot call `else` in a Kotlin case expression more than once +ERROR.43=A Kotlin cast expression must have one, and only one, cast INTERNAL.ERROR=Internal Error {0} diff --git a/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt b/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt index c212b094a..98d5c73f4 100644 --- a/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt +++ b/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt @@ -28,7 +28,6 @@ import org.junit.jupiter.api.Test import org.mybatis.dynamic.sql.exception.InvalidSqlException import org.mybatis.dynamic.sql.util.Messages import org.mybatis.dynamic.sql.util.kotlin.KInvalidSQLException -import org.mybatis.dynamic.sql.util.kotlin.elements.`as` import org.mybatis.dynamic.sql.util.kotlin.elements.case import org.mybatis.dynamic.sql.util.kotlin.elements.cast import org.mybatis.dynamic.sql.util.kotlin.elements.isEqualTo @@ -217,7 +216,7 @@ class KCaseExpressionTest { or { animalName isEqualTo "Big brown bat" } then(value("Bat")) } - `else`(cast(value("Not a Fox or a bat")) `as` "VARCHAR(30)") + `else`(cast { value("Not a Fox or a bat") `as` "VARCHAR(30)" }) }.`as`("AnimalType") ) { from(animalData, "a") @@ -720,7 +719,7 @@ class KCaseExpressionTest { val selectStatement = select( animalName, case(animalName) { - `when`(isEqualTo("Artic fox"), isEqualTo("Red fox")) { then(cast("It's a fox") `as` "VARCHAR(30)") } + `when`(isEqualTo("Artic fox"), isEqualTo("Red fox")) { then(cast { "It's a fox" `as` "VARCHAR(30)" })} `else`("It's not a fox") }.`as`("IsAFox") ) { From ff623a002005cc02a14dc82fc6cd4f876492ab05 Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Fri, 15 Mar 2024 11:20:04 -0400 Subject: [PATCH 12/19] Add a cast for doubles --- .../org/mybatis/dynamic/sql/SqlBuilder.java | 4 ++ .../sql/util/kotlin/elements/CastDSL.kt | 4 ++ .../dynamic/sql/util/messages.properties | 2 +- .../kotlin/animal/data/KCaseExpressionTest.kt | 43 +++++++++++++++++++ 4 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java index 1307b27c5..a100e7bf7 100644 --- a/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java +++ b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java @@ -535,6 +535,10 @@ static CastFinisher cast(String value) { return cast(stringConstant(value)); } + static CastFinisher cast(Double value) { + return cast(constant(value.toString())); + } + static CastFinisher cast(BasicColumn column) { return new CastFinisher(column); } diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/CastDSL.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/CastDSL.kt index 98f195ece..cebf26ee9 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/CastDSL.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/CastDSL.kt @@ -31,6 +31,10 @@ class CastDSL { cast = SqlBuilder.cast(this).`as`(targetType) } + infix fun Double.`as`(targetType: String) { + cast = SqlBuilder.cast(this).`as`(targetType) + } + infix fun BasicColumn.`as`(targetType: String) { cast = SqlBuilder.cast(this).`as`(targetType) } diff --git a/src/main/resources/org/mybatis/dynamic/sql/util/messages.properties b/src/main/resources/org/mybatis/dynamic/sql/util/messages.properties index 3921f6f7f..e7b125583 100644 --- a/src/main/resources/org/mybatis/dynamic/sql/util/messages.properties +++ b/src/main/resources/org/mybatis/dynamic/sql/util/messages.properties @@ -59,5 +59,5 @@ ERROR.39=When clauses in case expressions must render (optional conditions are n ERROR.40=Case expressions must have at least one "when" clause ERROR.41=You cannot call "then" in a Kotlin case expression more than once ERROR.42=You cannot call `else` in a Kotlin case expression more than once -ERROR.43=A Kotlin cast expression must have one, and only one, cast +ERROR.43=A Kotlin cast expression must have one, and only one, `as` element INTERNAL.ERROR=Internal Error {0} diff --git a/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt b/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt index 98d5c73f4..48d223870 100644 --- a/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt +++ b/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt @@ -840,6 +840,49 @@ class KCaseExpressionTest { } } + @Test + fun testCaseCastDoubles() { + newSession().use { session -> + val mapper = session.getMapper(CommonSelectMapper::class.java) + + val selectStatement = select( + animalName, + case(animalName) { + `when`(isEqualTo("Artic fox"), isEqualTo("Red fox")) { then( 1.1) } + `else`(cast { 2.2 `as` "DOUBLE" }) + }.`as`("IsAFox") + ) { + from(animalData) + where { id.isIn(31, 32, 38, 39) } + orderBy(id) + } + + val expected = "select animal_name, " + + "case animal_name when = #{parameters.p1,jdbcType=VARCHAR}, = #{parameters.p2,jdbcType=VARCHAR} then 1.1 " + + "else cast(2.2 as DOUBLE) end " + + "as IsAFox from AnimalData where id in " + + "(#{parameters.p3,jdbcType=INTEGER},#{parameters.p4,jdbcType=INTEGER},#{parameters.p5,jdbcType=INTEGER},#{parameters.p6,jdbcType=INTEGER}) " + + "order by id" + assertThat(selectStatement.selectStatement).isEqualTo(expected) + assertThat(selectStatement.parameters) + .containsOnly( + entry("p1", "Artic fox"), + entry("p2", "Red fox"), + entry("p3", 31), + entry("p4", 32), + entry("p5", 38), + entry("p6", 39) + ) + + val records = mapper.selectManyMappedRows(selectStatement) + assertThat(records).hasSize(4) + assertThat(records[0]).containsOnly(entry("ANIMAL_NAME", "Cat"), entry("ISAFOX", 2.2)) + assertThat(records[1]).containsOnly(entry("ANIMAL_NAME", "Artic fox"), entry("ISAFOX", 1.1)) + assertThat(records[2]).containsOnly(entry("ANIMAL_NAME", "Red fox"), entry("ISAFOX", 1.1)) + assertThat(records[3]).containsOnly(entry("ANIMAL_NAME", "Raccoon"), entry("ISAFOX", 2.2)) + } + } + @Test fun testInvalidDoubleElseSimple() { assertThatExceptionOfType(KInvalidSQLException::class.java).isThrownBy { From 735b23b883e99718164ec828369dda257b5e7665 Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Fri, 15 Mar 2024 11:27:33 -0400 Subject: [PATCH 13/19] Simplification --- .../sql/select/render/SearchedCaseRenderer.java | 15 ++++++--------- .../sql/select/render/SimpleCaseRenderer.java | 15 ++++++--------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/SearchedCaseRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/SearchedCaseRenderer.java index 91846cee2..050a87204 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/render/SearchedCaseRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/SearchedCaseRenderer.java @@ -38,15 +38,12 @@ public SearchedCaseRenderer(SearchedCaseModel searchedCaseModel, RenderingContex } public FragmentAndParameters render() { - FragmentAndParameters caseFragment = renderCase(); - FragmentAndParameters whenFragment = renderWhenConditions(); - Optional elseFragment = renderElse(); - FragmentAndParameters endFragment = renderEnd(); - - return elseFragment.map(ef -> Stream.of(caseFragment, whenFragment, ef, endFragment)) - .orElseGet(() -> Stream.of(caseFragment, whenFragment, endFragment)) - .collect(FragmentCollector.collect()) - .toFragmentAndParameters(Collectors.joining(" ")); //$NON-NLS-1$ + FragmentCollector fc = new FragmentCollector(); + fc.add(renderCase()); + fc.add(renderWhenConditions()); + renderElse().ifPresent(fc::add); + fc.add(renderEnd()); + return fc.toFragmentAndParameters(Collectors.joining(" ")); //$NON-NLS-1$ } private FragmentAndParameters renderCase() { diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseRenderer.java index a401f2aaf..2639d0b53 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseRenderer.java @@ -39,15 +39,12 @@ public SimpleCaseRenderer(SimpleCaseModel simpleCaseModel, RenderingContext r } public FragmentAndParameters render() { - FragmentAndParameters caseFragment = renderCase(); - FragmentAndParameters whenFragment = renderWhenConditions(); - Optional elseFragment = renderElse(); - FragmentAndParameters endFragment = renderEnd(); - - return elseFragment.map(ef -> Stream.of(caseFragment, whenFragment, ef, endFragment)) - .orElseGet(() -> Stream.of(caseFragment, whenFragment, endFragment)) - .collect(FragmentCollector.collect()) - .toFragmentAndParameters(Collectors.joining(" ")); //$NON-NLS-1$ + FragmentCollector fc = new FragmentCollector(); + fc.add(renderCase()); + fc.add(renderWhenConditions()); + renderElse().ifPresent(fc::add); + fc.add(renderEnd()); + return fc.toFragmentAndParameters(Collectors.joining(" ")); //$NON-NLS-1$ } private FragmentAndParameters renderCase() { From 026ebbc824f927138876b1cc95184b0b50d4e76c Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Fri, 15 Mar 2024 11:33:14 -0400 Subject: [PATCH 14/19] Invalid cast tests --- .../kotlin/animal/data/KCaseExpressionTest.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt b/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt index 48d223870..9071aa4df 100644 --- a/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt +++ b/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt @@ -947,4 +947,21 @@ class KCaseExpressionTest { select(case (id) { `else`("Fred") }) { from (animalData) } }.withMessage(Messages.getString("ERROR.40")) } + + @Test + fun testInvalidCastMissingAs() { + assertThatExceptionOfType(KInvalidSQLException::class.java).isThrownBy { + cast {} + }.withMessage(Messages.getString("ERROR.43")) + } + + @Test + fun testInvalidCastDoubleAs() { + assertThatExceptionOfType(KInvalidSQLException::class.java).isThrownBy { + cast { + "Fred" `as` "VARCHAR" + "Wilma" `as` "VARCHAR" + } + }.withMessage(Messages.getString("ERROR.43")) + } } From 2e53d74a0f5c9e34fdc94d4c8502286017508761 Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Fri, 15 Mar 2024 15:32:25 -0400 Subject: [PATCH 15/19] Infix functions for `as` with case expressions --- .../util/kotlin/elements/ColumnExtensions.kt | 6 ++++ .../kotlin/animal/data/KCaseExpressionTest.kt | 29 ++++++++++--------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/ColumnExtensions.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/ColumnExtensions.kt index 33235d298..fc4e2a104 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/ColumnExtensions.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/ColumnExtensions.kt @@ -17,11 +17,17 @@ package org.mybatis.dynamic.sql.util.kotlin.elements import org.mybatis.dynamic.sql.DerivedColumn import org.mybatis.dynamic.sql.SqlColumn +import org.mybatis.dynamic.sql.select.caseexpression.SearchedCaseModel +import org.mybatis.dynamic.sql.select.caseexpression.SimpleCaseModel infix fun DerivedColumn.`as`(alias: String): DerivedColumn = this.`as`(alias) infix fun SqlColumn.`as`(alias: String): SqlColumn = this.`as`(alias) +infix fun SearchedCaseModel.`as`(alias: String): SearchedCaseModel = this.`as`(alias) + +infix fun SimpleCaseModel.`as`(alias: String): SimpleCaseModel = this.`as`(alias) + /** * Adds a qualifier to a column for use with table aliases (typically in joins or sub queries). * This is as close to natural SQL syntax as we can get in Kotlin. Natural SQL would look like diff --git a/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt b/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt index 9071aa4df..b86db19af 100644 --- a/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt +++ b/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt @@ -28,6 +28,7 @@ import org.junit.jupiter.api.Test import org.mybatis.dynamic.sql.exception.InvalidSqlException import org.mybatis.dynamic.sql.util.Messages import org.mybatis.dynamic.sql.util.kotlin.KInvalidSQLException +import org.mybatis.dynamic.sql.util.kotlin.elements.`as` import org.mybatis.dynamic.sql.util.kotlin.elements.case import org.mybatis.dynamic.sql.util.kotlin.elements.cast import org.mybatis.dynamic.sql.util.kotlin.elements.isEqualTo @@ -63,7 +64,7 @@ class KCaseExpressionTest { then("Bat") } `else`("Not a Fox or a bat") - }.`as`("AnimalType") + } `as` "AnimalType" ) { from(animalData, "a") where { id.isIn(2, 3, 31, 32, 38, 39) } @@ -140,7 +141,7 @@ class KCaseExpressionTest { then(2) } `else`(3) - }.`as`("AnimalType") + } `as` "AnimalType" ) { from(animalData, "a") where { id.isIn(2, 3, 31, 32, 38, 39) } @@ -217,7 +218,7 @@ class KCaseExpressionTest { then(value("Bat")) } `else`(cast { value("Not a Fox or a bat") `as` "VARCHAR(30)" }) - }.`as`("AnimalType") + } `as` "AnimalType" ) { from(animalData, "a") where { id.isIn(2, 3, 31, 32, 38, 39) } @@ -297,7 +298,7 @@ class KCaseExpressionTest { or { animalName isEqualTo "Big brown bat" } then("Bat") } - }.`as`("AnimalType") + } `as` "AnimalType" ) { from(animalData, "a") where { id.isIn(2, 3, 31, 32, 38, 39) } @@ -379,7 +380,7 @@ class KCaseExpressionTest { then("Fred") } `else`("Not a Fox or a bat") - }.`as`("AnimalType") + } `as` "AnimalType" ) { from(animalData, "a") where { id.isIn(2, 3, 4, 31, 32, 38, 39) } @@ -458,7 +459,7 @@ class KCaseExpressionTest { case(animalName) { `when` (isEqualTo("Artic fox"), isEqualTo("Red fox")) { then("yes") } `else`("no") - }.`as`("IsAFox") + } `as` "IsAFox" ) { from(animalData) where { id.isIn(31, 32, 38, 39) } @@ -512,7 +513,7 @@ class KCaseExpressionTest { case(animalName) { `when` ("Artic fox", "Red fox") { then("yes") } `else`("no") - }.`as`("IsAFox") + } `as` "IsAFox" ) { from(animalData) where { id.isIn(31, 32, 38, 39) } @@ -566,7 +567,7 @@ class KCaseExpressionTest { case(animalName) { `when` (isEqualTo("Artic fox"), isEqualTo("Red fox")) { then(true) } `else`(false) - }.`as`("IsAFox") + } `as` "IsAFox" ) { from(animalData) where { id.isIn(31, 32, 38, 39) } @@ -620,7 +621,7 @@ class KCaseExpressionTest { case(animalName) { `when` ("Artic fox", "Red fox") { then(true) } `else`(false) - }.`as`("IsAFox") + } `as` "IsAFox" ) { from(animalData) where { id.isIn(31, 32, 38, 39) } @@ -673,7 +674,7 @@ class KCaseExpressionTest { animalName, case(animalName) { `when`(isEqualTo("Artic fox"), isEqualTo("Red fox")) { then("yes") } - }.`as`("IsAFox") + } `as` "IsAFox" ) { from(animalData) where { id.isIn(31, 32, 38, 39) } @@ -721,7 +722,7 @@ class KCaseExpressionTest { case(animalName) { `when`(isEqualTo("Artic fox"), isEqualTo("Red fox")) { then(cast { "It's a fox" `as` "VARCHAR(30)" })} `else`("It's not a fox") - }.`as`("IsAFox") + } `as` "IsAFox" ) { from(animalData) where { id.isIn(31, 32, 38, 39) } @@ -764,7 +765,7 @@ class KCaseExpressionTest { case(animalName) { `when`(isEqualTo("Artic fox"), isEqualTo("Red fox")) { then( 1L) } `else`(2L) - }.`as`("IsAFox") + } `as` "IsAFox" ) { from(animalData) where { id.isIn(31, 32, 38, 39) } @@ -807,7 +808,7 @@ class KCaseExpressionTest { case(animalName) { `when`(isEqualTo("Artic fox"), isEqualTo("Red fox")) { then( 1.1) } `else`(2.2) - }.`as`("IsAFox") + } `as` "IsAFox" ) { from(animalData) where { id.isIn(31, 32, 38, 39) } @@ -850,7 +851,7 @@ class KCaseExpressionTest { case(animalName) { `when`(isEqualTo("Artic fox"), isEqualTo("Red fox")) { then( 1.1) } `else`(cast { 2.2 `as` "DOUBLE" }) - }.`as`("IsAFox") + } `as` "IsAFox" ) { from(animalData) where { id.isIn(31, 32, 38, 39) } From 91d26dd4603ddd1aee5257455b7137874f4a200f Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Fri, 15 Mar 2024 16:09:29 -0400 Subject: [PATCH 16/19] Proper return value for case expressions --- .../mybatis/dynamic/sql/util/kotlin/elements/SqlElements.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 64298c3f5..367ad0712 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 @@ -101,7 +101,7 @@ fun or(receiver: GroupingCriteriaReceiver): AndOrCriteriaGroup = } // case expressions -fun case(dslCompleter: KSearchedCaseDSL.() -> Unit): BasicColumn = +fun case(dslCompleter: KSearchedCaseDSL.() -> Unit): SearchedCaseModel = KSearchedCaseDSL().apply(dslCompleter).run { SearchedCaseModel.Builder() .withWhenConditions(whenConditions) @@ -109,7 +109,7 @@ fun case(dslCompleter: KSearchedCaseDSL.() -> Unit): BasicColumn = .build() } -fun case(column: BindableColumn, dslCompleter: KSimpleCaseDSL.() -> Unit) : BasicColumn = +fun case(column: BindableColumn, dslCompleter: KSimpleCaseDSL.() -> Unit) : SimpleCaseModel = KSimpleCaseDSL().apply(dslCompleter).run { SimpleCaseModel.Builder() .withColumn(column) From ecf3d5655bd81c44f6572d45780e73a65928004a Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Fri, 15 Mar 2024 16:09:48 -0400 Subject: [PATCH 17/19] Documentation --- CHANGELOG.md | 23 +- src/site/markdown/docs/caseExpressions.md | 196 ++++++++++++++++ src/site/markdown/docs/exceptions.md | 2 +- .../markdown/docs/kotlinCaseExpressions.md | 216 ++++++++++++++++++ src/site/site.xml | 2 + 5 files changed, 437 insertions(+), 2 deletions(-) create mode 100644 src/site/markdown/docs/caseExpressions.md create mode 100644 src/site/markdown/docs/kotlinCaseExpressions.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f8b8d774..3ae4ea2ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,31 @@ This log will detail notable changes to MyBatis Dynamic SQL. Full details are av ## Release 1.5.1 - Unreleased -This is a minor release with a few small enhancements. +This is a minor release with several enhancements. GitHub milestone: [https://github.com/mybatis/mybatis-dynamic-sql/milestone/13](https://github.com/mybatis/mybatis-dynamic-sql/milestone/13) +### Case Expressions and Cast Function +We've added support for CASE expressions to the library. Both simple and searched case expressions are supported. +This is a fairly extensive enhancement as case expressions are quite complex, but we were able to reuse many of the +building blocks from the WHERE and HAVING support already in the library. You should be able to build CASE expressions +with relatively few limitations. + +It is also common to use a CAST function with CASE expressions, so we have added CAST as a built-in function +in the library. + +The DSL for both Java and Kotlin has been updated to fully support CASE expressions in the same idiomatic forms +as other parts of the library. + +We've tested this extensively and the code is, of course, 100% covered by test code. But it is possible that we've not +covered every possible scenario. Please let us know if you find issues. + +Full documentation is available here: +- [Java Case Expression DSL Documentation](caseExpressions.md) +- [Kotlin Case Expression DSL Documentation](kotlinCaseExpressions.md) + +The pull request for this change is + ### Parameter Values in Joins We've added the ability to specify typed values in equi-joins. This allows you to avoid the use of constants, and it is diff --git a/src/site/markdown/docs/caseExpressions.md b/src/site/markdown/docs/caseExpressions.md new file mode 100644 index 000000000..c85fff7fa --- /dev/null +++ b/src/site/markdown/docs/caseExpressions.md @@ -0,0 +1,196 @@ +# Case Expressions in the Java DSL + +Support for case expressions was added in version 1.5.1. For information about case expressions in the Kotlin DSL, see +the [Kotlin Case Expressions](kotlinCaseExpressions.md) page. + +## Case Statements in SQL +The library supports different types of case expressions - a "simple" case expression, and a "searched" case +expressions. + +A simple case expression checks the values of a single column. It looks like this: + +```sql +select case id + when 1, 2, 3 then true + else false + end as small_id +from foo +``` + +Some databases also support simple comparisons on simple case expressions, which look lke this: + +```sql +select case total_length + when < 10 then 'small' + when > 20 then 'large' + else 'medium' + end as tshirt_size +from foo +``` + +A searched case expression allows arbitrary logic, and it can check the values of multiple columns. It looks like this: + +```sql +select case + when animal_name = 'Small brown bat' or animal_name = 'Large brown bat' then 'Bat' + when animal_name = 'Artic fox' or animal_name = 'Red fox' then 'Fox' + else 'Other' + end as animal_type +from foo +``` + +## Bind Variables and Casting + +The library will always render the "when" part of a case expression using bind variables. Rendering of the "then" and +"else" parts of a case expression may or may not use bind variables depending on how you write the query. In general, +the library will render "then" and "else" as constants - meaning not using bind variables. If you wish to use bind +variables for these parts of a case expressions, then you can use the `value` function to turn a constant into a +bind variable. We will show examples of the different renderings in the following sections. + +If you choose to use bind variables for all "then" and "else" values, it is highly likely that the database will +require you to specify an expected datatype by using a `cast` function. + +Even for "then" and "else" sections that are rendered with constants, you may still desire to use a `cast` in some +cases. For example, if you specify Strings for all "then" and "else" values, the database will likely return all +values as datatype CHAR with the length of the longest constant string. Typically, we would prefer the use of VARCHAR, +so we don't have to strip trailing blanks from the results. This is a good use for a `cast` with a constant. +Similarly, Java float constants are often interpreted by databases as BigDecimal. You can use a `cast` to have them +returned as floats. + +Note: in the following sections we will use `?` to show a bind variable, but the actual rendered SQL will be different +because bind variables will be rendered appropriately for the execution engine you are using (either MyBatis or Spring). + +Also note: in Java, `case` and `else` are reserved words - meaning we cannot use them as method names. For this reason, +the library uses `case_` and `else_` respectively as method names. + +Full examples for case expressions are in the test code for the library here: +https://github.com/mybatis/mybatis-dynamic-sql/blob/master/src/test/java/examples/animal/data/CaseExpressionTest.java + +## Java DSL for Simple Case Statements with Simple Values + +A simple case expression can be coded like the following in the Java DSL: + +```java +select(case_(id) + .when(1, 2, 3).then(true) + .else_(false) + .end().as("small_id")) +.from(foo) +``` + +A statement written this way will render as follows: + +```sql +select case id when ?, ?, ? then true else false end as small_id from foo +``` + +Note that the "then" and "else" parts are NOT rendered with bind variables. If you with to use bind variables, then +you can write the query as follows: + +```java +select(case_(id) + .when(1, 2, 3).then(value(true)) + .else_(value(false)) + .end().as("small_id")) +.from(foo) +``` + +In this case, we are using the `value` function to denote a bind variable. The SQL will now be rendered as follows: + +```sql +select case id when ?, ?, ? then ? else ? end as small_id from foo +``` + +*Important*: Be aware that your database may throw an exception for SQL like this because the database cannot determine +the datatype of the resulting column. If that happens, you will need to cast one or more of the variables to the +expected data type. Here's an example of using the `cast` function: + +```java +select(case_(id) + .when(1, 2, 3).then(value(true)) + .else_(cast(value(false)).as("BOOLEAN)")) + .end().as("small_id")) +.from(foo) +``` + +In this case, the SQL will render as follows: + +```sql +select case id when ?, ?, ? then ? else cast(? as BOOLEAN) end as small_id from foo +``` + +In our testing, casting a single bound value is enough to inform the database of your expected datatype, but +you should perform your own testing. + +## Java DSL for Simple Case Statements with Conditions + +A simple case expression can be coded like the following in the Java DSL: + +```java +select(case_(total_length) + .when(isLessThan(10)).then_("small") + .when(isGreaterThan(20)).then_("large") + .else_("medium") + .end().as("tshirt_size")) +.from(foo) +``` + +A statement written this way will render as follows: + +```sql +select case total_length when < ? then 'small' when > ? then 'large' else 'medium' end as tshirt_size from foo +``` + +Note that the "then" and "else" parts are NOT rendered with bind variables. If you with to use bind variables, then +you can use the `value` function as shown above. + +A query like this could be a good place to use casting with constants. Most databases will return the calculated +"tshirt_size" column as CHAR(6) - so the "small" and "large" values will have a trailing blank. If you wish to use +VARCHAR, you can use the `cast` function as follows: + +```java +select(case_(total_length) + .when(isLessThan(10)).then_("small") + .when(isGreaterThan(20)).then_("large") + .else_(cast("medium").as("VARCHAR(6)")) + .end().as("tshirt_size")) +.from(foo) +``` + +In this case, we are using the `cast` function to specify the datatype of a constant. The SQL will now be rendered as +follows (without the line breaks): + +```sql +select case total_length + when < ? then 'small' when > ? then 'large' + else cast('medium' as VARCHAR(6)) end as tshirt_size from foo +``` + +## Java DSL for Searched Case Statements + +A searched case statement is written as follows: + +```java +select(case_() + .when(animalName, isEqualTo("Small brown bat")).or(animalName, isEqualTo("Large brown bat")).then("Bat") + .when(animalName, isEqualTo("Artic fox")).or(animalName, isEqualTo("Red fox")).then("Fox") + .else_("Other") + .end().as("animal_type")) +.from(foo) +``` + +The full syntax of "where" and "having" clauses is supported in the "when" clause - but that may or may not be supported +by your database. Testing is crucial. In addition, the library does not support conditions that don't render in a case +statement - so avoid the use of conditions like "isEqualToWhenPresent", etc. + +The rendered SQL will be as follows (without the line breaks): +```sql +select case + when animal_name = ? or animal_name = ? then 'Bat' + when animal_name = ? or animal_name = ? then 'Fox' + else 'Other' + end as animal_type +from foo +``` + +The use of the `value` function to support bind variables, and the use of casting, is the same is shown above. diff --git a/src/site/markdown/docs/exceptions.md b/src/site/markdown/docs/exceptions.md index c589ad345..8cf4263b3 100644 --- a/src/site/markdown/docs/exceptions.md +++ b/src/site/markdown/docs/exceptions.md @@ -35,7 +35,7 @@ rows in a table. For example, all rows could be deleted. As of version 1.4.1, th through either global configuration, or by configuring individual statements to allow for where clauses to be dropped. The important idea is that there are legitimate cases when it is reasonable to allow a where clause to not render, but -the decision to allow that should be very intentional. See the "Configuration of the Library" page for further details. +the decision to allow that should be very intentional. See the [Configuration of the Library](configuration.md) page for further details. The exception will only be thrown if a where clause is coded but fails to render. If you do not code a where clause in a statement, then we assume that you intend for all rows to be affected. diff --git a/src/site/markdown/docs/kotlinCaseExpressions.md b/src/site/markdown/docs/kotlinCaseExpressions.md new file mode 100644 index 000000000..1ac349aa5 --- /dev/null +++ b/src/site/markdown/docs/kotlinCaseExpressions.md @@ -0,0 +1,216 @@ +# Case Expressions in the Kotlin DSL + +Support for case expressions was added in version 1.5.1. For information about case expressions in the Java DSL, see +the [Java Case Expressions](caseExpressions.md) page. + +## Case Statements in SQL +The library supports different types of case expressions - a "simple" case expression, and a "searched" case +expressions. + +A simple case expression checks the values of a single column. It looks like this: + +```sql +select case id + when 1, 2, 3 then true + else false + end as small_id +from foo +``` + +Some databases also support simple comparisons on simple case expressions, which look lke this: + +```sql +select case total_length + when < 10 then 'small' + when > 20 then 'large' + else 'medium' + end as tshirt_size +from foo +``` + +A searched case expression allows arbitrary logic, and it can check the values of multiple columns. It looks like this: + +```sql +select case + when animal_name = 'Small brown bat' or animal_name = 'Large brown bat' then 'Bat' + when animal_name = 'Artic fox' or animal_name = 'Red fox' then 'Fox' + else 'Other' + end as animal_type +from foo +``` + +## Bind Variables and Casting + +The library will always render the "when" part of a case expression using bind variables. Rendering of the "then" and +"else" parts of a case expression may or may not use bind variables depending on how you write the query. In general, +the library will render "then" and "else" as constants - meaning not using bind variables. If you wish to use bind +variables for these parts of a case expressions, then you can use the `value` function to turn a constant into a +bind variable. We will show examples of the different renderings in the following sections. + +If you choose to use bind variables for all "then" and "else" values, it is highly likely that the database will +require you to specify an expected datatype by using a `cast` function. + +Even for "then" and "else" sections that are rendered with constants, you may still desire to use a `cast` in some +cases. For example, if you specify Strings for all "then" and "else" values, the database will likely return all +values as datatype CHAR with the length of the longest constant string. Typically, we would prefer the use of VARCHAR, +so we don't have to strip trailing blanks from the results. This is a good use for a `cast` with a constant. +Similarly, Kotlin float constants are often interpreted by databases as BigDecimal. You can use a `cast` to have them +returned as floats. + +Note: in the following sections we will use `?` to show a bind variable, but the actual rendered SQL will be different +because bind variables will be rendered appropriately for the execution engine you are using (either MyBatis or Spring). + +Also note: in Kotlin, `when` and `else` are reserved words - meaning we cannot use them as method names. For this +reason, the library uses `` `when` `` and `` `else` `` respectively as method names. + +Full examples for case expressions are in the test code for the library here: +https://github.com/mybatis/mybatis-dynamic-sql/blob/master/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt + +## Kotlin DSL for Simple Case Statements with Simple Values + +A simple case expression can be coded like the following in the Kotlin DSL: + +```kotlin +select(case(id) { + `when`(1, 2, 3) { then(true) } + `else`(false) + } `as` "small_id" +) { + from(foo) +} +``` + +A statement written this way will render as follows: + +```sql +select case id when ?, ?, ? then true else false end as small_id from foo +``` + +Note that the "then" and "else" parts are NOT rendered with bind variables. If you with to use bind variables, then +you can write the query as follows: + +```kotlin +select(case(id) { + `when`(1, 2, 3) { then(value(true)) } + `else`(value(false)) + } `as` "small_id" +) { + from(foo) +} +``` + +In this case, we are using the `value` function to denote a bind variable. The SQL will now be rendered as follows: + +```sql +select case id when ?, ?, ? then ? else ? end as small_id from foo +``` + +*Important*: Be aware that your database may throw an exception for SQL like this because the database cannot determine +the datatype of the resulting column. If that happens, you will need to cast one or more of the variables to the +expected data type. Here's an example of using the `cast` function: + +```kotlin +select(case(id) { + `when`(1, 2, 3) { then(value(true)) } + `else`(cast { value(false) `as` "BOOLEAN" }) + } `as` "small_id" +) { + from(foo) +} +``` + +In this case, the SQL will render as follows: + +```sql +select case id when ?, ?, ? then ? else cast(? as BOOLEAN) end as small_id from foo +``` + +In our testing, casting a single bound value is enough to inform the database of your expected datatype, but +you should perform your own testing. + +## Kotlin DSL for Simple Case Statements with Conditions + +A simple case expression can be coded like the following in the Kotlin DSL: + +```kotlin +select(case(total_length) { + `when`(isLessThan(10)) { then("small") } + `when`(isGreaterThan(20)) { then("large") } + `else`("medium") + } `as` "tshirt_size" +) { + from(foo) +} +``` + +A statement written this way will render as follows: + +```sql +select case total_length when < ? then 'small' when > ? then 'large' else 'medium' end as tshirt_size from foo +``` + +Note that the "then" and "else" parts are NOT rendered with bind variables. If you with to use bind variables, then +you can use the `value` function as shown above. + +A query like this could be a good place to use casting with constants. Most databases will return the calculated +"tshirt_size" column as CHAR(6) - so the "small" and "large" values will have a trailing blank. If you wish to use +VARCHAR, you can use the `cast` function as follows: + +```kotlin +select(case(total_length) { + `when`(isLessThan(10)) { then("small") } + `when`(isGreaterThan(20)) { then("large") } + `else`(cast { "medium" `as` "VARCHAR(6)" }) + } `as` "tshirt_size" +) { + from(foo) +} +``` + +In this case, we are using the `cast` function to specify the datatype of a constant. The SQL will now be rendered as +follows (without the line breaks): + +```sql +select case total_length + when < ? then 'small' when > ? then 'large' + else cast('medium' as VARCHAR(6)) end as tshirt_size from foo +``` + +## Kotlin DSL for Searched Case Statements + +A searched case statement is written as follows: + +```kotlin +select(case { + `when` { + animalName isEqualTo "Small brown bat" + or { animalName isEqualTo "Large brown bat"} + then("Bat") + } + `when` { + animalName isEqualTo "Artic fox" + or { animalName isEqualTo "Red fox"} + then("Fox") + } + `else`("Other") + } `as` "animal_type" +) { + from(foo) +} +``` + +The full syntax of "where" and "having" clauses is supported in the "when" clause - but that may or may not be supported +by your database. Testing is crucial. In addition, the library does not support conditions that don't render in a case +statement - so avoid the use of conditions like "isEqualToWhenPresent", etc. + +The rendered SQL will be as follows (without the line breaks): +```sql +select case + when animal_name = ? or animal_name = ? then 'Bat' + when animal_name = ? or animal_name = ? then 'Fox' + else 'Other' + end as animal_type +from foo +``` + +The use of the `value` function to support bind variables, and the use of casting, is the same is shown above. diff --git a/src/site/site.xml b/src/site/site.xml index 772666ae8..e22bf264d 100644 --- a/src/site/site.xml +++ b/src/site/site.xml @@ -44,6 +44,7 @@ + @@ -55,6 +56,7 @@ + From 431bc10403dd61762a889ffd4f34830eb13a11a7 Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Fri, 15 Mar 2024 16:11:32 -0400 Subject: [PATCH 18/19] Documentation --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ae4ea2ae..b2d0d9675 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ The DSL for both Java and Kotlin has been updated to fully support CASE expressi as other parts of the library. We've tested this extensively and the code is, of course, 100% covered by test code. But it is possible that we've not -covered every possible scenario. Please let us know if you find issues. +covered every scenario. Please let us know if you find issues. Full documentation is available here: - [Java Case Expression DSL Documentation](caseExpressions.md) From f1a3e0027697cd2d5870901bd5ff552f6c516abc Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Fri, 15 Mar 2024 16:24:25 -0400 Subject: [PATCH 19/19] Documentation --- CHANGELOG.md | 2 +- src/site/markdown/docs/caseExpressions.md | 12 ++++++------ src/site/markdown/docs/kotlinCaseExpressions.md | 10 +++++----- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2d0d9675..649a9d565 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,7 @@ Full documentation is available here: - [Java Case Expression DSL Documentation](caseExpressions.md) - [Kotlin Case Expression DSL Documentation](kotlinCaseExpressions.md) -The pull request for this change is +The pull request for this change is ([#761](https://github.com/mybatis/mybatis-dynamic-sql/pull/761)) ### Parameter Values in Joins diff --git a/src/site/markdown/docs/caseExpressions.md b/src/site/markdown/docs/caseExpressions.md index c85fff7fa..fe076811b 100644 --- a/src/site/markdown/docs/caseExpressions.md +++ b/src/site/markdown/docs/caseExpressions.md @@ -11,7 +11,7 @@ A simple case expression checks the values of a single column. It looks like thi ```sql select case id - when 1, 2, 3 then true + when 1, 2, 3 then true else false end as small_id from foo @@ -32,8 +32,8 @@ A searched case expression allows arbitrary logic, and it can check the values o ```sql select case - when animal_name = 'Small brown bat' or animal_name = 'Large brown bat' then 'Bat' - when animal_name = 'Artic fox' or animal_name = 'Red fox' then 'Fox' + when animal_name = 'Small brown bat' or animal_name = 'Large brown bat' then 'Bat' + when animal_name = 'Artic fox' or animal_name = 'Red fox' then 'Fox' else 'Other' end as animal_type from foo @@ -44,7 +44,7 @@ from foo The library will always render the "when" part of a case expression using bind variables. Rendering of the "then" and "else" parts of a case expression may or may not use bind variables depending on how you write the query. In general, the library will render "then" and "else" as constants - meaning not using bind variables. If you wish to use bind -variables for these parts of a case expressions, then you can use the `value` function to turn a constant into a +variables for these parts of a case expressions, then you can use the `value` function to turn a constant into a bind variable. We will show examples of the different renderings in the following sections. If you choose to use bind variables for all "then" and "else" values, it is highly likely that the database will @@ -186,8 +186,8 @@ statement - so avoid the use of conditions like "isEqualToWhenPresent", etc. The rendered SQL will be as follows (without the line breaks): ```sql select case - when animal_name = ? or animal_name = ? then 'Bat' - when animal_name = ? or animal_name = ? then 'Fox' + when animal_name = ? or animal_name = ? then 'Bat' + when animal_name = ? or animal_name = ? then 'Fox' else 'Other' end as animal_type from foo diff --git a/src/site/markdown/docs/kotlinCaseExpressions.md b/src/site/markdown/docs/kotlinCaseExpressions.md index 1ac349aa5..f34a30f18 100644 --- a/src/site/markdown/docs/kotlinCaseExpressions.md +++ b/src/site/markdown/docs/kotlinCaseExpressions.md @@ -11,7 +11,7 @@ A simple case expression checks the values of a single column. It looks like thi ```sql select case id - when 1, 2, 3 then true + when 1, 2, 3 then true else false end as small_id from foo @@ -32,8 +32,8 @@ A searched case expression allows arbitrary logic, and it can check the values o ```sql select case - when animal_name = 'Small brown bat' or animal_name = 'Large brown bat' then 'Bat' - when animal_name = 'Artic fox' or animal_name = 'Red fox' then 'Fox' + when animal_name = 'Small brown bat' or animal_name = 'Large brown bat' then 'Bat' + when animal_name = 'Artic fox' or animal_name = 'Red fox' then 'Fox' else 'Other' end as animal_type from foo @@ -206,8 +206,8 @@ statement - so avoid the use of conditions like "isEqualToWhenPresent", etc. The rendered SQL will be as follows (without the line breaks): ```sql select case - when animal_name = ? or animal_name = ? then 'Bat' - when animal_name = ? or animal_name = ? then 'Fox' + when animal_name = ? or animal_name = ? then 'Bat' + when animal_name = ? or animal_name = ? then 'Fox' else 'Other' end as animal_type from foo