Skip to content

Commit cf39fa2

Browse files
schaudermp911de
authored andcommitted
Adds a CAST expression to the SQL DSL.
Example: Expressions.cast(table_user.column("name"),"VARCHAR2") Also adds a toString to AbstractSegment to avoid stack overflows. Closes #1066 Original pull request: #1071.
1 parent b2c4509 commit cf39fa2

File tree

7 files changed

+179
-16
lines changed

7 files changed

+179
-16
lines changed

spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AbstractSegment.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
*/
1616
package org.springframework.data.relational.core.sql;
1717

18+
import java.util.Arrays;
19+
1820
import org.springframework.util.Assert;
21+
import org.springframework.util.StringUtils;
1922

2023
/**
2124
* Abstract implementation to support {@link Segment} implementations.
@@ -64,4 +67,10 @@ public int hashCode() {
6467
public boolean equals(Object obj) {
6568
return obj instanceof Segment && toString().equals(obj.toString());
6669
}
70+
71+
@Override
72+
public String toString() {
73+
return StringUtils.collectionToDelimitedString(Arrays.asList(children), ", ", getClass().getSimpleName() + "(",
74+
")");
75+
}
6776
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright 2021 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.springframework.data.relational.core.sql;
17+
18+
import org.springframework.util.Assert;
19+
20+
/**
21+
* Represents a CAST expression like {@code CAST(something AS JSON}.
22+
*
23+
* @author Jens Schauder
24+
* @since 2.3
25+
*/
26+
public class Cast extends AbstractSegment implements Expression {
27+
28+
private final String targetType;
29+
private final Expression expression;
30+
31+
private Cast(Expression expression, String targetType) {
32+
33+
super(expression);
34+
35+
Assert.notNull(targetType, "Cast target must not be null!");
36+
37+
this.expression = expression;
38+
this.targetType = targetType;
39+
}
40+
41+
/**
42+
* Creates a new CAST expression.
43+
*
44+
* @param expression the expression to cast. Must not be {@literal null}.
45+
* @param targetType the type to cast to. Must not be {@literal null}.
46+
* @return guaranteed to be not {@literal null}.
47+
*/
48+
static Expression create(Expression expression, String targetType) {
49+
return new Cast(expression, targetType);
50+
}
51+
52+
public String getTargetType() {
53+
return targetType;
54+
}
55+
56+
@Override
57+
public String toString() {
58+
return "CAST(" + expression + " AS " + targetType + ")";
59+
}
60+
}

spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Expressions.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@ public static Expression asterisk(Table table) {
5353
return table.asterisk();
5454
}
5555

56+
/**
57+
* @return a new {@link Cast} expression.
58+
*/
59+
public static Expression cast(Expression expression, String targetType) {
60+
return Cast.create(expression, targetType);
61+
}
62+
5663
// Utility constructor.
5764
private Expressions() {}
5865

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright 2021 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.springframework.data.relational.core.sql.render;
17+
18+
import java.util.StringJoiner;
19+
20+
import org.springframework.data.relational.core.sql.Cast;
21+
import org.springframework.data.relational.core.sql.Visitable;
22+
import org.springframework.lang.Nullable;
23+
import org.springframework.util.Assert;
24+
25+
/**
26+
* Renders a CAST expression, by delegating to an {@link ExpressionVisitor} and building the expression out of the
27+
* rendered parts.
28+
*
29+
* @author Jens Schauder
30+
* @since 2.3
31+
*/
32+
class CastVisitor extends TypedSubtreeVisitor<Cast> implements PartRenderer {
33+
34+
private final RenderContext context;
35+
@Nullable private StringJoiner joiner;
36+
@Nullable private ExpressionVisitor expressionVisitor;
37+
38+
CastVisitor(RenderContext context) {
39+
40+
this.context = context;
41+
}
42+
43+
@Override
44+
Delegation enterMatched(Cast cast) {
45+
46+
joiner = new StringJoiner(", ", "CAST(", " AS " + cast.getTargetType() + ")");
47+
48+
return super.enterMatched(cast);
49+
}
50+
51+
@Override
52+
Delegation enterNested(Visitable segment) {
53+
54+
expressionVisitor = new ExpressionVisitor(context, ExpressionVisitor.AliasHandling.IGNORE);
55+
return Delegation.delegateTo(expressionVisitor);
56+
}
57+
58+
@Override
59+
Delegation leaveNested(Visitable segment) {
60+
61+
Assert.state(joiner != null, "Joiner must not be null.");
62+
Assert.state(expressionVisitor != null, "ExpressionVisitor must not be null.");
63+
64+
joiner.add(expressionVisitor.getRenderedPart());
65+
return super.leaveNested(segment);
66+
}
67+
68+
@Override
69+
public CharSequence getRenderedPart() {
70+
71+
if (joiner == null) {
72+
throw new IllegalStateException("Joiner must not be null.");
73+
}
74+
75+
return joiner.toString();
76+
}
77+
}

spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,7 @@
1515
*/
1616
package org.springframework.data.relational.core.sql.render;
1717

18-
import org.springframework.data.relational.core.sql.AsteriskFromTable;
19-
import org.springframework.data.relational.core.sql.BindMarker;
20-
import org.springframework.data.relational.core.sql.Column;
21-
import org.springframework.data.relational.core.sql.Condition;
22-
import org.springframework.data.relational.core.sql.Expression;
23-
import org.springframework.data.relational.core.sql.Named;
24-
import org.springframework.data.relational.core.sql.SimpleFunction;
25-
import org.springframework.data.relational.core.sql.SubselectExpression;
26-
import org.springframework.data.relational.core.sql.Visitable;
18+
import org.springframework.data.relational.core.sql.*;
2719
import org.springframework.lang.Nullable;
2820
import org.springframework.util.Assert;
2921

@@ -105,6 +97,11 @@ Delegation enterMatched(Expression segment) {
10597
}
10698
} else if (segment instanceof AsteriskFromTable) {
10799
value = NameRenderer.render(context, ((AsteriskFromTable) segment).getTable()) + ".*";
100+
} else if (segment instanceof Cast) {
101+
102+
CastVisitor visitor = new CastVisitor(context);
103+
partRenderer = visitor;
104+
return Delegation.delegateTo(visitor);
108105
} else {
109106
// works for literals and just and possibly more
110107
value = segment.toString();
@@ -138,6 +135,7 @@ Delegation enterNested(Visitable segment) {
138135
Delegation leaveMatched(Expression segment) {
139136

140137
if (partRenderer != null) {
138+
141139
value = partRenderer.getRenderedPart();
142140
partRenderer = null;
143141
}

spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/ExpressionVisitorUnitTests.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,10 @@ static List<Fixture> expressionsWithOutAliasGetRendered() {
6868
fixture("Count *", Functions.count(Expressions.asterisk()), "COUNT(*)"), //
6969
fixture("Function", SimpleFunction.create("Function", asList(SQL.literalOf("one"), SQL.literalOf("two"))), //
7070
"Function('one', 'two')"), //
71-
fixture("Null", SQL.nullLiteral(), "NULL")); //
71+
fixture("Null", SQL.nullLiteral(), "NULL"), //
72+
fixture("Cast", Expressions.cast(Column.create("col", Table.create("tab")), "JSON"), "CAST(tab.col AS JSON)"), //
73+
fixture("Cast with alias", Expressions.cast(Column.create("col", Table.create("tab")).as("alias"), "JSON"),
74+
"CAST(tab.col AS JSON)")); //
7275
}
7376

7477
@Test // GH-1003

spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/SelectRendererUnitTests.java

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,7 @@ void shouldRenderArbitraryJoinCondition() {
168168
.join(department) //
169169
.on(Conditions.isEqual(employee.column("department_id"), department.column("id")) //
170170
.or(Conditions.isNotEqual(employee.column("tenant"), department.column("tenant")) //
171-
))
172-
.build();
171+
)).build();
173172

174173
assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee " //
175174
+ "JOIN department ON employee.department_id = department.id " //
@@ -183,12 +182,11 @@ void shouldRenderJoinWithJustExpression() {
183182
Table department = SQL.table("department");
184183

185184
Select select = Select.builder().select(employee.column("id"), department.column("name")).from(employee) //
186-
.join(department)
187-
.on(Expressions.just("alpha")).equals(Expressions.just("beta")) //
185+
.join(department).on(Expressions.just("alpha")).equals(Expressions.just("beta")) //
188186
.build();
189187

190-
assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee "
191-
+ "JOIN department ON alpha = beta");
188+
assertThat(SqlRenderer.toString(select))
189+
.isEqualTo("SELECT employee.id, department.name FROM employee " + "JOIN department ON alpha = beta");
192190
}
193191

194192
@Test // DATAJDBC-309
@@ -458,4 +456,15 @@ void simpleComparison() {
458456
final String rendered = SqlRenderer.toString(select);
459457
assertThat(rendered).isEqualTo("SELECT User.name, User.age FROM User WHERE User.age > 20");
460458
}
459+
460+
@Test // GH-1066
461+
void shouldRenderCast() {
462+
463+
Table table_user = SQL.table("User");
464+
Select select = StatementBuilder.select(Expressions.cast(table_user.column("name"), "VARCHAR2")).from(table_user)
465+
.build();
466+
467+
final String rendered = SqlRenderer.toString(select);
468+
assertThat(rendered).isEqualTo("SELECT CAST(User.name AS VARCHAR2) FROM User");
469+
}
461470
}

0 commit comments

Comments
 (0)