diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AbstractSegment.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AbstractSegment.java index 7f567ca62..be2147d93 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AbstractSegment.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AbstractSegment.java @@ -15,7 +15,10 @@ */ package org.springframework.data.relational.core.sql; +import java.util.Arrays; + import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * Abstract implementation to support {@link Segment} implementations. @@ -64,4 +67,10 @@ abstract class AbstractSegment implements Segment { public boolean equals(Object obj) { return obj instanceof Segment && toString().equals(obj.toString()); } + + @Override + public String toString() { + return StringUtils.collectionToDelimitedString(Arrays.asList(children), ", ", getClass().getSimpleName() + "(", + ")"); + } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Cast.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Cast.java new file mode 100644 index 000000000..2d473ce19 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Cast.java @@ -0,0 +1,60 @@ +/* + * Copyright 2021 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.springframework.data.relational.core.sql; + +import org.springframework.util.Assert; + +/** + * Represents a CAST expression like {@code CAST(something AS JSON}. + * + * @author Jens Schauder + * @since 2.3 + */ +public class Cast extends AbstractSegment implements Expression { + + private final String targetType; + private final Expression expression; + + private Cast(Expression expression, String targetType) { + + super(expression); + + Assert.notNull(targetType, "Cast target must not be null!"); + + this.expression = expression; + this.targetType = targetType; + } + + /** + * Creates a new CAST expression. + * + * @param expression the expression to cast. Must not be {@literal null}. + * @param targetType the type to cast to. Must not be {@literal null}. + * @return guaranteed to be not {@literal null}. + */ + static Expression create(Expression expression, String targetType) { + return new Cast(expression, targetType); + } + + public String getTargetType() { + return targetType; + } + + @Override + public String toString() { + return "CAST(" + expression + " AS " + targetType + ")"; + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Expressions.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Expressions.java index 3501db9ba..571d2081b 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Expressions.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Expressions.java @@ -53,6 +53,13 @@ public abstract class Expressions { return table.asterisk(); } + /** + * @return a new {@link Cast} expression. + */ + public static Expression cast(Expression expression, String targetType) { + return Cast.create(expression, targetType); + } + // Utility constructor. private Expressions() {} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/CastVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/CastVisitor.java new file mode 100644 index 000000000..a89e70993 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/CastVisitor.java @@ -0,0 +1,77 @@ +/* + * Copyright 2021 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.springframework.data.relational.core.sql.render; + +import java.util.StringJoiner; + +import org.springframework.data.relational.core.sql.Cast; +import org.springframework.data.relational.core.sql.Visitable; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Renders a CAST expression, by delegating to an {@link ExpressionVisitor} and building the expression out of the + * rendered parts. + * + * @author Jens Schauder + * @since 2.3 + */ +class CastVisitor extends TypedSubtreeVisitor implements PartRenderer { + + private final RenderContext context; + @Nullable private StringJoiner joiner; + @Nullable private ExpressionVisitor expressionVisitor; + + CastVisitor(RenderContext context) { + + this.context = context; + } + + @Override + Delegation enterMatched(Cast cast) { + + joiner = new StringJoiner(", ", "CAST(", " AS " + cast.getTargetType() + ")"); + + return super.enterMatched(cast); + } + + @Override + Delegation enterNested(Visitable segment) { + + expressionVisitor = new ExpressionVisitor(context, ExpressionVisitor.AliasHandling.IGNORE); + return Delegation.delegateTo(expressionVisitor); + } + + @Override + Delegation leaveNested(Visitable segment) { + + Assert.state(joiner != null, "Joiner must not be null."); + Assert.state(expressionVisitor != null, "ExpressionVisitor must not be null."); + + joiner.add(expressionVisitor.getRenderedPart()); + return super.leaveNested(segment); + } + + @Override + public CharSequence getRenderedPart() { + + if (joiner == null) { + throw new IllegalStateException("Joiner must not be null."); + } + + return joiner.toString(); + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java index 43f391c99..a63ec8fb8 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java @@ -15,15 +15,7 @@ */ package org.springframework.data.relational.core.sql.render; -import org.springframework.data.relational.core.sql.AsteriskFromTable; -import org.springframework.data.relational.core.sql.BindMarker; -import org.springframework.data.relational.core.sql.Column; -import org.springframework.data.relational.core.sql.Condition; -import org.springframework.data.relational.core.sql.Expression; -import org.springframework.data.relational.core.sql.Named; -import org.springframework.data.relational.core.sql.SimpleFunction; -import org.springframework.data.relational.core.sql.SubselectExpression; -import org.springframework.data.relational.core.sql.Visitable; +import org.springframework.data.relational.core.sql.*; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -105,6 +97,11 @@ class ExpressionVisitor extends TypedSubtreeVisitor implements PartR } } else if (segment instanceof AsteriskFromTable) { value = NameRenderer.render(context, ((AsteriskFromTable) segment).getTable()) + ".*"; + } else if (segment instanceof Cast) { + + CastVisitor visitor = new CastVisitor(context); + partRenderer = visitor; + return Delegation.delegateTo(visitor); } else { // works for literals and just and possibly more value = segment.toString(); @@ -138,6 +135,7 @@ class ExpressionVisitor extends TypedSubtreeVisitor implements PartR Delegation leaveMatched(Expression segment) { if (partRenderer != null) { + value = partRenderer.getRenderedPart(); partRenderer = null; } diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/ExpressionVisitorUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/ExpressionVisitorUnitTests.java index 9808aed29..f2128a4c8 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/ExpressionVisitorUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/ExpressionVisitorUnitTests.java @@ -68,7 +68,10 @@ public class ExpressionVisitorUnitTests { fixture("Count *", Functions.count(Expressions.asterisk()), "COUNT(*)"), // fixture("Function", SimpleFunction.create("Function", asList(SQL.literalOf("one"), SQL.literalOf("two"))), // "Function('one', 'two')"), // - fixture("Null", SQL.nullLiteral(), "NULL")); // + fixture("Null", SQL.nullLiteral(), "NULL"), // + fixture("Cast", Expressions.cast(Column.create("col", Table.create("tab")), "JSON"), "CAST(tab.col AS JSON)"), // + fixture("Cast with alias", Expressions.cast(Column.create("col", Table.create("tab")).as("alias"), "JSON"), + "CAST(tab.col AS JSON)")); // } @Test // GH-1003 diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/SelectRendererUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/SelectRendererUnitTests.java index 15895fd40..7a223c156 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/SelectRendererUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/SelectRendererUnitTests.java @@ -168,8 +168,7 @@ class SelectRendererUnitTests { .join(department) // .on(Conditions.isEqual(employee.column("department_id"), department.column("id")) // .or(Conditions.isNotEqual(employee.column("tenant"), department.column("tenant")) // - )) - .build(); + )).build(); assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee " // + "JOIN department ON employee.department_id = department.id " // @@ -183,12 +182,11 @@ class SelectRendererUnitTests { Table department = SQL.table("department"); Select select = Select.builder().select(employee.column("id"), department.column("name")).from(employee) // - .join(department) - .on(Expressions.just("alpha")).equals(Expressions.just("beta")) // + .join(department).on(Expressions.just("alpha")).equals(Expressions.just("beta")) // .build(); - assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee " - + "JOIN department ON alpha = beta"); + assertThat(SqlRenderer.toString(select)) + .isEqualTo("SELECT employee.id, department.name FROM employee " + "JOIN department ON alpha = beta"); } @Test // DATAJDBC-309 @@ -458,4 +456,15 @@ class SelectRendererUnitTests { final String rendered = SqlRenderer.toString(select); assertThat(rendered).isEqualTo("SELECT User.name, User.age FROM User WHERE User.age > 20"); } + + @Test // GH-1066 + void shouldRenderCast() { + + Table table_user = SQL.table("User"); + Select select = StatementBuilder.select(Expressions.cast(table_user.column("name"), "VARCHAR2")).from(table_user) + .build(); + + final String rendered = SqlRenderer.toString(select); + assertThat(rendered).isEqualTo("SELECT CAST(User.name AS VARCHAR2) FROM User"); + } }