Add support for CASE statement in select and order by clauses.

Original pull request #1844
This commit is contained in:
sven.rienstra
2024-07-29 10:47:43 +02:00
committed by Jens Schauder
parent 1a860bfd3f
commit b11b85aef9
9 changed files with 303 additions and 11 deletions

View File

@@ -0,0 +1,94 @@
package org.springframework.data.relational.core.sql;
import java.util.ArrayList;
import java.util.List;
import static java.util.stream.Collectors.joining;
/**
* Case with one or more conditions expression.
* <p>
* Results in a rendered condition:
* <pre>
* CASE
* WHEN condition1 THEN result1
* WHEN condition2 THEN result2
* ELSE result
* END
* </pre>
* </p>
*
* @author Sven Rienstra
* @since 3.4
*/
public class CaseExpression extends AbstractSegment implements Expression {
private final List<When> whenList;
private final Expression elseExpression;
private CaseExpression(List<When> whenList, Expression elseExpression) {
super(children(whenList, elseExpression));
this.whenList = whenList;
this.elseExpression = elseExpression;
}
/**
* Create CASE {@link Expression} with initial {@link When} condition.
* @param condition initial {@link When} condition
* @return the {@link CaseExpression}
*/
public static CaseExpression create(When condition) {
return new CaseExpression(List.of(condition), null);
}
/**
* Add additional {@link When} condition
* @param condition the {@link When} condition
* @return the {@link CaseExpression}
*/
public CaseExpression when(When condition) {
List<When> conditions = new ArrayList<>(this.whenList);
conditions.add(condition);
return new CaseExpression(conditions, elseExpression);
}
/**
* Add ELSE clause
* @param elseExpression the {@link Expression} else value
* @return the {@link CaseExpression}
*/
public CaseExpression elseExpression(Literal elseExpression) {
return new CaseExpression(whenList, elseExpression);
}
/**
* @return the {@link When} conditions
*/
public List<When> getWhenList() {
return whenList;
}
/**
* @return the ELSE {@link Literal} value
*/
public Expression getElseExpression() {
return elseExpression;
}
@Override
public String toString() {
return "CASE " + whenList.stream().map(When::toString).collect(joining(" ")) + (elseExpression != null ? " ELSE " + elseExpression : "") + " END";
}
private static Segment[] children(List<When> whenList, Expression elseExpression) {
List<Segment> segments = new ArrayList<>();
segments.addAll(whenList);
if (elseExpression != null) {
segments.add(elseExpression);
}
return segments.toArray(new Segment[segments.size()]);
}
}

View File

@@ -0,0 +1,53 @@
package org.springframework.data.relational.core.sql;
/**
* When segment for Case statement.
* <p>
* Results in a rendered condition: {@code WHEN <condition> THEN <value>}.
* </p>
*
* @author Sven Rienstra
* @since 3.4
*/
public class When extends AbstractSegment {
private final Condition condition;
private final Expression value;
private When(Condition condition, Expression value) {
super(condition, value);
this.condition = condition;
this.value = value;
}
/**
* Creates a new {@link When} given two {@link Expression} condition and {@link Literal} value.
*
* @param condition the condition {@link Expression}.
* @param value the {@link Literal} value.
* @return the {@link When}.
*/
public static When when(Condition condition, Expression value) {
return new When(condition, value);
}
/**
* @return the condition
*/
public Condition getCondition() {
return condition;
}
/**
* @return the value
*/
public Expression getValue() {
return value;
}
@Override
public String toString() {
return "WHEN " + condition + " THEN " + value;
}
}

View File

@@ -0,0 +1,59 @@
package org.springframework.data.relational.core.sql.render;
import org.springframework.data.relational.core.sql.CaseExpression;
import org.springframework.data.relational.core.sql.Literal;
import org.springframework.data.relational.core.sql.Visitable;
import org.springframework.data.relational.core.sql.When;
/**
* Renderer for {@link CaseExpression}.
*
* @author Sven Rienstra
* @since 3.4
*/
public class CaseExpressionVisitor extends TypedSingleConditionRenderSupport<CaseExpression> implements PartRenderer {
private final StringBuilder part = new StringBuilder();
CaseExpressionVisitor(RenderContext context) {
super(context);
}
@Override
Delegation leaveNested(Visitable segment) {
if (hasDelegatedRendering()) {
CharSequence renderedPart = consumeRenderedPart();
if (segment instanceof When) {
part.append(" ");
part.append(renderedPart);
} else if (segment instanceof Literal<?>) {
part.append(" ELSE ");
part.append(renderedPart);
}
}
return super.leaveNested(segment);
}
@Override
Delegation enterMatched(CaseExpression segment) {
part.append("CASE");
return super.enterMatched(segment);
}
@Override
Delegation leaveMatched(CaseExpression segment) {
part.append(" END");
return super.leaveMatched(segment);
}
@Override
public CharSequence getRenderedPart() {
return part;
}
}

View File

