Skip to content

Commit f450687

Browse files
authored
Merge pull request #761 from jeffgbutler/case-expression
Add Support for CASE Expressions
2 parents 5d5b9b6 + f1a3e00 commit f450687

36 files changed

+3612
-39
lines changed

CHANGELOG.md

+22-1
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,31 @@ This log will detail notable changes to MyBatis Dynamic SQL. Full details are av
44

55
## Release 1.5.1 - Unreleased
66

7-
This is a minor release with a few small enhancements.
7+
This is a minor release with several enhancements.
88

99
GitHub milestone: [https://github.com/mybatis/mybatis-dynamic-sql/milestone/13](https://github.com/mybatis/mybatis-dynamic-sql/milestone/13)
1010

11+
### Case Expressions and Cast Function
12+
We've added support for CASE expressions to the library. Both simple and searched case expressions are supported.
13+
This is a fairly extensive enhancement as case expressions are quite complex, but we were able to reuse many of the
14+
building blocks from the WHERE and HAVING support already in the library. You should be able to build CASE expressions
15+
with relatively few limitations.
16+
17+
It is also common to use a CAST function with CASE expressions, so we have added CAST as a built-in function
18+
in the library.
19+
20+
The DSL for both Java and Kotlin has been updated to fully support CASE expressions in the same idiomatic forms
21+
as other parts of the library.
22+
23+
We've tested this extensively and the code is, of course, 100% covered by test code. But it is possible that we've not
24+
covered every scenario. Please let us know if you find issues.
25+
26+
Full documentation is available here:
27+
- [Java Case Expression DSL Documentation](caseExpressions.md)
28+
- [Kotlin Case Expression DSL Documentation](kotlinCaseExpressions.md)
29+
30+
The pull request for this change is ([#761](https://github.com/mybatis/mybatis-dynamic-sql/pull/761))
31+
1132
### Parameter Values in Joins
1233

1334
We've added the ability to specify typed values in equi-joins. This allows you to avoid the use of constants, and it is

src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java

+41
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,10 @@
4343
import org.mybatis.dynamic.sql.select.aggregate.Max;
4444
import org.mybatis.dynamic.sql.select.aggregate.Min;
4545
import org.mybatis.dynamic.sql.select.aggregate.Sum;
46+
import org.mybatis.dynamic.sql.select.caseexpression.SearchedCaseDSL;
47+
import org.mybatis.dynamic.sql.select.caseexpression.SimpleCaseDSL;
4648
import org.mybatis.dynamic.sql.select.function.Add;
49+
import org.mybatis.dynamic.sql.select.function.Cast;
4750
import org.mybatis.dynamic.sql.select.function.Concat;
4851
import org.mybatis.dynamic.sql.select.function.Concatenate;
4952
import org.mybatis.dynamic.sql.select.function.Divide;
@@ -442,6 +445,17 @@ static <T> JoinCriterion<T> on(BindableColumn<T> joinColumn, JoinCondition<T> jo
442445
.build();
443446
}
444447

448+
// case expressions
449+
@SuppressWarnings("java:S100")
450+
static <T> SimpleCaseDSL<T> case_(BindableColumn<T> column) {
451+
return SimpleCaseDSL.simpleCase(column);
452+
}
453+
454+
@SuppressWarnings("java:S100")
455+
static SearchedCaseDSL case_() {
456+
return SearchedCaseDSL.searchedCase();
457+
}
458+
445459
static <T> EqualTo<T> equalTo(BindableColumn<T> column) {
446460
return new EqualTo<>(column);
447461
}
@@ -517,6 +531,18 @@ static <T> Subtract<T> subtract(BindableColumn<T> firstColumn, BasicColumn secon
517531
return Subtract.of(firstColumn, secondColumn, subsequentColumns);
518532
}
519533

534+
static CastFinisher cast(String value) {
535+
return cast(stringConstant(value));
536+
}
537+
538+
static CastFinisher cast(Double value) {
539+
return cast(constant(value.toString()));
540+
}
541+
542+
static CastFinisher cast(BasicColumn column) {
543+
return new CastFinisher(column);
544+
}
545+
520546
/**
521547
* Concatenate function that renders as "(x || y || z)". This will not work on some
522548
* databases like MySql. In that case, use {@link SqlBuilder#concat(BindableColumn, BasicColumn...)}
@@ -968,4 +994,19 @@ public <T> GeneralInsertDSL.SetClauseFinisher<T> set(SqlColumn<T> column) {
968994
.set(column);
969995
}
970996
}
997+
998+
class CastFinisher {
999+
private final BasicColumn column;
1000+
1001+
public CastFinisher(BasicColumn column) {
1002+
this.column = column;
1003+
}
1004+
1005+
public Cast as(String targetType) {
1006+
return new Cast.Builder()
1007+
.withColumn(column)
1008+
.withTargetType(targetType)
1009+
.build();
1010+
}
1011+
}
9711012
}

src/main/java/org/mybatis/dynamic/sql/StringConstant.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ public Optional<String> alias() {
4242

4343
@Override
4444
public FragmentAndParameters render(RenderingContext renderingContext) {
45-
return FragmentAndParameters.fromFragment("'" + value + "'"); //$NON-NLS-1$ //$NON-NLS-2$
45+
String escaped = value.replace("'", "''"); //$NON-NLS-1$ //$NON-NLS-2$
46+
return FragmentAndParameters.fromFragment("'" + escaped + "'"); //$NON-NLS-1$ //$NON-NLS-2$
4647
}
4748

4849
@Override

src/main/java/org/mybatis/dynamic/sql/common/AbstractBooleanExpressionDSL.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -144,9 +144,13 @@ private void addSubCriteria(String connector, List<AndOrCriteriaGroup> criteria)
144144
.build());
145145
}
146146

147+
protected void setInitialCriterion(SqlCriterion initialCriterion) {
148+
this.initialCriterion = initialCriterion;
149+
}
150+
147151
protected void setInitialCriterion(SqlCriterion initialCriterion, StatementType statementType) {
148152
Validator.assertTrue(this.initialCriterion == null, statementType.messageNumber());
149-
this.initialCriterion = initialCriterion;
153+
setInitialCriterion(initialCriterion);
150154
}
151155

152156
// may be null!

src/main/java/org/mybatis/dynamic/sql/select/aggregate/Sum.java

+6-5
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
import org.mybatis.dynamic.sql.select.function.AbstractUniTypeFunction;
2424
import org.mybatis.dynamic.sql.util.FragmentAndParameters;
2525
import org.mybatis.dynamic.sql.util.Validator;
26-
import org.mybatis.dynamic.sql.where.render.DefaultConditionVisitor;
26+
import org.mybatis.dynamic.sql.where.render.ColumnAndConditionRenderer;
2727

2828
public class Sum<T> extends AbstractUniTypeFunction<T, Sum<T>> {
2929
private final Function<RenderingContext, FragmentAndParameters> renderer;
@@ -38,12 +38,13 @@ private Sum(BindableColumn<T> column, VisitableCondition<T> condition) {
3838
renderer = rc -> {
3939
Validator.assertTrue(condition.shouldRender(rc), "ERROR.37", "sum"); //$NON-NLS-1$ //$NON-NLS-2$
4040

41-
DefaultConditionVisitor<T> visitor = new DefaultConditionVisitor.Builder<T>()
41+
return new ColumnAndConditionRenderer.Builder<T>()
4242
.withColumn(column)
43+
.withCondition(condition)
4344
.withRenderingContext(rc)
44-
.build();
45-
46-
return condition.accept(visitor).mapFragment(this::applyAggregate);
45+
.build()
46+
.render()
47+
.mapFragment(this::applyAggregate);
4748
};
4849
}
4950

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2016-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.mybatis.dynamic.sql.select.caseexpression;
17+
18+
import java.util.ArrayList;
19+
import java.util.List;
20+
import java.util.stream.Stream;
21+
22+
import org.mybatis.dynamic.sql.BasicColumn;
23+
24+
public class BasicWhenCondition<T> extends SimpleCaseWhenCondition<T> {
25+
private final List<T> conditions = new ArrayList<>();
26+
27+
public BasicWhenCondition(List<T> conditions, BasicColumn thenValue) {
28+
super(thenValue);
29+
this.conditions.addAll(conditions);
30+
}
31+
32+
public Stream<T> conditions() {
33+
return conditions.stream();
34+
}
35+
36+
@Override
37+
public <R> R accept(SimpleCaseWhenConditionVisitor<T, R> visitor) {
38+
return visitor.visit(this);
39+
}
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2016-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.mybatis.dynamic.sql.select.caseexpression;
17+
18+
import java.util.ArrayList;
19+
import java.util.List;
20+
import java.util.stream.Stream;
21+
22+
import org.mybatis.dynamic.sql.BasicColumn;
23+
import org.mybatis.dynamic.sql.VisitableCondition;
24+
25+
public class ConditionBasedWhenCondition<T> extends SimpleCaseWhenCondition<T> {
26+
private final List<VisitableCondition<T>> conditions = new ArrayList<>();
27+
28+
public ConditionBasedWhenCondition(List<VisitableCondition<T>> conditions, BasicColumn thenValue) {
29+
super(thenValue);
30+
this.conditions.addAll(conditions);
31+
}
32+
33+
public Stream<VisitableCondition<T>> conditions() {
34+
return conditions.stream();
35+
}
36+
37+
@Override
38+
public <R> R accept(SimpleCaseWhenConditionVisitor<T, R> visitor) {
39+
return visitor.visit(this);
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2016-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.mybatis.dynamic.sql.select.caseexpression;
17+
18+
import org.mybatis.dynamic.sql.BasicColumn;
19+
import org.mybatis.dynamic.sql.Constant;
20+
import org.mybatis.dynamic.sql.StringConstant;
21+
22+
public interface ElseDSL<T> {
23+
24+
@SuppressWarnings("java:S100")
25+
default T else_(String value) {
26+
return else_(StringConstant.of(value));
27+
}
28+
29+
@SuppressWarnings("java:S100")
30+
default T else_(Boolean value) {
31+
return else_(Constant.of(value.toString()));
32+
}
33+
34+
@SuppressWarnings("java:S100")
35+
default T else_(Integer value) {
36+
return else_(Constant.of(value.toString()));
37+
}
38+
39+
@SuppressWarnings("java:S100")
40+
default T else_(Long value) {
41+
return else_(Constant.of(value.toString()));
42+
}
43+
44+
@SuppressWarnings("java:S100")
45+
default T else_(Double value) {
46+
return else_(Constant.of(value.toString()));
47+
}
48+
49+
@SuppressWarnings("java:S100")
50+
T else_(BasicColumn column);
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Copyright 2016-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.mybatis.dynamic.sql.select.caseexpression;
17+
18+
import java.util.ArrayList;
19+
import java.util.Arrays;
20+
import java.util.List;
21+
22+
import org.mybatis.dynamic.sql.AndOrCriteriaGroup;
23+
import org.mybatis.dynamic.sql.BasicColumn;
24+
import org.mybatis.dynamic.sql.BindableColumn;
25+
import org.mybatis.dynamic.sql.ColumnAndConditionCriterion;
26+
import org.mybatis.dynamic.sql.CriteriaGroup;
27+
import org.mybatis.dynamic.sql.SqlCriterion;
28+
import org.mybatis.dynamic.sql.VisitableCondition;
29+
import org.mybatis.dynamic.sql.common.AbstractBooleanExpressionDSL;
30+
31+
public class SearchedCaseDSL implements ElseDSL<SearchedCaseDSL.SearchedCaseEnder> {
32+
private final List<SearchedCaseModel.SearchedWhenCondition> whenConditions = new ArrayList<>();
33+
private BasicColumn elseValue;
34+
35+
public <T> WhenDSL when(BindableColumn<T> column, VisitableCondition<T> condition,
36+
AndOrCriteriaGroup... subCriteria) {
37+
return when(column, condition, Arrays.asList(subCriteria));
38+
}
39+
40+
public <T> WhenDSL when(BindableColumn<T> column, VisitableCondition<T> condition,
41+
List<AndOrCriteriaGroup> subCriteria) {
42+
SqlCriterion sqlCriterion = ColumnAndConditionCriterion.withColumn(column)
43+
.withCondition(condition)
44+
.withSubCriteria(subCriteria)
45+
.build();
46+
47+
return initialize(sqlCriterion);
48+
}
49+
50+
public WhenDSL when(SqlCriterion initialCriterion, AndOrCriteriaGroup... subCriteria) {
51+
return when(initialCriterion, Arrays.asList(subCriteria));
52+
}
53+
54+
public WhenDSL when(SqlCriterion initialCriterion, List<AndOrCriteriaGroup> subCriteria) {
55+
SqlCriterion sqlCriterion = new CriteriaGroup.Builder()
56+
.withInitialCriterion(initialCriterion)
57+
.withSubCriteria(subCriteria)
58+
.build();
59+
60+
return initialize(sqlCriterion);
61+
}
62+
63+
private WhenDSL initialize(SqlCriterion sqlCriterion) {
64+
return new WhenDSL(sqlCriterion);
65+
}
66+
67+
@SuppressWarnings("java:S100")
68+
@Override
69+
public SearchedCaseEnder else_(BasicColumn column) {
70+
elseValue = column;
71+
return new SearchedCaseEnder();
72+
}
73+
74+
public BasicColumn end() {
75+
return new SearchedCaseModel.Builder()
76+
.withElseValue(elseValue)
77+
.withWhenConditions(whenConditions)
78+
.build();
79+
}
80+
81+
public class WhenDSL extends AbstractBooleanExpressionDSL<WhenDSL> implements ThenDSL<SearchedCaseDSL> {
82+
private WhenDSL(SqlCriterion sqlCriterion) {
83+
setInitialCriterion(sqlCriterion);
84+
}
85+
86+
@Override
87+
public SearchedCaseDSL then(BasicColumn column) {
88+
whenConditions.add(new SearchedCaseModel.SearchedWhenCondition(getInitialCriterion(), subCriteria,
89+
column));
90+
return SearchedCaseDSL.this;
91+
}
92+
93+
@Override
94+
protected WhenDSL getThis() {
95+
return this;
96+
}
97+
}
98+
99+
public class SearchedCaseEnder {
100+
public BasicColumn end() {
101+
return SearchedCaseDSL.this.end();
102+
}
103+
}
104+
105+
public static SearchedCaseDSL searchedCase() {
106+
return new SearchedCaseDSL();
107+
}
108+
}

0 commit comments

Comments
 (0)