diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f8b8d774..649a9d565 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 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 ([#761](https://github.com/mybatis/mybatis-dynamic-sql/pull/761)) + ### 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/main/java/org/mybatis/dynamic/sql/SqlBuilder.java b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java index 0e2f372b3..a100e7bf7 100644 --- a/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java +++ b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java @@ -43,7 +43,10 @@ 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.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; @@ -442,6 +445,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); } @@ -517,6 +531,18 @@ 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(Double value) { + return cast(constant(value.toString())); + } + + 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...)} @@ -968,4 +994,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..ca38ed1da 100644 --- a/src/main/java/org/mybatis/dynamic/sql/StringConstant.java +++ b/src/main/java/org/mybatis/dynamic/sql/StringConstant.java @@ -42,7 +42,8 @@ public Optional alias() { @Override public FragmentAndParameters render(RenderingContext renderingContext) { - return FragmentAndParameters.fromFragment("'" + value + "'"); //$NON-NLS-1$ //$NON-NLS-2$ + 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/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/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/select/caseexpression/BasicWhenCondition.java b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/BasicWhenCondition.java new file mode 100644 index 000000000..8fe46e5fd --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/BasicWhenCondition.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 java.util.ArrayList; +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, BasicColumn 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..16af8f891 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/ConditionBasedWhenCondition.java @@ -0,0 +1,41 @@ +/* + * 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; + +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, BasicColumn 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/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 new file mode 100644 index 000000000..4e070b98d --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SearchedCaseDSL.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.caseexpression; + +import java.util.ArrayList; +import java.util.Arrays; +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; +import org.mybatis.dynamic.sql.SqlCriterion; +import org.mybatis.dynamic.sql.VisitableCondition; +import org.mybatis.dynamic.sql.common.AbstractBooleanExpressionDSL; + +public class SearchedCaseDSL implements ElseDSL { + private final List whenConditions = new ArrayList<>(); + private BasicColumn 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); + } + + @SuppressWarnings("java:S100") + @Override + public SearchedCaseEnder else_(BasicColumn column) { + elseValue = column; + return new SearchedCaseEnder(); + } + + public BasicColumn end() { + return new SearchedCaseModel.Builder() + .withElseValue(elseValue) + .withWhenConditions(whenConditions) + .build(); + } + + public class WhenDSL extends AbstractBooleanExpressionDSL implements ThenDSL { + private WhenDSL(SqlCriterion sqlCriterion) { + setInitialCriterion(sqlCriterion); + } + + @Override + public SearchedCaseDSL then(BasicColumn column) { + whenConditions.add(new SearchedCaseModel.SearchedWhenCondition(getInitialCriterion(), subCriteria, + column)); + return SearchedCaseDSL.this; + } + + @Override + protected WhenDSL getThis() { + return this; + } + } + + 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/caseexpression/SearchedCaseModel.java b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SearchedCaseModel.java new file mode 100644 index 000000000..50ca830ad --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SearchedCaseModel.java @@ -0,0 +1,110 @@ +/* + * 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.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; +import org.mybatis.dynamic.sql.util.Validator; + +public class SearchedCaseModel implements BasicColumn { + private final List whenConditions; + private final BasicColumn elseValue; + private final String alias; + + private SearchedCaseModel(Builder builder) { + whenConditions = builder.whenConditions; + alias = builder.alias; + elseValue = builder.elseValue; + Validator.assertNotEmpty(whenConditions, "ERROR.40"); //$NON-NLS-1$ + } + + 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 BasicColumn thenValue; + + public BasicColumn thenValue() { + return thenValue; + } + + public SearchedWhenCondition(SqlCriterion initialCriterion, List subCriteria, + BasicColumn thenValue) { + super(initialCriterion, subCriteria); + this.thenValue = Objects.requireNonNull(thenValue); + } + } + + public static class Builder { + private final List whenConditions = new ArrayList<>(); + private BasicColumn elseValue; + private String alias; + + public Builder withWhenConditions(List whenConditions) { + this.whenConditions.addAll(whenConditions); + return this; + } + + public Builder withElseValue(BasicColumn 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/caseexpression/SimpleCaseDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseDSL.java new file mode 100644 index 000000000..83e46473a --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseDSL.java @@ -0,0 +1,111 @@ +/* + * 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.Arrays; +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; + +public class SimpleCaseDSL implements ElseDSL.SimpleCaseEnder> { + private final BindableColumn column; + private final List> whenConditions = new ArrayList<>(); + private BasicColumn elseValue; + + private SimpleCaseDSL(BindableColumn column) { + this.column = Objects.requireNonNull(column); + } + + @SafeVarargs + public final ConditionBasedWhenFinisher when(VisitableCondition condition, + VisitableCondition... subsequentConditions) { + return when(condition, Arrays.asList(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") + @Override + public SimpleCaseEnder else_(BasicColumn column) { + elseValue = column; + return new SimpleCaseEnder(); + } + + public BasicColumn end() { + return new SimpleCaseModel.Builder() + .withColumn(column) + .withWhenConditions(whenConditions) + .withElseValue(elseValue) + .build(); + } + + public class ConditionBasedWhenFinisher implements ThenDSL> { + private final List> conditions = new ArrayList<>(); + + private ConditionBasedWhenFinisher(VisitableCondition condition, + List> subsequentConditions) { + conditions.add(condition); + conditions.addAll(subsequentConditions); + } + + @Override + public SimpleCaseDSL then(BasicColumn column) { + whenConditions.add(new ConditionBasedWhenCondition<>(conditions, column)); + return SimpleCaseDSL.this; + } + } + + public class BasicWhenFinisher implements ThenDSL> { + private final List values = new ArrayList<>(); + + private BasicWhenFinisher(T value, List subsequentValues) { + values.add(value); + values.addAll(subsequentValues); + } + + @Override + public SimpleCaseDSL then(BasicColumn column) { + whenConditions.add(new BasicWhenCondition<>(values, column)); + return SimpleCaseDSL.this; + } + } + + 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/caseexpression/SimpleCaseModel.java b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseModel.java new file mode 100644 index 000000000..4b71407ae --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseModel.java @@ -0,0 +1,107 @@ +/* + * 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.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.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; + private final List> whenConditions; + private final BasicColumn elseValue; + private final String alias; + + private SimpleCaseModel(Builder builder) { + column = Objects.requireNonNull(builder.column); + whenConditions = builder.whenConditions; + elseValue = builder.elseValue; + alias = builder.alias; + Validator.assertNotEmpty(whenConditions, "ERROR.40"); //$NON-NLS-1$ + } + + 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 Builder { + private BindableColumn column; + private final List> whenConditions = new ArrayList<>(); + private BasicColumn 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(BasicColumn 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/caseexpression/SimpleCaseWhenCondition.java b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseWhenCondition.java new file mode 100644 index 000000000..5466f2f3f --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/caseexpression/SimpleCaseWhenCondition.java @@ -0,0 +1,34 @@ +/* + * 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; + +import org.mybatis.dynamic.sql.BasicColumn; + +public abstract class SimpleCaseWhenCondition { + private final BasicColumn thenValue; + + protected SimpleCaseWhenCondition(BasicColumn thenValue) { + this.thenValue = Objects.requireNonNull(thenValue); + } + + public BasicColumn 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/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 new file mode 100644 index 000000000..050a87204 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/SearchedCaseRenderer.java @@ -0,0 +1,88 @@ +/* + * 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.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; +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() { + 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() { + return FragmentAndParameters.fromFragment("case"); //$NON-NLS-1$ + } + + private FragmentAndParameters renderWhenConditions() { + return searchedCaseModel.whenConditions().map(this::renderWhenCondition) + .collect(FragmentCollector.collect()) + .toFragmentAndParameters(Collectors.joining(" ")); //$NON-NLS-1$ + } + + 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) { + 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 whenCondition.thenValue().render(renderingContext).mapFragment(f -> "then " + f); + } + + private Optional renderElse() { + return searchedCaseModel.elseValue().map(this::renderElse); + } + + private FragmentAndParameters renderElse(BasicColumn elseValue) { + return elseValue.render(renderingContext).mapFragment(f -> "else " + f); //$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/SearchedCaseWhenConditionRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/SearchedCaseWhenConditionRenderer.java new file mode 100644 index 000000000..cb487b2d4 --- /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.caseexpression.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..2639d0b53 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseRenderer.java @@ -0,0 +1,94 @@ +/* + * 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.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; +import org.mybatis.dynamic.sql.util.FragmentAndParameters; +import org.mybatis.dynamic.sql.util.FragmentCollector; + +public class SimpleCaseRenderer { + private final SimpleCaseModel simpleCaseModel; + private final RenderingContext renderingContext; + private final SimpleCaseWhenConditionRenderer whenConditionRenderer; + + public SimpleCaseRenderer(SimpleCaseModel simpleCaseModel, RenderingContext renderingContext) { + this.simpleCaseModel = Objects.requireNonNull(simpleCaseModel); + this.renderingContext = Objects.requireNonNull(renderingContext); + whenConditionRenderer = new SimpleCaseWhenConditionRenderer<>(renderingContext, simpleCaseModel.column()); + } + + public FragmentAndParameters render() { + 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() { + return simpleCaseModel.column().render(renderingContext) + .mapFragment(f -> "case " + f); //$NON-NLS-1$ + } + + private FragmentAndParameters renderWhenConditions() { + return simpleCaseModel.whenConditions().map(this::renderWhenCondition) + .collect(FragmentCollector.collect()) + .toFragmentAndParameters(Collectors.joining(" ")); //$NON-NLS-1$ + } + + 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() { + return FragmentAndParameters.fromFragment("when"); //$NON-NLS-1$ + } + + private FragmentAndParameters renderConditions(SimpleCaseWhenCondition whenCondition) { + return whenCondition.accept(whenConditionRenderer); + } + + private FragmentAndParameters renderThen(SimpleCaseWhenCondition whenCondition) { + return whenCondition.thenValue().render(renderingContext) + .mapFragment(f -> "then " + f); //$NON-NLS-1$ + } + + private Optional renderElse() { + return simpleCaseModel.elseValue().map(this::renderElse); + } + + private FragmentAndParameters renderElse(BasicColumn elseValue) { + return elseValue.render(renderingContext).mapFragment(f -> "else " + f); //$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/SimpleCaseWhenConditionRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/SimpleCaseWhenConditionRenderer.java new file mode 100644 index 000000000..4bb8d8aff --- /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.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; +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$ + } + + @Override + public FragmentAndParameters visit(BasicWhenCondition whenCondition) { + return whenCondition.conditions().map(this::renderBasicValue) + .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 renderBasicValue(T value) { + RenderedParameterInfo rpi = renderingContext.calculateParameterInfo(column); + return FragmentAndParameters.withFragment(rpi.renderedPlaceHolder()) + .withParameter(rpi.parameterMapKey(), value) + .build(); + } +} 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(); } 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..e3f244f8e --- /dev/null +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/CaseDSLs.kt @@ -0,0 +1,149 @@ +/* + * 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.VisitableCondition +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 + +class KSearchedCaseDSL : KElseDSL { + internal var elseValue: BasicColumn? = 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)) + } + + override fun `else`(column: BasicColumn) { + this.elseValue = column + } +} + +class SearchedCaseCriteriaCollector : GroupingCriteriaCollector(), 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 + } +} + +class KSimpleCaseDSL : KElseDSL { + internal var elseValue: BasicColumn? = null + private set(value) { + assertNull(field, "ERROR.42") //$NON-NLS-1$ + field = value + } + internal val whenConditions = mutableListOf>() + + 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, thenValue)) + } + + fun `when`(firstValue: T, vararg subsequentValues: T, completer: SimpleCaseThenGatherer.() -> Unit) = + SimpleCaseThenGatherer().apply(completer).run { + val allConditions = buildList { + add(firstValue) + addAll(subsequentValues) + } + + whenConditions.add(BasicWhenCondition(allConditions, thenValue)) + } + + override fun `else`(column: BasicColumn) { + this.elseValue = column + } +} + +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) { + then(stringConstant(value)) + } + + fun then(value: Boolean) { + then(constant(value.toString())) + } + + fun then(value: Int) { + then(constant(value.toString())) + } + + 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/CastDSL.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/CastDSL.kt new file mode 100644 index 000000000..cebf26ee9 --- /dev/null +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/elements/CastDSL.kt @@ -0,0 +1,41 @@ +/* + * 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 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/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/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..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 @@ -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.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 @@ -34,6 +36,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 @@ -46,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 @@ -96,6 +100,24 @@ fun or(receiver: GroupingCriteriaReceiver): AndOrCriteriaGroup = .build() } +// case expressions +fun case(dslCompleter: KSearchedCaseDSL.() -> Unit): SearchedCaseModel = + KSearchedCaseDSL().apply(dslCompleter).run { + SearchedCaseModel.Builder() + .withWhenConditions(whenConditions) + .withElseValue(elseValue) + .build() + } + +fun case(column: BindableColumn, dslCompleter: KSimpleCaseDSL.() -> Unit) : SimpleCaseModel = + KSimpleCaseDSL().apply(dslCompleter).run { + SimpleCaseModel.Builder() + .withColumn(column) + .withWhenConditions(whenConditions) + .withElseValue(elseValue) + .build() + } + // aggregate support fun count(): CountAll = SqlBuilder.count() @@ -145,6 +167,9 @@ fun subtract( vararg subsequentColumns: BasicColumn ): Subtract = Subtract.of(firstColumn, secondColumn, subsequentColumns.asList()) +fun cast(receiver: CastDSL.() -> Unit): Cast = + invalidIfNull(CastDSL().apply(receiver).cast, "ERROR.43") + fun concat( firstColumn: BindableColumn, vararg subsequentColumns: BasicColumn 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..e7b125583 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,9 @@ 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 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 +ERROR.43=A Kotlin cast expression must have one, and only one, `as` element INTERNAL.ERROR=Internal Error {0} diff --git a/src/site/markdown/docs/caseExpressions.md b/src/site/markdown/docs/caseExpressions.md new file mode 100644 index 000000000..fe076811b --- /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..f34a30f18 --- /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 @@ + 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..322d9504b --- /dev/null +++ b/src/test/java/examples/animal/data/CaseExpressionTest.java @@ -0,0 +1,752 @@ +/* + * 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.brainWeight; +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.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; +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.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; +import java.math.BigDecimal; +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.caseexpression.SearchedCaseDSL; +import org.mybatis.dynamic.sql.select.SelectModel; +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; + +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 testSearchedCaseWithStrings() { + 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("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) + .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 '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}," + + "#{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 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 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()) { + CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); + + 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)) + .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, 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) + .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 '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}," + + "#{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 testSimpleCassLessThan() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(animalName, 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) + .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(4); + } + } + + @Test + void testSimpleCaseWithStrings() { + 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("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 testSimpleCaseBasicWithStrings() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(animalName, 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()) { + CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(animalName, 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)); + } + } + + @Test + void testSimpleCaseWithBoundValues() { + 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(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()) { + CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(animalName, 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()) { + CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(animalName, case_(animalName) + .when(isEqualTo("Artic fox"), isEqualTo("Red fox")).then("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 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_() + .when(animalName, isEqualToWhenPresent((String) null)).then("Fred").end()) + .from(animalData) + .build(); + + assertThatExceptionOfType(InvalidSqlException.class) + .isThrownBy(() -> model.render(RenderingStrategies.MYBATIS3)) + .withMessage(Messages.getString("ERROR.39")); + } + + @Test + void testInvalidSimpleCaseNoConditionsRender() { + SelectModel model = select(case_(animalName) + .when(isEqualToWhenPresent((String) null)).then("Fred").end()) + .from(animalData) + .build(); + + assertThatExceptionOfType(InvalidSqlException.class) + .isThrownBy(() -> model.render(RenderingStrategies.MYBATIS3)) + .withMessage(Messages.getString("ERROR.39")); + } + + @Test + void testInvalidSearchedCaseNoWhenConditions() { + SearchedCaseDSL dsl = case_(); + + assertThatExceptionOfType(InvalidSqlException.class).isThrownBy(dsl::end) + .withMessage(Messages.getString("ERROR.40")); + } + + @Test + void testInvalidSimpleCaseNoWhenConditions() { + 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/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..b86db19af --- /dev/null +++ b/src/test/kotlin/examples/kotlin/animal/data/KCaseExpressionTest.kt @@ -0,0 +1,968 @@ +/* + * 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.`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 +import java.math.BigDecimal + +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 testSearchedCaseWithStrings() { + 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`("Not a Fox or a 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' " + + "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}," + + "#{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 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 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 -> + 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`("Not a Fox or a bat") + } `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 '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}," + + "#{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 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`("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 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 -> + 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) + ) + } + } + + @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 -> + 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 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 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 { + case(animalName) { + `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 { + 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 { `else`("Fred") }) { from(animalData) } + }.withMessage(Messages.getString("ERROR.40")) + } + + @Test + fun testInvalidSimpleMissingWhen() { + assertThatExceptionOfType(InvalidSqlException::class.java).isThrownBy { + 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")) + } +}