@@ -108,6 +108,10 @@ class ExpressionVisitor extends TypedSubtreeVisitor<Expression> implements PartR
CastVisitor visitor = new CastVisitor(context);
partRenderer = visitor;
return Delegation.delegateTo(visitor);
} else if (segment instanceof CaseExpression) {
CaseExpressionVisitor visitor = new CaseExpressionVisitor(context);
partRenderer = visitor;
return Delegation.delegateTo(visitor);
} else {
// works for literals and just and possibly more
value = segment.toString();

View File

@@ -15,7 +15,9 @@
*/
package org.springframework.data.relational.core.sql.render;
import org.springframework.data.relational.core.sql.Column;
import org.springframework.data.relational.core.sql.CaseExpression;
import org.springframework.data.relational.core.sql.Expressions;
import org.springframework.data.relational.core.sql.OrderByField;
import org.springframework.data.relational.core.sql.SimpleFunction;
@@ -83,7 +85,7 @@ class OrderByClauseVisitor extends TypedSubtreeVisitor<OrderByField> implements
return Delegation.delegateTo((SimpleFunctionVisitor)delegate);
}
if (segment instanceof Expressions.SimpleExpression) {
if (segment instanceof Expressions.SimpleExpression || segment instanceof CaseExpression) {
delegate = new ExpressionVisitor(context);
return Delegation.delegateTo((ExpressionVisitor)delegate);
}

View File

@@ -18,6 +18,7 @@ package org.springframework.data.relational.core.sql.render;
import org.springframework.data.relational.core.sql.Condition;
import org.springframework.data.relational.core.sql.Expression;
import org.springframework.data.relational.core.sql.Visitable;
import org.springframework.data.relational.core.sql.When;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -26,6 +27,7 @@ import org.springframework.util.Assert;
* delegate nested {@link Expression} and {@link Condition} rendering.
*
* @author Mark Paluch
* @author Sven Rienstra
* @since 1.1
*/
abstract class TypedSingleConditionRenderSupport<T extends Visitable> extends TypedSubtreeVisitor<T> {
@@ -40,8 +42,8 @@ abstract class TypedSingleConditionRenderSupport<T extends Visitable> extends Ty
@Override
Delegation enterNested(Visitable segment) {
if (segment instanceof Expression) {
ExpressionVisitor visitor = new ExpressionVisitor(context);
if (segment instanceof When) {
WhenVisitor visitor = new WhenVisitor(context);
current = visitor;
return Delegation.delegateTo(visitor);
}
@@ -52,6 +54,12 @@ abstract class TypedSingleConditionRenderSupport<T extends Visitable> extends Ty
return Delegation.delegateTo(visitor);
}
if (segment instanceof Expression) {
ExpressionVisitor visitor = new ExpressionVisitor(context);
current = visitor;
return Delegation.delegateTo(visitor);
}
throw new IllegalStateException("Cannot provide visitor for " + segment);
}

View File

@@ -0,0 +1,48 @@
package org.springframework.data.relational.core.sql.render;
import org.springframework.data.relational.core.sql.Visitable;
import org.springframework.data.relational.core.sql.When;
/**
* Renderer for {@link When} segments.
*
* @author Sven Rienstra
* @since 3.4
*/
public class WhenVisitor extends TypedSingleConditionRenderSupport<When> implements PartRenderer {
private final StringBuilder part = new StringBuilder();
private boolean conditionRendered;
WhenVisitor(RenderContext context) {
super(context);
}
@Override
Delegation leaveNested(Visitable segment) {
if (hasDelegatedRendering()) {
if (conditionRendered) {
part.append(" THEN ");
}
part.append(consumeRenderedPart());
conditionRendered = true;
}
return super.leaveNested(segment);
}
@Override
Delegation enterMatched(When segment) {
part.append("WHEN ");
return super.enterMatched(segment);
}
@Override
public CharSequence getRenderedPart() {
return part;
}
}

View File

@@ -18,14 +18,7 @@ package org.springframework.data.relational.core.sql.render;
import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.Test;
import org.springframework.data.relational.core.sql.Column;
import org.springframework.data.relational.core.sql.Expression;
import org.springframework.data.relational.core.sql.Expressions;
import org.springframework.data.relational.core.sql.OrderByField;
import org.springframework.data.relational.core.sql.SQL;
import org.springframework.data.relational.core.sql.Select;
import org.springframework.data.relational.core.sql.SimpleFunction;
import org.springframework.data.relational.core.sql.Table;
import org.springframework.data.relational.core.sql.*;
import java.util.Arrays;
import java.util.List;
@@ -129,4 +122,18 @@ class OrderByClauseVisitorUnitTests {
assertThat(visitor.getRenderedPart().toString()).isEqualTo("1 ASC");
}
@Test
void shouldRenderOrderByCase() {
Table employee = SQL.table("employee").as("emp");
Column column = employee.column("name");
CaseExpression caseExpression = CaseExpression.create(When.when(column.isNull(), SQL.literalOf(1))).elseExpression(SQL.literalOf(2));
Select select = Select.builder().select(column).from(employee).orderBy(OrderByField.from(caseExpression).asc()).build();
OrderByClauseVisitor visitor = new OrderByClauseVisitor(new SimpleRenderContext(NamingStrategies.asIs()));
select.visit(visitor);
assertThat(visitor.getRenderedPart().toString()).isEqualTo("CASE WHEN emp.name IS NULL THEN 1 ELSE 2 END ASC");
}
}

View File

@@ -688,6 +688,23 @@ class SelectRendererUnitTests {
assertThat(rendered).isEqualTo("SELECT e.*, e.id FROM employee e");
}
@Test
void rendersCaseExpression() {
Table table = SQL.table("table");
Column column = table.column("name");
CaseExpression caseExpression = CaseExpression.create(When.when(column.isNull(), SQL.literalOf(1))) //
.when(When.when(column.isNotNull(), SQL.literalOf(2))) //
.elseExpression(SQL.literalOf(3));
Select select = StatementBuilder.select(caseExpression) //
.from(table) //
.build();
String rendered = SqlRenderer.toString(select);
assertThat(rendered).isEqualTo("SELECT CASE WHEN table.name IS NULL THEN 1 WHEN table.name IS NOT NULL THEN 2 ELSE 3 END FROM table");
}
/**
* Tests the rendering of analytic functions.
*/