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 490da7b1..33070905 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 @@ -21,15 +21,20 @@ import org.springframework.util.Assert; * Abstract implementation to support {@link Segment} implementations. * * @author Mark Paluch + * @since 1.1 */ abstract class AbstractSegment implements Segment { private final Segment[] children; - protected AbstractSegment(Segment ... children) { + protected AbstractSegment(Segment... children) { this.children = children; } + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.Visitable#visit(org.springframework.data.relational.core.sql.Visitor) + */ @Override public void visit(Visitor visitor) { diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Aliased.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Aliased.java index 07826bba..829177fa 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Aliased.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Aliased.java @@ -19,6 +19,7 @@ package org.springframework.data.relational.core.sql; * Aliased element exposing an {@link #getAlias() alias}. * * @author Mark Paluch + * @since 1.1 */ public interface Aliased { diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AliasedExpression.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AliasedExpression.java index 6d2b48a8..1c926741 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AliasedExpression.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AliasedExpression.java @@ -19,8 +19,9 @@ package org.springframework.data.relational.core.sql; * An expression with an alias. * * @author Jens Schauder + * @since 1.1 */ -public class AliasedExpression extends AbstractSegment implements Aliased, Expression { +class AliasedExpression extends AbstractSegment implements Aliased, Expression { private final Expression expression; private final String alias; @@ -33,11 +34,19 @@ public class AliasedExpression extends AbstractSegment implements Aliased, Expre this.alias = alias; } + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.Aliased#getAlias() + */ @Override public String getAlias() { return alias; } + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ @Override public String toString() { return expression.toString() + " AS " + alias; diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AndCondition.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AndCondition.java index a0201d8c..4447eed1 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AndCondition.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AndCondition.java @@ -19,6 +19,7 @@ package org.springframework.data.relational.core.sql; * {@link Condition} representing an {@code AND} relation between two {@link Condition}s. * * @author Mark Paluch + * @since 1.1 * @see Condition#and(Condition) */ public class AndCondition extends MultipleCondition { @@ -26,5 +27,4 @@ public class AndCondition extends MultipleCondition { AndCondition(Condition... conditions) { super(" AND ", conditions); } - } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AsteriskFromTable.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AsteriskFromTable.java index 7dc5cd87..f2803ade 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AsteriskFromTable.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AsteriskFromTable.java @@ -15,14 +15,19 @@ */ package org.springframework.data.relational.core.sql; -import org.springframework.util.Assert; - /** * {@link Segment} to select all columns from a {@link Table}. *

- * * Renders to: {@code .*} as in {@code SELECT
.* FROM …}. + * * Renders to: {@code + * +
+ * .*} as in {@code SELECT + * +
+ * .* FROM …}. * * @author Mark Paluch + * @since 1.1 * @see Table#asterisk() */ public class AsteriskFromTable extends AbstractSegment implements Expression { @@ -45,6 +50,10 @@ public class AsteriskFromTable extends AbstractSegment implements Expression { return table; } + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ @Override public String toString() { diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/BindMarker.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/BindMarker.java index 5f14d0cb..16957cde 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/BindMarker.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/BindMarker.java @@ -21,6 +21,7 @@ import org.springframework.lang.Nullable; * Bind marker/parameter placeholder used to construct prepared statements with parameter substitution. * * @author Mark Paluch + * @since 1.1 */ public class BindMarker extends AbstractSegment implements Expression { diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Column.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Column.java index dccf97ee..356a8e83 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Column.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Column.java @@ -24,6 +24,7 @@ import org.springframework.util.Assert; * Renders to: {@code } or {@code .}. * * @author Mark Paluch + * @since 1.1 */ public class Column extends AbstractSegment implements Expression, Named { @@ -72,7 +73,7 @@ public class Column extends AbstractSegment implements Expression, Named { } /** - * Create a new aliased {@link Column}. + * Creates a new aliased {@link Column}. * * @param alias column alias name, must not {@literal null} or empty. * @return the aliased {@link Column}. @@ -85,7 +86,7 @@ public class Column extends AbstractSegment implements Expression, Named { } /** - * Create a new {@link Column} associated with a {@link Table}. + * Creates a new {@link Column} associated with a {@link Table}. * * @param table the table, must not be {@literal null}. * @return a new {@link Column} associated with {@link Table}. @@ -97,6 +98,108 @@ public class Column extends AbstractSegment implements Expression, Named { return new Column(name, table); } + // ------------------------------------------------------------------------- + // Methods for Condition creation. + // ------------------------------------------------------------------------- + + /** + * Creates a {@code =} (equals) {@link Condition}. + * + * @param expression right side of the comparison. + * @return the {@link Comparison} condition. + */ + public Comparison isEqualTo(Expression expression) { + return Conditions.isEqual(this, expression); + } + + /** + * Creates a {@code !=} (not equals) {@link Condition}. + * + * @param expression right side of the comparison. + * @return the {@link Comparison} condition. + */ + public Comparison isNotEqualTo(Expression expression) { + return Conditions.isNotEqual(this, expression); + } + + /** + * Creates a {@code <} (less) {@link Condition} {@link Condition}. + * + * @param expression right side of the comparison. + * @return the {@link Comparison} condition. + */ + public Comparison isLess(Expression expression) { + return Conditions.isLess(this, expression); + } + + /** + * CCreates a {@code <=} (greater ) {@link Condition} {@link Condition}. + * + * @param expression right side of the comparison. + * @return the {@link Comparison} condition. + */ + public Comparison isLessOrEqualTo(Expression expression) { + return Conditions.isLessOrEqualTo(this, expression); + } + + /** + * Creates a {@code !=} (not equals) {@link Condition}. + * + * @param expression right side of the comparison. + * @return the {@link Comparison} condition. + */ + public Comparison isGreater(Expression expression) { + return Conditions.isGreater(this, expression); + } + + /** + * Creates a {@code <=} (greater or equal to) {@link Condition} {@link Condition}. + * + * @param expression right side of the comparison. + * @return the {@link Comparison} condition. + */ + public Comparison isGreaterOrEqualTo(Expression expression) { + return Conditions.isGreaterOrEqualTo(this, expression); + } + + /** + * Creates a {@code LIKE} {@link Condition}. + * + * @param expression right side of the comparison. + * @return the {@link Like} condition. + */ + public Like like(Expression expression) { + return Conditions.like(this, expression); + } + + /** + * Creates a new {@link In} {@link Condition} given right {@link Expression}s. + * + * @param expression right side of the comparison. + * @return the {@link In} condition. + */ + public In in(Expression... expression) { + return Conditions.in(this, expression); + } + + /** + * Creates a {@code IS NULL} condition. + * + * @return the {@link IsNull} condition. + */ + public IsNull isNull() { + return Conditions.isNull(this); + } + + /** + * Creates a {@code IS NOT NULL} condition. + * + * @return the {@link Condition} condition. + */ + public Condition isNotNull() { + return isNull().not(); + } + /* * (non-Javadoc) * @see org.springframework.data.relational.core.sql.Named#getName() diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Comparison.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Comparison.java new file mode 100644 index 00000000..a5641c23 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Comparison.java @@ -0,0 +1,100 @@ +/* + * Copyright 2019 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 + * + * http://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; + +/** + * Comparing {@link Condition} comparing two {@link Expression}s. + *

+ * Results in a rendered condition: {@code } (e.g. {@code col = 'predicate'}. + * + * @author Mark Paluch + * @since 1.1 + */ +public class Comparison extends AbstractSegment implements Condition { + + private final Expression left; + private final String comparator; + private final Expression right; + + private Comparison(Expression left, String comparator, Expression right) { + + super(left, right); + + this.left = left; + this.comparator = comparator; + this.right = right; + } + + /** + * Creates a new {@link Comparison} {@link Condition} given two {@link Expression}s. + * + * @param leftColumnOrExpression the left {@link Expression}. + * @param comparator the comparator. + * @param rightColumnOrExpression the right {@link Expression}. + * @return the {@link Comparison} condition. + */ + public static Comparison create(Expression leftColumnOrExpression, String comparator, + Expression rightColumnOrExpression) { + + Assert.notNull(leftColumnOrExpression, "Left expression must not be null!"); + Assert.notNull(comparator, "Comparator must not be null!"); + Assert.notNull(rightColumnOrExpression, "Right expression must not be null!"); + + return new Comparison(leftColumnOrExpression, comparator, rightColumnOrExpression); + } + + @Override + public Condition not() { + + if ("=".equals(comparator)) { + return new Comparison(left, "!=", right); + } + + if ("!=".equals(comparator)) { + return new Comparison(left, "=", right); + } + + return new Not(this); + } + + /** + * @return the left {@link Expression}. + */ + public Expression getLeft() { + return left; + } + + /** + * @return the comparator. + */ + public String getComparator() { + return comparator; + } + + /** + * @return the right {@link Expression}. + */ + public Expression getRight() { + return right; + } + + @Override + public String toString() { + return left.toString() + " " + comparator + " " + right.toString(); + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Condition.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Condition.java index 88341993..b7799392 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Condition.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Condition.java @@ -19,6 +19,8 @@ package org.springframework.data.relational.core.sql; * AST {@link Segment} for a condition. * * @author Mark Paluch + * @author Jens Schauder + * @since 1.1 * @see Conditions */ public interface Condition extends Segment { @@ -43,6 +45,11 @@ public interface Condition extends Segment { return new OrCondition(this, other); } + /** + * Creates a {@link Condition} that negates this {@link Condition}. + * + * @return the negated {@link Condition}. + */ default Condition not() { return new Not(this); } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Conditions.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Conditions.java index 3e16c13d..bee428a9 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Conditions.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Conditions.java @@ -15,23 +15,24 @@ */ package org.springframework.data.relational.core.sql; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; + +import org.springframework.util.Assert; + /** * Factory for common {@link Condition}s. * * @author Mark Paluch + * @author Jens Schauder + * @since 1.1 * @see SQL * @see Expressions * @see Functions */ public abstract class Conditions { - /** - * @return a new {@link Equals} condition. - */ - public static Equals equals(Expression left, Expression right) { - return Equals.create(left, right); - } - /** * Creates a plain {@code sql} {@link Condition}. * @@ -42,20 +43,153 @@ public abstract class Conditions { return new ConstantCondition(sql); } - // Utility constructor. - private Conditions() { + /** + * Creates a {@code IS NULL} condition. + * + * @param expression the expression to check for nullability, must not be {@literal null}. + * @return the {@code IS NULL} condition. + */ + public static IsNull isNull(Expression expression) { + return IsNull.create(expression); } - public static Condition isNull(Expression expression) { - return new IsNull(expression); + /** + * Creates a {@code =} (equals) {@link Condition}. + * + * @param leftColumnOrExpression left side of the comparison. + * @param rightColumnOrExpression right side of the comparison. + * @return the {@link Comparison} condition. + */ + public static Comparison isEqual(Expression leftColumnOrExpression, Expression rightColumnOrExpression) { + return Comparison.create(leftColumnOrExpression, "=", rightColumnOrExpression); } - public static Condition isEqual(Column bar, Expression param) { - return new Equals(bar, param); + /** + * Creates a {@code !=} (not equals) {@link Condition}. + * + * @param leftColumnOrExpression left side of the comparison. + * @param rightColumnOrExpression right side of the comparison. + * @return the {@link Comparison} condition. + */ + public static Comparison isNotEqual(Expression leftColumnOrExpression, Expression rightColumnOrExpression) { + return Comparison.create(leftColumnOrExpression, "!=", rightColumnOrExpression); } - public static Condition in(Column bar, Expression subselectExpression) { - return new In(bar, subselectExpression); + /** + * Creates a {@code <} (less) {@link Condition} comparing {@code left} is less than {@code right}. + * + * @param leftColumnOrExpression left side of the comparison. + * @param rightColumnOrExpression right side of the comparison. + * @return the {@link Comparison} condition. + */ + public static Comparison isLess(Expression leftColumnOrExpression, Expression rightColumnOrExpression) { + return Comparison.create(leftColumnOrExpression, "<", rightColumnOrExpression); + } + + /** + * Creates a {@code <=} (less or equal to) {@link Condition} comparing {@code left} is less than or equal to + * {@code right}. + * + * @param leftColumnOrExpression left side of the comparison. + * @param rightColumnOrExpression right side of the comparison. + * @return the {@link Comparison} condition. + */ + public static Comparison isLessOrEqualTo(Expression leftColumnOrExpression, Expression rightColumnOrExpression) { + return Comparison.create(leftColumnOrExpression, "<=", rightColumnOrExpression); + } + + /** + * Creates a {@code <=} (greater ) {@link Condition} comparing {@code left} is greater than {@code right}. + * + * @param leftColumnOrExpression left side of the comparison. + * @param rightColumnOrExpression right side of the comparison. + * @return the {@link Comparison} condition. + */ + public static Comparison isGreater(Expression leftColumnOrExpression, Expression rightColumnOrExpression) { + return Comparison.create(leftColumnOrExpression, ">", rightColumnOrExpression); + } + + /** + * Creates a {@code <=} (greater or equal to) {@link Condition} comparing {@code left} is greater than or equal to + * {@code right}. + * + * @param leftColumnOrExpression left side of the comparison. + * @param rightColumnOrExpression right side of the comparison. + * @return the {@link Comparison} condition. + */ + public static Comparison isGreaterOrEqualTo(Expression leftColumnOrExpression, Expression rightColumnOrExpression) { + return Comparison.create(leftColumnOrExpression, ">=", rightColumnOrExpression); + } + + /** + * Creates a {@code LIKE} {@link Condition}. + * + * @param leftColumnOrExpression left side of the comparison. + * @param rightColumnOrExpression right side of the comparison. + * @return the {@link Comparison} condition. + */ + public static Like like(Expression leftColumnOrExpression, Expression rightColumnOrExpression) { + return Like.create(leftColumnOrExpression, rightColumnOrExpression); + } + + /** + * Creates a {@code IN} {@link Condition clause}. + * + * @param columnOrExpression left side of the comparison. + * @param arg IN argument. + * @return the {@link In} condition. + */ + public static Condition in(Expression columnOrExpression, Expression arg) { + + Assert.notNull(columnOrExpression, "Comparison column or expression must not be null"); + Assert.notNull(arg, "Expression argument must not be null"); + + return In.create(columnOrExpression, arg); + } + + /** + * Creates a new {@link In} {@link Condition} given left and right {@link Expression}s. + * + * @param columnOrExpression left hand side of the {@link Condition} must not be {@literal null}. + * @param expressions right hand side (collection {@link Expression}) must not be {@literal null}. + * @return the {@link In} {@link Condition}. + */ + public static Condition in(Expression columnOrExpression, Collection expressions) { + + Assert.notNull(columnOrExpression, "Comparison column or expression must not be null"); + Assert.notNull(expressions, "Expression argument must not be null"); + + return In.create(columnOrExpression, new ArrayList<>(expressions)); + } + + /** + * Creates a new {@link In} {@link Condition} given left and right {@link Expression}s. + * + * @param columnOrExpression left hand side of the {@link Condition} must not be {@literal null}. + * @param expressions right hand side (collection {@link Expression}) must not be {@literal null}. + * @return the {@link In} {@link Condition}. + */ + public static In in(Expression columnOrExpression, Expression... expressions) { + + Assert.notNull(columnOrExpression, "Comparison column or expression must not be null"); + Assert.notNull(expressions, "Expression argument must not be null"); + + return In.create(columnOrExpression, Arrays.asList(expressions)); + } + + /** + * Creates a {@code IN} {@link Condition clause} for a {@link Select subselect}. + * + * @param column the column to compare. + * @param subselect the subselect. + * @return the {@link In} condition. + */ + public static Condition in(Column column, Select subselect) { + + Assert.notNull(column, "Column must not be null"); + Assert.notNull(subselect, "Subselect must not be null"); + + return in(column, new SubselectExpression(subselect)); } static class ConstantCondition extends AbstractSegment implements Condition { @@ -71,8 +205,7 @@ public abstract class Conditions { return condition; } } + + // Utility constructor. + private Conditions() {} } - - - - diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelect.java index 50f6507b..6bd8f8d5 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelect.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelect.java @@ -26,11 +26,12 @@ import org.springframework.util.Assert; * Default {@link Select} implementation. * * @author Mark Paluch + * @since 1.1 */ class DefaultSelect implements Select { private final boolean distinct; - private final List selectList; + private final SelectList selectList; private final From from; private final long limit; private final long offset; @@ -39,10 +40,10 @@ class DefaultSelect implements Select { private final List orderBy; DefaultSelect(boolean distinct, List selectList, List

from, long limit, long offset, - List joins, @Nullable Condition where, List orderBy) { + List joins, @Nullable Condition where, List orderBy) { this.distinct = distinct; - this.selectList = new ArrayList<>(selectList); + this.selectList = new SelectList(new ArrayList<>(selectList)); this.from = new From(from); this.limit = limit; this.offset = offset; @@ -85,7 +86,7 @@ class DefaultSelect implements Select { visitor.enter(this); - selectList.forEach(it -> it.visit(visitor)); + selectList.visit(visitor); from.visit(visitor); joins.forEach(it -> it.visit(visitor)); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelectBuilder.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelectBuilder.java index 15dfb3f6..b5434f1b 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelectBuilder.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelectBuilder.java @@ -24,11 +24,13 @@ import org.springframework.data.relational.core.sql.Join.JoinType; import org.springframework.data.relational.core.sql.SelectBuilder.SelectAndFrom; import org.springframework.data.relational.core.sql.SelectBuilder.SelectFromAndJoin; import org.springframework.data.relational.core.sql.SelectBuilder.SelectWhereAndOr; +import org.springframework.lang.Nullable; /** * Default {@link SelectBuilder} implementation. * * @author Mark Paluch + * @since 1.1 */ class DefaultSelectBuilder implements SelectBuilder, SelectAndFrom, SelectFromAndJoin, SelectWhereAndOr { @@ -38,7 +40,7 @@ class DefaultSelectBuilder implements SelectBuilder, SelectAndFrom, SelectFromAn private long limit = -1; private long offset = -1; private List joins = new ArrayList<>(); - private Condition where; + private @Nullable Condition where; private List orderBy = new ArrayList<>(); /* @@ -273,7 +275,7 @@ class DefaultSelectBuilder implements SelectBuilder, SelectAndFrom, SelectFromAn private final DefaultSelectBuilder selectBuilder; private Expression from; private Expression to; - private Condition condition; + private @Nullable Condition condition; JoinBuilder(Table table, DefaultSelectBuilder selectBuilder) { this.table = table; @@ -314,12 +316,12 @@ class DefaultSelectBuilder implements SelectBuilder, SelectAndFrom, SelectFromAn } private void finishCondition() { - Equals equals = Equals.create(from, to); + Comparison comparison = Comparison.create(from, "=", to); if (condition == null) { - condition = equals; + condition = comparison; } else { - condition = condition.and(equals); + condition = condition.and(comparison); } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Expression.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Expression.java index 63a8b917..07e083ce 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Expression.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Expression.java @@ -19,9 +19,8 @@ package org.springframework.data.relational.core.sql; * Expression that can be used in select lists. * * @author Mark Paluch + * @since 1.1 * @see SQL * @see Expressions */ -public interface Expression extends Segment { - -} +public interface Expression extends Segment {} 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 580d6d4f..be56122a 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 @@ -19,6 +19,7 @@ package org.springframework.data.relational.core.sql; * Factory for common {@link Expression}s. * * @author Mark Paluch + * @since 1.1 * @see SQL * @see Conditions * @see Functions @@ -52,8 +53,7 @@ public abstract class Expressions { } // Utility constructor. - private Expressions() { - } + private Expressions() {} static class SimpleExpression extends AbstractSegment implements Expression { @@ -69,7 +69,3 @@ public abstract class Expressions { } } } - - - - diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/From.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/From.java index 777aeda0..6c6bfc40 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/From.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/From.java @@ -16,16 +16,15 @@ package org.springframework.data.relational.core.sql; import java.util.Arrays; -import java.util.Collections; import java.util.List; -import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** * {@code FROM} clause. * * @author Mark Paluch + * @since 1.1 */ public class From extends AbstractSegment { @@ -37,7 +36,7 @@ public class From extends AbstractSegment { From(List
tables) { - super(tables.toArray(new Table[]{})); + super(tables.toArray(new Table[] {})); this.tables = tables; } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Functions.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Functions.java index 6edf7ae2..651b5455 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Functions.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Functions.java @@ -25,6 +25,7 @@ import org.springframework.util.Assert; * Factory for common {@link Expression function expressions}. * * @author Mark Paluch + * @since 1.1 * @see SQL * @see Expressions * @see Functions @@ -42,7 +43,7 @@ public class Functions { Assert.notNull(columns, "Columns must not be null!"); Assert.notEmpty(columns, "Columns must contains at least one column"); - return new SimpleFunction("COUNT", Arrays.asList(columns)); + return SimpleFunction.create("COUNT", Arrays.asList(columns)); } /** @@ -55,7 +56,7 @@ public class Functions { Assert.notNull(columns, "Columns must not be null!"); - return new SimpleFunction("COUNT", new ArrayList<>(columns)); + return SimpleFunction.create("COUNT", new ArrayList<>(columns)); } // Utility constructor. diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/In.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/In.java index a66c3d81..23d72823 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/In.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/In.java @@ -15,24 +15,99 @@ */ package org.springframework.data.relational.core.sql; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + /** + * {@code IN} {@link Condition} clause. + * * @author Jens Schauder + * @author Mark Paluch + * @since 1.1 */ public class In extends AbstractSegment implements Condition { private final Expression left; - private final Expression right; + private final Collection expressions; - public In(Expression left, Expression right) { + private In(Expression left, Collection expressions) { - super(left, right); + super(toArray(left, expressions)); this.left = left; - this.right = right; + this.expressions = expressions; } + private static Segment[] toArray(Expression expression, Collection expressions) { + + Segment[] segments = new Segment[1 + expressions.size()]; + segments[0] = expression; + + int index = 1; + + for (Expression e : expressions) { + segments[index++] = e; + } + + return segments; + } + + /** + * Creates a new {@link In} {@link Condition} given left and right {@link Expression}s. + * + * @param columnOrExpression left hand side of the {@link Condition} must not be {@literal null}. + * @param arg right hand side (collection {@link Expression}) must not be {@literal null}. + * @return the {@link In} {@link Condition}. + */ + public static In create(Expression columnOrExpression, Expression arg) { + + Assert.notNull(columnOrExpression, "Comparison column or expression must not be null"); + Assert.notNull(arg, "Expression argument must not be null"); + + return new In(columnOrExpression, Collections.singletonList(arg)); + } + + /** + * Creates a new {@link In} {@link Condition} given left and right {@link Expression}s. + * + * @param columnOrExpression left hand side of the {@link Condition} must not be {@literal null}. + * @param expressions right hand side (collection {@link Expression}) must not be {@literal null}. + * @return the {@link In} {@link Condition}. + */ + public static In create(Expression columnOrExpression, Collection expressions) { + + Assert.notNull(columnOrExpression, "Comparison column or expression must not be null"); + Assert.notNull(expressions, "Expression argument must not be null"); + + return new In(columnOrExpression, new ArrayList<>(expressions)); + } + + /** + * Creates a new {@link In} {@link Condition} given left and right {@link Expression}s. + * + * @param columnOrExpression left hand side of the {@link Condition} must not be {@literal null}. + * @param expressions right hand side (collection {@link Expression}) must not be {@literal null}. + * @return the {@link In} {@link Condition}. + */ + public static In create(Expression columnOrExpression, Expression... expressions) { + + Assert.notNull(columnOrExpression, "Comparison column or expression must not be null"); + Assert.notNull(expressions, "Expression argument must not be null"); + + return new In(columnOrExpression, Arrays.asList(expressions)); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ @Override public String toString() { - return left + " IN " + right; + return left + " IN (" + StringUtils.collectionToDelimitedString(expressions, ", ") + ")"; } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/IsNull.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/IsNull.java index 88e7d29c..99d950c6 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/IsNull.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/IsNull.java @@ -15,16 +15,24 @@ */ package org.springframework.data.relational.core.sql; +import org.springframework.util.Assert; + /** + * {@code IS NULL} {@link Condition}. + * * @author Jens Schauder + * @since 1.1 */ public class IsNull extends AbstractSegment implements Condition { private final Expression expression; - private final boolean negated; - public IsNull(Expression expression, boolean negated) { + private IsNull(Expression expression) { + this(expression, false); + } + + private IsNull(Expression expression, boolean negated) { super(expression); @@ -32,21 +40,38 @@ public class IsNull extends AbstractSegment implements Condition { this.negated = negated; } - public IsNull(Expression expression) { - this(expression, false); + /** + * Creates a new {@link IsNull} expression. + * + * @param expression must not be {@literal null}. + * @return + */ + public static IsNull create(Expression expression) { + + Assert.notNull(expression, "Expression must not be null"); + + return new IsNull(expression); } + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.Condition#not() + */ @Override public Condition not() { return new IsNull(expression, !negated); } + public boolean isNegated() { + return negated; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ @Override public String toString() { return expression + (negated ? " IS NOT NULL" : " IS NULL"); } - - public boolean isNegated() { - return negated; - } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Join.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Join.java index 58c6b98a..867d29e2 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Join.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Join.java @@ -19,10 +19,12 @@ package org.springframework.data.relational.core.sql; * {@link Segment} for a {@code JOIN} declaration. *

* Renders to: {@code JOIN - *

+ * +
* ON }. * * @author Mark Paluch + * @since 1.1 */ public class Join extends AbstractSegment { diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Equals.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Like.java similarity index 56% rename from spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Equals.java rename to spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Like.java index 5d077625..1a6c3ab3 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Equals.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Like.java @@ -18,18 +18,19 @@ package org.springframework.data.relational.core.sql; import org.springframework.util.Assert; /** - * Equals to {@link Condition} comparing two {@link Expression}s. + * LIKE {@link Condition} comparing two {@link Expression}s. *

- * Results in a rendered condition: {@code = }. + * Results in a rendered condition: {@code LIKE }. * * @author Mark Paluch + * @since 1.1 */ -public class Equals extends AbstractSegment implements Condition { +public class Like extends AbstractSegment implements Condition { private final Expression left; private final Expression right; - Equals(Expression left, Expression right) { + private Like(Expression left, Expression right) { super(left, right); @@ -38,18 +39,18 @@ public class Equals extends AbstractSegment implements Condition { } /** - * Creates a new {@link Equals} {@link Condition} given two {@link Expression}s. + * Creates a new {@link Like} {@link Condition} given two {@link Expression}s. * - * @param left the left {@link Expression}. - * @param right the right {@link Expression}. - * @return the {@link Equals} condition. + * @param leftColumnOrExpression the left {@link Expression}. + * @param rightColumnOrExpression the right {@link Expression}. + * @return the {@link Like} condition. */ - public static Equals create(Expression left, Expression right) { + public static Like create(Expression leftColumnOrExpression, Expression rightColumnOrExpression) { - Assert.notNull(left, "Left expression must not be null!"); - Assert.notNull(right, "Right expression must not be null!"); + Assert.notNull(leftColumnOrExpression, "Left expression must not be null!"); + Assert.notNull(rightColumnOrExpression, "Right expression must not be null!"); - return new Equals(left, right); + return new Like(leftColumnOrExpression, rightColumnOrExpression); } /** @@ -68,6 +69,6 @@ public class Equals extends AbstractSegment implements Condition { @Override public String toString() { - return left.toString() + " = " + right.toString(); + return left.toString() + " LIKE " + right.toString(); } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/MultipleCondition.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/MultipleCondition.java index 107e8dbb..82077288 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/MultipleCondition.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/MultipleCondition.java @@ -19,10 +19,11 @@ import java.util.Arrays; import java.util.List; import java.util.StringJoiner; -import org.springframework.util.Assert; - /** + * Wrapper for multiple {@link Condition}s. + * * @author Jens Schauder + * @since 1.1 */ public abstract class MultipleCondition extends AbstractSegment implements Condition { diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Named.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Named.java index a205e10b..44f7449c 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Named.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Named.java @@ -19,6 +19,7 @@ package org.springframework.data.relational.core.sql; * Named element exposing a {@link #getName() name}. * * @author Mark Paluch + * @since 1.1 */ public interface Named { diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Not.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Not.java index 993d5972..a691b6dd 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Not.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Not.java @@ -17,23 +17,32 @@ package org.springframework.data.relational.core.sql; /** * @author Jens Schauder + * @since 1.1 */ public class Not extends AbstractSegment implements Condition { private final Condition condition; - public Not(Condition condition) { + Not(Condition condition) { super(condition); this.condition = condition; } + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.Condition#not() + */ @Override public Condition not() { return condition; } + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ @Override public String toString() { return "NOT " + condition.toString(); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/OrCondition.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/OrCondition.java index 7886d978..94e872fa 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/OrCondition.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/OrCondition.java @@ -19,6 +19,7 @@ package org.springframework.data.relational.core.sql; * {@link Condition} representing an {@code OR} relation between two {@link Condition}s. * * @author Mark Paluch + * @since 1.1 * @see Condition#or(Condition) */ public class OrCondition extends MultipleCondition { diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/OrderByField.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/OrderByField.java index 187f9c16..ae09b5fa 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/OrderByField.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/OrderByField.java @@ -22,7 +22,10 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** + * Represents a field in the {@code ORDER BY} clause. + * * @author Mark Paluch + * @since 1.1 */ public class OrderByField extends AbstractSegment { @@ -30,7 +33,7 @@ public class OrderByField extends AbstractSegment { private final @Nullable Sort.Direction direction; private final Sort.NullHandling nullHandling; - OrderByField(Expression expression, Direction direction, NullHandling nullHandling) { + private OrderByField(Expression expression, @Nullable Direction direction, NullHandling nullHandling) { super(expression); Assert.notNull(expression, "Order by expression must not be null"); @@ -41,18 +44,42 @@ public class OrderByField extends AbstractSegment { this.nullHandling = nullHandling; } + /** + * Creates a new {@link OrderByField} from a {@link Column} applying default ordering. + * + * @param column must not be {@literal null}. + * @return the {@link OrderByField}. + */ public static OrderByField from(Column column) { return new OrderByField(column, null, NullHandling.NATIVE); } + /** + * Creates a new {@link OrderByField} from a the current one using ascending sorting. + * + * @return the new {@link OrderByField} with ascending sorting. + * @see #desc() + */ public OrderByField asc() { - return new OrderByField(expression, Direction.ASC, NullHandling.NATIVE); + return new OrderByField(expression, Direction.ASC, nullHandling); } + /** + * Creates a new {@link OrderByField} from a the current one using descending sorting. + * + * @return the new {@link OrderByField} with descending sorting. + * @see #asc() + */ public OrderByField desc() { - return new OrderByField(expression, Direction.DESC, NullHandling.NATIVE); + return new OrderByField(expression, Direction.DESC, nullHandling); } + /** + * Creates a new {@link OrderByField} with {@link NullHandling} applied. + * + * @param nullHandling must not be {@literal null}. + * @return the new {@link OrderByField} with {@link NullHandling} applied. + */ public OrderByField withNullHandling(NullHandling nullHandling) { return new OrderByField(expression, direction, nullHandling); } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SQL.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SQL.java index 369727fe..0f45a863 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SQL.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SQL.java @@ -16,53 +16,24 @@ package org.springframework.data.relational.core.sql; import org.springframework.data.relational.core.sql.BindMarker.NamedBindMarker; -import org.springframework.data.relational.core.sql.SelectBuilder.SelectAndFrom; import org.springframework.util.Assert; /** - * Utility to create SQL {@link Segment}s. Typically used as entry point to the Query Builder AST. - * Objects and dependent objects created by the Query AST are immutable except for builders. - *

The Query Builder API is intended for framework usage to produce SQL required for framework operations. + * Utility to create SQL {@link Segment}s. Typically used as entry point to the Statement Builder. Objects and dependent + * objects created by the Query AST are immutable except for builders. + *

+ * The Statement Builder API is intended for framework usage to produce SQL required for framework operations. * * @author Mark Paluch + * @author Jens Schauder + * @since 1.1 * @see Expressions * @see Conditions * @see Functions + * @see StatementBuilder */ public abstract class SQL { - /** - * Creates a new {@link SelectBuilder} by specifying a {@code SELECT} column. - * - * @param expression the select list expression. - * @return the {@link SelectBuilder} containing {@link Expression}. - * @see SelectBuilder#select(Expression) - */ - public static SelectAndFrom newSelect(Expression expression) { - return Select.builder().select(expression); - } - - /** - * Creates a new {@link SelectBuilder} by specifying one or more {@code SELECT} columns. - * - * @param expressions the select list expressions. - * @return the {@link SelectBuilder} containing {@link Expression}s. - * @see SelectBuilder#select(Expression...) - */ - public static SelectAndFrom newSelect(Expression... expressions) { - return Select.builder().select(expressions); - } - - /** - * Creates a new {@link SelectBuilder}. - * - * @return the new {@link SelectBuilder}. - * @see SelectBuilder - */ - public static SelectBuilder select() { - return Select.builder(); - } - /** * Creates a new {@link Column} associated with a source {@link Table}. * @@ -107,6 +78,5 @@ public abstract class SQL { } // Utility constructor. - private SQL() { - } + private SQL() {} } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Segment.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Segment.java index 14ec424d..138d7cf0 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Segment.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Segment.java @@ -16,9 +16,11 @@ package org.springframework.data.relational.core.sql; /** - * Supertype of all Abstract Syntax Tree (AST) segments. Segments are typically immutable and mutator methods return new instances instead of changing the called instance. + * Supertype of all Abstract Syntax Tree (AST) segments. Segments are typically immutable and mutator methods return new + * instances instead of changing the called instance. * * @author Mark Paluch + * @since 1.1 */ public interface Segment extends Visitable { @@ -28,8 +30,7 @@ public interface Segment extends Visitable { * Equality is typically given if the {@link #toString()} representation matches. * * @param other the reference object with which to compare. - * @return {@literal true} if this object is the same as the {@code other} - * argument; {@literal false} otherwise. + * @return {@literal true} if this object is the same as the {@code other} argument; {@literal false} otherwise. */ @Override boolean equals(Object other); @@ -37,7 +38,8 @@ public interface Segment extends Visitable { /** * Generate a hash code from this{@link Segment}. *

- * Hashcode typically derives from the {@link #toString()} representation so two {@link Segment}s yield the same {@link #hashCode()} if their {@link #toString()} representation matches. + * Hashcode typically derives from the {@link #toString()} representation so two {@link Segment}s yield the same + * {@link #hashCode()} if their {@link #toString()} representation matches. * * @return a hash code value for this object. */ @@ -47,7 +49,9 @@ public interface Segment extends Visitable { /** * Return a SQL string representation of this {@link Segment}. *

- * The representation is intended for debugging purposes and an approximation to the generated SQL. While it might work in the context of a specific dialect, you should not that the {@link #toString()} representation works across multiple databases. + * The representation is intended for debugging purposes and an approximation to the generated SQL. While it might + * work in the context of a specific dialect, you should not that the {@link #toString()} representation works across + * multiple databases. * * @return a SQL string representation of this {@link Segment}. */ diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Select.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Select.java index d225364a..a45c01b8 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Select.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Select.java @@ -18,11 +18,10 @@ package org.springframework.data.relational.core.sql; import java.util.OptionalLong; /** - * AST for a {@code SELECT} statement. - * Visiting order: + * AST for a {@code SELECT} statement. Visiting order: *

    *
  1. Self
  2. - *
  3. {@link Column SELECT columns}
  4. + *
  5. {@link Column SELECT columns}
  6. *
  7. {@link Table FROM tables} clause
  8. *
  9. {@link Join JOINs}
  10. *
  11. {@link Condition WHERE} condition
  12. @@ -30,12 +29,13 @@ import java.util.OptionalLong; *
* * @author Mark Paluch + * @since 1.1 + * @see StatementBuilder * @see SelectBuilder * @see SQL */ public interface Select extends Segment, Visitable { - /** * Creates a new {@link SelectBuilder}. * diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SelectBuilder.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SelectBuilder.java index 4825584a..b1ea5ceb 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SelectBuilder.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SelectBuilder.java @@ -21,6 +21,7 @@ import java.util.Collection; * Entry point to construct a {@link Select} statement. * * @author Mark Paluch + * @since 1.1 */ public interface SelectBuilder { @@ -67,12 +68,13 @@ public interface SelectBuilder { SelectAndFrom distinct(); /** - * Builder exposing {@code select} and {@code from} methods. + * Builder exposing {@code SELECT} and {@code FROM} methods. */ interface SelectAndFrom extends SelectFrom { /** - * Include a {@link Expression} in the select list. Multiple calls to this or other {@code select} methods keep adding items to the select list and do not replace previously contained items. + * Include a {@link Expression} in the select list. Multiple calls to this or other {@code select} methods keep + * adding items to the select list and do not replace previously contained items. * * @param expression the expression to include. * @return {@code this} builder. @@ -81,7 +83,8 @@ public interface SelectBuilder { SelectFrom select(Expression expression); /** - * Include one or more {@link Expression}s in the select list. Multiple calls to this or other {@code select} methods keep adding items to the select list and do not replace previously contained items. + * Include one or more {@link Expression}s in the select list. Multiple calls to this or other {@code select} + * methods keep adding items to the select list and do not replace previously contained items. * * @param expressions the expressions to include. * @return {@code this} builder. @@ -90,7 +93,8 @@ public interface SelectBuilder { SelectFrom select(Expression... expressions); /** - * Include one or more {@link Expression}s in the select list. Multiple calls to this or other {@code select} methods keep adding items to the select list and do not replace previously contained items. + * Include one or more {@link Expression}s in the select list. Multiple calls to this or other {@code select} + * methods keep adding items to the select list and do not replace previously contained items. * * @param expressions the expressions to include. * @return {@code this} builder. @@ -106,8 +110,8 @@ public interface SelectBuilder { SelectAndFrom distinct(); /** - * Declare a {@link Table} to {@code SELECT … FROM}. - * Multiple calls to this or other {@code from} methods keep adding items to the select list and do not replace previously contained items. + * Declare a {@link Table} to {@code SELECT … FROM}. Multiple calls to this or other {@code from} methods keep + * adding items to the select list and do not replace previously contained items. * * @param table the table to {@code SELECT … FROM} must not be {@literal null}. * @return {@code this} builder. @@ -118,8 +122,8 @@ public interface SelectBuilder { SelectFromAndJoin from(Table table); /** - * Declare one or more {@link Table}s to {@code SELECT … FROM}. - * Multiple calls to this or other {@code from} methods keep adding items to the select list and do not replace previously contained items. + * Declare one or more {@link Table}s to {@code SELECT … FROM}. Multiple calls to this or other {@code from} methods + * keep adding items to the select list and do not replace previously contained items. * * @param tables the tables to {@code SELECT … FROM} must not be {@literal null}. * @return {@code this} builder. @@ -130,8 +134,8 @@ public interface SelectBuilder { SelectFromAndJoin from(Table... tables); /** - * Declare one or more {@link Table}s to {@code SELECT … FROM}. - * Multiple calls to this or other {@code from} methods keep adding items to the select list and do not replace previously contained items. + * Declare one or more {@link Table}s to {@code SELECT … FROM}. Multiple calls to this or other {@code from} methods + * keep adding items to the select list and do not replace previously contained items. * * @param tables the tables to {@code SELECT … FROM} must not be {@literal null}. * @return {@code this} builder. @@ -143,13 +147,13 @@ public interface SelectBuilder { } /** - * Builder exposing {@code from} methods. + * Builder exposing {@code FROM} methods. */ interface SelectFrom extends BuildSelect { /** - * Declare a {@link Table} to {@code SELECT … FROM}. - * Multiple calls to this or other {@code from} methods keep adding items to the select list and do not replace previously contained items. + * Declare a {@link Table} to {@code SELECT … FROM}. Multiple calls to this or other {@code from} methods keep + * adding items to the select list and do not replace previously contained items. * * @param table the table name to {@code SELECT … FROM} must not be {@literal null} or empty. * @return {@code this} builder. @@ -159,8 +163,8 @@ public interface SelectBuilder { SelectFromAndOrderBy from(String table); /** - * Declare a {@link Table} to {@code SELECT … FROM}. - * Multiple calls to this or other {@code from} methods keep adding items to the select list and do not replace previously contained items. + * Declare a {@link Table} to {@code SELECT … FROM}. Multiple calls to this or other {@code from} methods keep + * adding items to the select list and do not replace previously contained items. * * @param table the table to {@code SELECT … FROM} must not be {@literal null}. * @return {@code this} builder. @@ -170,8 +174,8 @@ public interface SelectBuilder { SelectFromAndOrderBy from(Table table); /** - * Declare one or more {@link Table}s to {@code SELECT … FROM}. - * Multiple calls to this or other {@code from} methods keep adding items to the select list and do not replace previously contained items. + * Declare one or more {@link Table}s to {@code SELECT … FROM}. Multiple calls to this or other {@code from} methods + * keep adding items to the select list and do not replace previously contained items. * * @param tables the tables to {@code SELECT … FROM} must not be {@literal null}. * @return {@code this} builder. @@ -181,8 +185,8 @@ public interface SelectBuilder { SelectFromAndOrderBy from(Table... tables); /** - * Declare one or more {@link Table}s to {@code SELECT … FROM}. - * Multiple calls to this or other {@code from} methods keep adding items to the select list and do not replace previously contained items. + * Declare one or more {@link Table}s to {@code SELECT … FROM}. Multiple calls to this or other {@code from} methods + * keep adding items to the select list and do not replace previously contained items. * * @param tables the tables to {@code SELECT … FROM} must not be {@literal null}. * @return {@code this} builder. @@ -193,7 +197,7 @@ public interface SelectBuilder { } /** - * Builder exposing {@code from} and {@code order by} methods. + * Builder exposing {@code FROM}, {@code JOIN}, {@code WHERE} and {@code LIMIT/OFFSET} methods. */ interface SelectFromAndOrderBy extends SelectFrom, SelectOrdered, SelectLimitOffset, BuildSelect { @@ -228,11 +232,14 @@ public interface SelectBuilder { SelectFromAndOrderBy orderBy(Collection orderByFields); } + /** + * Builder exposing {@code FROM}, {@code JOIN}, {@code WHERE} and {@code LIMIT/OFFSET} methods. + */ interface SelectFromAndJoin extends SelectFromAndOrderBy, BuildSelect, SelectJoin, SelectWhere, SelectLimitOffset { /** - * Declare a {@link Table} to {@code SELECT … FROM}. - * Multiple calls to this or other {@code from} methods keep adding items to the select list and do not replace previously contained items. + * Declare a {@link Table} to {@code SELECT … FROM}. Multiple calls to this or other {@code from} methods keep + * adding items to the select list and do not replace previously contained items. * * @param table the table to {@code SELECT … FROM} must not be {@literal null}. * @return {@code this} builder. @@ -243,8 +250,8 @@ public interface SelectBuilder { SelectFromAndJoin from(Table table); /** - * Declare one or more {@link Table}s to {@code SELECT … FROM}. - * Multiple calls to this or other {@code from} methods keep adding items to the select list and do not replace previously contained items. + * Declare one or more {@link Table}s to {@code SELECT … FROM}. Multiple calls to this or other {@code from} methods + * keep adding items to the select list and do not replace previously contained items. * * @param tables the tables to {@code SELECT … FROM} must not be {@literal null}. * @return {@code this} builder. @@ -255,8 +262,8 @@ public interface SelectBuilder { SelectFromAndJoin from(Table... tables); /** - * Declare one or more {@link Table}s to {@code SELECT … FROM}. - * Multiple calls to this or other {@code from} methods keep adding items to the select list and do not replace previously contained items. + * Declare one or more {@link Table}s to {@code SELECT … FROM}. Multiple calls to this or other {@code from} methods + * keep adding items to the select list and do not replace previously contained items. * * @param tables the tables to {@code SELECT … FROM} must not be {@literal null}. * @return {@code this} builder. @@ -267,8 +274,8 @@ public interface SelectBuilder { SelectFromAndJoin from(Collection tables); /** - * Apply {@code limit} and {@code offset} parameters to the select statement. - * To read the first 20 rows from start use {@code limitOffset(20, 0)}. to read the next 20 use {@code limitOffset(20, 20)}. + * Apply {@code limit} and {@code offset} parameters to the select statement. To read the first 20 rows from start + * use {@code limitOffset(20, 0)}. to read the next 20 use {@code limitOffset(20, 20)}. * * @param limit rows to read. * @param offset row offset, zero-based. @@ -294,13 +301,14 @@ public interface SelectBuilder { } /** - * Builder exposing join/where/and {@code JOIN … ON} continuation methods. + * Builder exposing {@code FROM}, {@code WHERE}, {@code LIMIT/OFFSET}, and JOIN {@code AND} continuation methods. */ - interface SelectFromAndJoinCondition extends BuildSelect, SelectJoin, SelectWhere, SelectOnCondition, SelectLimitOffset { + interface SelectFromAndJoinCondition + extends BuildSelect, SelectJoin, SelectWhere, SelectOnCondition, SelectLimitOffset { /** - * Apply {@code limit} and {@code offset} parameters to the select statement. - * To read the first 20 rows from start use {@code limitOffset(20, 0)}. to read the next 20 use {@code limitOffset(20, 20)}. + * Apply {@code limit} and {@code offset} parameters to the select statement. To read the first 20 rows from start + * use {@code limitOffset(20, 0)}. to read the next 20 use {@code limitOffset(20, 20)}. * * @param limit rows to read. * @param offset row offset, zero-based. @@ -331,8 +339,8 @@ public interface SelectBuilder { interface SelectLimitOffset { /** - * Apply {@code limit} and {@code offset} parameters to the select statement. - * To read the first 20 rows from start use {@code limitOffset(20, 0)}. to read the next 20 use {@code limitOffset(20, 20)}. + * Apply {@code limit} and {@code offset} parameters to the select statement. To read the first 20 rows from start + * use {@code limitOffset(20, 0)}. to read the next 20 use {@code limitOffset(20, 20)}. * * @param limit rows to read. * @param offset row offset, zero-based. @@ -404,7 +412,7 @@ public interface SelectBuilder { } /** - * Interface exposing {@code AND}/{@code OR} combinatior methods for {@code WHERE} {@link Condition}s. + * Interface exposing {@code AND}/{@code OR} combinator methods for {@code WHERE} {@link Condition}s. */ interface SelectWhereAndOr extends SelectOrdered, BuildSelect { @@ -458,7 +466,6 @@ public interface SelectBuilder { */ interface SelectOn { - /** * Declare the source column in the {@code JOIN}. * @@ -505,7 +512,8 @@ public interface SelectBuilder { interface BuildSelect { /** - * Build the {@link Select} statement and verify basic relationship constraints such as all referenced columns have a {@code FROM} or {@code JOIN} table import. + * Build the {@link Select} statement and verify basic relationship constraints such as all referenced columns have + * a {@code FROM} or {@code JOIN} table import. * * @return the build and immutable {@link Select} statement. */ diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SelectList.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SelectList.java new file mode 100644 index 00000000..1e8681ce --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SelectList.java @@ -0,0 +1,45 @@ +/* + * Copyright 2019 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 + * + * http://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 java.util.List; + +import org.springframework.util.StringUtils; + +/** + * Value object representing the select list (selected columns, functions). + * + * @author Mark Paluch + * @since 1.1 + */ +public class SelectList extends AbstractSegment { + + private final List selectList; + + SelectList(List selectList) { + super(selectList.toArray(new Expression[0])); + this.selectList = selectList; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return StringUtils.collectionToDelimitedString(selectList, ", "); + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SelectValidator.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SelectValidator.java index f760b019..e1f92578 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SelectValidator.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SelectValidator.java @@ -19,7 +19,13 @@ import java.util.HashSet; import java.util.Set; /** + * Validator for {@link Select} statements. + *

+ * Validates that all {@link Column}s using a table qualifier have a table import from either the {@code FROM} or + * {@code JOIN} clause. + * * @author Mark Paluch + * @since 1.1 */ class SelectValidator implements Visitor { @@ -81,8 +87,7 @@ class SelectValidator implements Visitor { selectFieldCount++; } - if (segment instanceof Column - && (parent instanceof Select || parent instanceof SimpleFunction)) { + if (segment instanceof Column && (parent instanceof Select || parent instanceof SimpleFunction)) { selectFieldCount++; Table table = ((Column) segment).getTable(); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SimpleCondition.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SimpleCondition.java index 6a0d7853..517029ed 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SimpleCondition.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SimpleCondition.java @@ -15,12 +15,11 @@ */ package org.springframework.data.relational.core.sql; -import org.springframework.util.Assert; - /** * Simple condition consisting of {@link Expression}, {@code comparator} and {@code predicate}. * * @author Mark Paluch + * @since 1.1 */ public class SimpleCondition extends AbstractSegment implements Condition { diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SimpleFunction.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SimpleFunction.java index b7ac738c..c5c7d31a 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SimpleFunction.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SimpleFunction.java @@ -24,20 +24,36 @@ import org.springframework.util.StringUtils; * Simple function accepting one or more {@link Expression}s. * * @author Mark Paluch + * @since 1.1 */ public class SimpleFunction extends AbstractSegment implements Expression { private String functionName; private List expressions; - SimpleFunction(String functionName, List expressions) { + private SimpleFunction(String functionName, List expressions) { - super(expressions.toArray(new Expression[]{})); + super(expressions.toArray(new Expression[0])); this.functionName = functionName; this.expressions = expressions; } + /** + * Creates a new {@link SimpleFunction} given {@code functionName} and {@link List} of {@link Expression}s. + * + * @param functionName must not be {@literal null}. + * @param expressions zero or many {@link Expression}s, must not be {@literal null}. + * @return + */ + public static SimpleFunction create(String functionName, List expressions) { + + Assert.hasText(functionName, "Function name must not be null or empty"); + Assert.notNull(expressions, "Expressions name must not be null"); + + return new SimpleFunction(functionName, expressions); + } + /** * Expose this function result under a column {@code alias}. * @@ -79,6 +95,10 @@ public class SimpleFunction extends AbstractSegment implements Expression { this.alias = alias; } + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.Aliased#getAlias() + */ @Override public String getAlias() { return alias; diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SimpleSegment.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SimpleSegment.java index ba705787..58a37a37 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SimpleSegment.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SimpleSegment.java @@ -17,12 +17,13 @@ package org.springframework.data.relational.core.sql; /** * @author Mark Paluch + * @since 1.1 */ public class SimpleSegment extends AbstractSegment { private final String sql; - public SimpleSegment(String sql) { + SimpleSegment(String sql) { this.sql = sql; } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/StatementBuilder.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/StatementBuilder.java new file mode 100644 index 00000000..91168860 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/StatementBuilder.java @@ -0,0 +1,80 @@ +/* + * Copyright 2019 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 + * + * http://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 java.util.Collection; + +import org.springframework.data.relational.core.sql.SelectBuilder.SelectAndFrom; + +/** + * Entrypoint to build SQL statements. + * + * @author Mark Paluch + * @since 1.1 + * @see SQL + * @see Expressions + * @see Conditions + * @see Functions + */ +public abstract class StatementBuilder { + + /** + * Creates a new {@link SelectBuilder} by specifying a {@code SELECT} column. + * + * @param expression the select list expression. + * @return the {@link SelectBuilder} containing {@link Expression}. + * @see SelectBuilder#select(Expression) + */ + public static SelectAndFrom select(Expression expression) { + return Select.builder().select(expression); + } + + /** + * Creates a new {@link SelectBuilder} by specifying one or more {@code SELECT} columns. + * + * @param expressions the select list expressions. + * @return the {@link SelectBuilder} containing {@link Expression}s. + * @see SelectBuilder#select(Expression...) + */ + public static SelectAndFrom select(Expression... expressions) { + return Select.builder().select(expressions); + } + + /** + * Include one or more {@link Expression}s in the select list. + * + * @param expressions the expressions to include. + * @return {@code this} builder. + * @see Table#columns(String...) + */ + public static SelectAndFrom select(Collection expressions) { + return Select.builder().select(expressions); + } + + /** + * Creates a new {@link SelectBuilder}. + * + * @return the new {@link SelectBuilder}. + * @see SelectBuilder + */ + public static SelectBuilder select() { + return Select.builder(); + } + + private StatementBuilder() { + + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SubselectExpression.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SubselectExpression.java index 7671ba89..1b5b61ed 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SubselectExpression.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SubselectExpression.java @@ -16,19 +16,26 @@ package org.springframework.data.relational.core.sql; /** + * Wrapper for a {@link Select} query to be used as subselect. + * * @author Jens Schauder + * @since 1.1 */ public class SubselectExpression extends AbstractSegment implements Expression { private final Select subselect; - public SubselectExpression(Select subselect) { + SubselectExpression(Select subselect) { super(subselect); this.subselect = subselect; } + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ @Override public String toString() { return "(" + subselect.toString() + ")"; diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Table.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Table.java index aaa7fc6c..357cc5d6 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Table.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Table.java @@ -16,6 +16,8 @@ package org.springframework.data.relational.core.sql; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.List; import org.springframework.util.Assert; @@ -27,6 +29,7 @@ import org.springframework.util.Assert; * Renders to: {@code } or {@code AS }. * * @author Mark Paluch + * @since 1.1 */ public class Table extends AbstractSegment { @@ -66,7 +69,7 @@ public class Table extends AbstractSegment { } /** - * Create a new {@link Table} aliased to {@code alias}. + * Creates a new {@link Table} aliased to {@code alias}. * * @param alias must not be {@literal null} or empty. * @return the new {@link Table} using the {@code alias}. @@ -79,7 +82,7 @@ public class Table extends AbstractSegment { } /** - * Create a new {@link Column} associated with this {@link Table}. + * Creates a new {@link Column} associated with this {@link Table}. *

* Note: This {@link Table} does not track column creation and there is no possibility to enumerate all * {@link Column}s that were created for this table. @@ -95,7 +98,7 @@ public class Table extends AbstractSegment { } /** - * Create a {@link List} of {@link Column}s associated with this {@link Table}. + * Creates a {@link List} of {@link Column}s associated with this {@link Table}. *

* Note: This {@link Table} does not track column creation and there is no possibility to enumerate all * {@link Column}s that were created for this table. @@ -107,6 +110,22 @@ public class Table extends AbstractSegment { Assert.notNull(names, "Names must not be null"); + return columns(Arrays.asList(names)); + } + + /** + * Creates a {@link List} of {@link Column}s associated with this {@link Table}. + *

+ * Note: This {@link Table} does not track column creation and there is no possibility to enumerate all + * {@link Column}s that were created for this table. + * + * @param names column names, must not be {@literal null} or empty. + * @return a new {@link List} of {@link Column}s associated with this {@link Table}. + */ + public List columns(Collection names) { + + Assert.notNull(names, "Names must not be null"); + List columns = new ArrayList<>(); for (String name : names) { columns.add(column(name)); @@ -117,7 +136,8 @@ public class Table extends AbstractSegment { /** * Creates a {@link AsteriskFromTable} maker selecting all columns from this {@link Table} (e.g. {@code SELECT - *

+ * +
* .*}. * * @return the select all marker for this {@link Table}. diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Visitable.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Visitable.java index 2ac7f989..4ebb993e 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Visitable.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Visitable.java @@ -21,12 +21,13 @@ import org.springframework.util.Assert; * Interface for implementations that wish to be visited by a {@link Visitor}. * * @author Mark Paluch + * @since 1.1 * @see Visitor */ public interface Visitable { /** - * Accept a {@link Visitor} visiting this {@link Segment} and its nested {@link Segment}s if applicable. + * Accept a {@link Visitor} visiting this {@link Visitable} and its nested {@link Visitable}s if applicable. * * @param visitor the visitor to notify, must not be {@literal null}. */ diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Visitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Visitor.java index 6d6eff53..0b6d9591 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Visitor.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Visitor.java @@ -16,26 +16,26 @@ package org.springframework.data.relational.core.sql; /** - * AST {@link Segment} visitor. Visitor methods get called by segments on entering a {@link Segment}, their child {@link Segment}s and on leaving the {@link Segment}. + * AST {@link Segment} visitor. Visitor methods get called by segments on entering a {@link Visitable}, their child + * {@link Visitable}s and on leaving the {@link Visitable}. * * @author Mark Paluch + * @since 1.1 */ @FunctionalInterface public interface Visitor { /** - * Enter a {@link Segment}. + * Enter a {@link Visitable}. * * @param segment the segment to visit. */ void enter(Visitable segment); /** - * Leave a {@link Segment}. + * Leave a {@link Visitable}. * * @param segment the visited segment. */ - default void leave(Visitable segment) { - - } + default void leave(Visitable segment) {} } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Where.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Where.java index 42cc266f..e6d0b34d 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Where.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Where.java @@ -15,12 +15,11 @@ */ package org.springframework.data.relational.core.sql; -import org.springframework.util.Assert; - /** * {@code Where} clause. * * @author Mark Paluch + * @since 1.1 */ public class Where extends AbstractSegment { @@ -33,6 +32,10 @@ public class Where extends AbstractSegment { this.condition = condition; } + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ @Override public String toString() { return "WHERE " + condition.toString(); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/package-info.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/package-info.java index 62f340aa..a6801860 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/package-info.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/package-info.java @@ -1,24 +1,16 @@ -/* - * Copyright 2019 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 - * - * http://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. - */ /** - * Query Builder AST. Use {@link org.springframework.data.relational.core.sql.SQL} as entry point to create SQL objects. Objects and dependent objects created by the Query AST are immutable except for builders. - *

The Query Builder API is intended for framework usage to produce SQL required for framework operations. + * Statement Builder implementation. Use {@link org.springframework.data.relational.core.sql.StatementBuilder} to create + * statements and {@link org.springframework.data.relational.core.sql.SQL} to create SQL objects. Objects and dependent + * objects created by the Statement Builder are immutable except for builders. + *

+ * The Statement Builder API is intended for framework usage to produce SQL required for framework operations. + * + * @since 1.1 */ @NonNullApi +@NonNullFields package org.springframework.data.relational.core.sql; import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ComparisonVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ComparisonVisitor.java new file mode 100644 index 00000000..cd5f882e --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ComparisonVisitor.java @@ -0,0 +1,100 @@ +/* + * Copyright 2019 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 + * + * http://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 org.springframework.data.relational.core.sql.Comparison; +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.lang.Nullable; + +/** + * {@link org.springframework.data.relational.core.sql.Visitor} rendering comparison {@link Condition}. Uses a + * {@link RenderTarget} to call back for render results. + * + * @author Mark Paluch + * @author Jens Schauder + * @since 1.1 + * @see Comparison + */ +class ComparisonVisitor extends FilteredSubtreeVisitor { + + private final RenderContext context; + private final Comparison condition; + private final RenderTarget target; + private final StringBuilder part = new StringBuilder(); + private @Nullable PartRenderer current; + + ComparisonVisitor(RenderContext context, Comparison condition, RenderTarget target) { + super(it -> it == condition); + this.condition = condition; + this.target = target; + this.context = context; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.FilteredSubtreeVisitor#enterNested(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + Delegation enterNested(Visitable segment) { + + if (segment instanceof Expression) { + ExpressionVisitor visitor = new ExpressionVisitor(context); + current = visitor; + return Delegation.delegateTo(visitor); + } + + if (segment instanceof Condition) { + ConditionVisitor visitor = new ConditionVisitor(context); + current = visitor; + return Delegation.delegateTo(visitor); + } + + throw new IllegalStateException("Cannot provide visitor for " + segment); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.FilteredSubtreeVisitor#leaveNested(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + Delegation leaveNested(Visitable segment) { + + if (current != null) { + if (part.length() != 0) { + part.append(' ').append(condition.getComparator()).append(' '); + } + + part.append(current.getRenderedPart()); + current = null; + } + + return super.leaveNested(segment); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.FilteredSubtreeVisitor#leaveMatched(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + Delegation leaveMatched(Visitable segment) { + + target.onRendered(part); + + return super.leaveMatched(segment); + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ConditionVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ConditionVisitor.java new file mode 100644 index 00000000..a6b22eec --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ConditionVisitor.java @@ -0,0 +1,100 @@ +/* + * Copyright 2019 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 + * + * http://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 org.springframework.data.relational.core.sql.AndCondition; +import org.springframework.data.relational.core.sql.Comparison; +import org.springframework.data.relational.core.sql.Condition; +import org.springframework.data.relational.core.sql.In; +import org.springframework.data.relational.core.sql.IsNull; +import org.springframework.data.relational.core.sql.Like; +import org.springframework.data.relational.core.sql.OrCondition; +import org.springframework.lang.Nullable; + +/** + * {@link org.springframework.data.relational.core.sql.Visitor} delegating {@link Condition} rendering to condition + * {@link org.springframework.data.relational.core.sql.Visitor}s. + * + * @author Mark Paluch + * @author Jens Schauder + * @since 1.1 + * @see AndCondition + * @see OrCondition + * @see IsNull + * @see Comparison + * @see Like + * @see In + */ +class ConditionVisitor extends TypedSubtreeVisitor implements PartRenderer { + + private final RenderContext context; + private StringBuilder builder = new StringBuilder(); + + ConditionVisitor(RenderContext context) { + this.context = context; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#enterMatched(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + Delegation enterMatched(Condition segment) { + + DelegatingVisitor visitor = getDelegation(segment); + + return visitor != null ? Delegation.delegateTo(visitor) : Delegation.retain(); + } + + @Nullable + private DelegatingVisitor getDelegation(Condition segment) { + + if (segment instanceof AndCondition) { + return new MultiConcatConditionVisitor(context, (AndCondition) segment, builder::append); + } + + if (segment instanceof OrCondition) { + return new MultiConcatConditionVisitor(context, (OrCondition) segment, builder::append); + } + + if (segment instanceof IsNull) { + return new IsNullVisitor(context, builder::append); + } + + if (segment instanceof Comparison) { + return new ComparisonVisitor(context, (Comparison) segment, builder::append); + } + + if (segment instanceof Like) { + return new LikeVisitor((Like) segment, context, builder::append); + } + + if (segment instanceof In) { + return new InVisitor(context, builder::append); + } + + return null; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.PartRenderer#getRenderedPart() + */ + @Override + public CharSequence getRenderedPart() { + return builder; + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/DelegatingVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/DelegatingVisitor.java new file mode 100644 index 00000000..a04be55c --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/DelegatingVisitor.java @@ -0,0 +1,198 @@ +/* + * Copyright 2019 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 + * + * http://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.Stack; + +import org.springframework.data.relational.core.sql.Visitable; +import org.springframework.data.relational.core.sql.Visitor; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Abstract base class for delegating {@link Visitor} implementations. This class implements a delegation pattern using + * visitors. A delegating {@link Visitor} can implement {@link #doEnter(Visitable)} and {@link #doLeave(Visitable)} + * methods to provide its functionality. + *

+ *

Delegation

Typically, a {@link Visitor} is scoped to a single responsibility. If a {@link Visitor segment} + * requires {@link #doEnter(Visitable) processing} that is not directly implemented by the visitor itself, the current + * {@link Visitor} can delegate processing to a {@link DelegatingVisitor delegate}. Once a delegation is installed, the + * {@link DelegatingVisitor delegate} is used as {@link Visitor} for the current and all subsequent items until it + * {@link #doLeave(Visitable) signals} that it is no longer responsible. + *

+ * Nested visitors are required to properly signal once they are no longer responsible for a {@link Visitor segment} to + * step back from the delegation. Otherwise, parents are no longer involved in the visitation. + *

+ * Delegation is recursive and limited by the stack size. + * + * @author Mark Paluch + * @since 1.1 + * @see FilteredSubtreeVisitor + * @see TypedSubtreeVisitor + */ +abstract class DelegatingVisitor implements Visitor { + + private Stack delegation = new Stack<>(); + + /** + * Invoked for a {@link Visitable segment} when entering the segment. + *

+ * This method can signal whether it is responsible for handling the {@link Visitor segment} or whether the segment + * requires delegation to a sub-{@link Visitor}. When delegating to a sub-{@link Visitor}, {@link #doEnter(Visitable)} + * is called on the {@link DelegatingVisitor delegate}. + * + * @param segment must not be {@literal null}. + * @return + */ + @Nullable + public abstract Delegation doEnter(Visitable segment); + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.Visitor#enter(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + public final void enter(Visitable segment) { + + if (delegation.isEmpty()) { + + Delegation visitor = doEnter(segment); + Assert.notNull(visitor, + () -> String.format("Visitor must not be null. Caused by %s.doEnter(…)", getClass().getName())); + Assert.state(!visitor.isLeave(), + () -> String.format("Delegation indicates leave. Caused by %s.doEnter(…)", getClass().getName())); + + if (visitor.isDelegate()) { + delegation.push(visitor.getDelegate()); + visitor.getDelegate().enter(segment); + } + } else { + delegation.peek().enter(segment); + } + } + + /** + * Invoked for a {@link Visitable segment} when leaving the segment. + *

+ * This method can signal whether this {@link Visitor} should remain responsible for handling subsequent + * {@link Visitor segments} or whether it should step back from delegation. When stepping back from delegation, + * {@link #doLeave(Visitable)} is called on the {@link DelegatingVisitor parent delegate}. + * + * @param segment must not be {@literal null}. + * @return + */ + public abstract Delegation doLeave(Visitable segment); + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.Visitor#leave(org.springframework.data.relational.core.sql.Visitable) + */ + public final void leave(Visitable segment) { + doLeave0(segment); + } + + private Delegation doLeave0(Visitable segment) { + + if (delegation.isEmpty()) { + return doLeave(segment); + } else { + + DelegatingVisitor visitor = delegation.peek(); + while (visitor != null) { + + Delegation result = visitor.doLeave0(segment); + Assert.notNull(visitor, + () -> String.format("Visitor must not be null. Caused by %s.doLeave(…)", getClass().getName())); + + if (visitor == this) { + if (result.isLeave()) { + return delegation.isEmpty() ? Delegation.leave() : Delegation.retain(); + } + return Delegation.retain(); + } + + if (result.isRetain()) { + return result; + } + + if (result.isLeave()) { + + if (!delegation.isEmpty()) { + delegation.pop(); + } + + if (!delegation.isEmpty()) { + visitor = delegation.peek(); + } else { + visitor = this; + } + } + } + } + + return Delegation.leave(); + } + + /** + * Value object to control delegation. + */ + static class Delegation { + + private static Delegation RETAIN = new Delegation(true, false, null); + private static Delegation LEAVE = new Delegation(false, true, null); + + private final boolean retain; + private final boolean leave; + + private final @Nullable DelegatingVisitor delegate; + + private Delegation(boolean retain, boolean leave, @Nullable DelegatingVisitor delegate) { + this.retain = retain; + this.leave = leave; + this.delegate = delegate; + } + + public static Delegation retain() { + return RETAIN; + } + + public static Delegation leave() { + return LEAVE; + } + + public static Delegation delegateTo(DelegatingVisitor visitor) { + return new Delegation(false, false, visitor); + } + + boolean isDelegate() { + return delegate != null; + } + + boolean isRetain() { + return retain; + } + + boolean isLeave() { + return leave; + } + + DelegatingVisitor getDelegate() { + + Assert.state(isDelegate(), "No delegate available"); + return delegate; + } + } +} 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 new file mode 100644 index 00000000..604190e0 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java @@ -0,0 +1,118 @@ +/* + * Copyright 2019 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 + * + * http://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 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.SubselectExpression; +import org.springframework.data.relational.core.sql.Visitable; +import org.springframework.lang.Nullable; + +/** + * {@link PartRenderer} for {@link Expression}s. + * + * @author Mark Paluch + * @author Jens Schauder + * @since 1.1 + * @see Column + * @see SubselectExpression + */ +class ExpressionVisitor extends TypedSubtreeVisitor implements PartRenderer { + + private final RenderContext context; + + private CharSequence value = ""; + private @Nullable PartRenderer partRenderer; + + ExpressionVisitor(RenderContext context) { + this.context = context; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#enterMatched(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + Delegation enterMatched(Expression segment) { + + if (segment instanceof SubselectExpression) { + + SelectStatementVisitor visitor = new SelectStatementVisitor(context); + partRenderer = visitor; + return Delegation.delegateTo(visitor); + } + + if (segment instanceof Column) { + + RenderNamingStrategy namingStrategy = context.getNamingStrategy(); + Column column = (Column) segment; + + value = namingStrategy.getReferenceName(column.getTable()) + "." + namingStrategy.getReferenceName(column); + } else if (segment instanceof BindMarker) { + + if (segment instanceof Named) { + value = ((Named) segment).getName(); + } else { + value = segment.toString(); + } + } + + return Delegation.retain(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#enterNested(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + Delegation enterNested(Visitable segment) { + + if (segment instanceof Condition) { + ConditionVisitor visitor = new ConditionVisitor(context); + partRenderer = visitor; + return Delegation.delegateTo(visitor); + } + + return super.enterNested(segment); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#leaveMatched(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + Delegation leaveMatched(Expression segment) { + + if (partRenderer != null) { + value = partRenderer.getRenderedPart(); + partRenderer = null; + } + + return super.leaveMatched(segment); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.PartRenderer#getRenderedPart() + */ + @Override + public CharSequence getRenderedPart() { + return value; + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/FilteredSingleConditionRenderSupport.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/FilteredSingleConditionRenderSupport.java new file mode 100644 index 00000000..99f9b5df --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/FilteredSingleConditionRenderSupport.java @@ -0,0 +1,96 @@ +/* + * Copyright 2019 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 + * + * http://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.function.Predicate; + +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.util.Assert; + +/** + * Support class for {@link FilteredSubtreeVisitor filtering visitors} that want to render a single {@link Condition} + * and delegate nested {@link Expression} and {@link Condition} rendering. + * + * @author Mark Paluch + * @since 1.1 + */ +abstract class FilteredSingleConditionRenderSupport extends FilteredSubtreeVisitor { + + private final RenderContext context; + private PartRenderer current; + + /** + * Creates a new {@link FilteredSingleConditionRenderSupport} given the filter {@link Predicate}. + * + * @param context + * @param filter filter predicate to identify when to {@link #enterMatched(Visitable) + * enter}/{@link #leaveMatched(Visitable) leave} the {@link Visitable segment} that this visitor is + * responsible for. + */ + FilteredSingleConditionRenderSupport(RenderContext context, Predicate filter) { + super(filter); + this.context = context; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.FilteredSubtreeVisitor#enterNested(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + Delegation enterNested(Visitable segment) { + + if (segment instanceof Expression) { + ExpressionVisitor visitor = new ExpressionVisitor(context); + current = visitor; + return Delegation.delegateTo(visitor); + } + + if (segment instanceof Condition) { + ConditionVisitor visitor = new ConditionVisitor(context); + current = visitor; + return Delegation.delegateTo(visitor); + } + + throw new IllegalStateException("Cannot provide visitor for " + segment); + } + + /** + * Returns whether rendering was delegated to a {@link ExpressionVisitor} or {@link ConditionVisitor}. + * + * @return {@literal true} when rendering was delegated to a {@link ExpressionVisitor} or {@link ConditionVisitor}. + */ + protected boolean hasDelegatedRendering() { + return current != null; + } + + /** + * Consumes the delegated rendering part. Call {@link #hasDelegatedRendering()} to check whether rendering was + * actually delegated. Consumption releases the delegated rendered. + * + * @return the delegated rendered part. + * @throws IllegalStateException if rendering was not delegate. + */ + protected CharSequence consumeRenderedPart() { + + Assert.state(hasDelegatedRendering(), "Rendering not delegated. Cannot consume delegated rendering part."); + + PartRenderer current = this.current; + this.current = null; + return current.getRenderedPart(); + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/FilteredSubtreeVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/FilteredSubtreeVisitor.java new file mode 100644 index 00000000..6b370616 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/FilteredSubtreeVisitor.java @@ -0,0 +1,146 @@ +/* + * Copyright 2019 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 + * + * http://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.function.Predicate; + +import org.springframework.data.relational.core.sql.Visitable; +import org.springframework.data.relational.core.sql.Visitor; +import org.springframework.lang.Nullable; + +/** + * Filtering {@link DelegatingVisitor visitor} applying a {@link Predicate filter}. Typically used as base class for + * {@link Visitor visitors} that wish to apply hierarchical processing based on a well-defined entry {@link Visitor + * segment}. + *

+ * Filtering is a three-way process: + *

    + *
  1. Ignores elements that do not match the filter {@link Predicate}.
  2. + *
  3. {@link #enterMatched(Visitable) enter}/{@link #leaveMatched(Visitable) leave} matched callbacks for the + * {@link Visitable segment} that matches the {@link Predicate}.
  4. + *
  5. {@link #enterNested(Visitable) enter}/{@link #leaveNested(Visitable) leave} nested callbacks for direct/nested + * children of the matched {@link Visitable} until {@link #leaveMatched(Visitable) leaving the matched} + * {@link Visitable}.
  6. + *
+ * + * @author Mark Paluch + * @see TypedSubtreeVisitor + * @since 1.1 + */ +abstract class FilteredSubtreeVisitor extends DelegatingVisitor { + + private final Predicate filter; + + private @Nullable Visitable currentSegment; + + /** + * Creates a new {@link FilteredSubtreeVisitor} given the filter {@link Predicate}. + * + * @param filter filter predicate to identify when to {@link #enterMatched(Visitable) + * enter}/{@link #leaveMatched(Visitable) leave} the {@link Visitable segment} that this visitor is + * responsible for. + */ + FilteredSubtreeVisitor(Predicate filter) { + this.filter = filter; + } + + /** + * {@link Visitor#enter(Visitable) Enter} callback for a {@link Visitable} that this {@link Visitor} is responsible + * for. The default implementation retains delegation by default. + * + * @param segment the segment, must not be {@literal null}. + * @return delegation options. Can be either {@link Delegation#retain()} or + * {@link Delegation#delegateTo(DelegatingVisitor)}. + * @see Delegation#retain() + */ + Delegation enterMatched(Visitable segment) { + return Delegation.retain(); + } + + /** + * {@link Visitor#enter(Visitable) Enter} callback for a nested {@link Visitable}. The default implementation retains + * delegation by default. + * + * @param segment the segment, must not be {@literal null}. + * @return delegation options. Can be either {@link Delegation#retain()} or + * {@link Delegation#delegateTo(DelegatingVisitor)}. + * @see Delegation#retain() + */ + Delegation enterNested(Visitable segment) { + return Delegation.retain(); + } + + /** + * {@link Visitor#leave(Visitable) Leave} callback for the matched {@link Visitable}. The default implementation steps + * back from delegation by default. + * + * @param segment the segment, must not be {@literal null}. + * @return delegation options. Can be either {@link Delegation#retain()} or {@link Delegation#leave()}. + * @see Delegation#leave() + */ + Delegation leaveMatched(Visitable segment) { + return Delegation.leave(); + } + + /** + * {@link Visitor#leave(Visitable) Leave} callback for a nested {@link Visitable}. The default implementation retains + * delegation by default. + * + * @param segment the segment, must not be {@literal null}. + * @return delegation options. Can be either {@link Delegation#retain()} or {@link Delegation#leave()}. + * @see Delegation#retain() + */ + Delegation leaveNested(Visitable segment) { + return Delegation.retain(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.DelegatingVisitor#doEnter(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + public final Delegation doEnter(Visitable segment) { + + if (currentSegment == null) { + + if (filter.test(segment)) { + currentSegment = segment; + return enterMatched(segment); + } + } else { + return enterNested(segment); + } + + return Delegation.retain(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.DelegatingVisitor#doLeave(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + public final Delegation doLeave(Visitable segment) { + + if (currentSegment == null) { + return Delegation.leave(); + } else if (segment == currentSegment) { + currentSegment = null; + return leaveMatched(segment); + } else { + return leaveNested(segment); + } + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/FromClauseVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/FromClauseVisitor.java new file mode 100644 index 00000000..2a334ac4 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/FromClauseVisitor.java @@ -0,0 +1,69 @@ +/* + * Copyright 2019 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 + * + * http://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 org.springframework.data.relational.core.sql.From; +import org.springframework.data.relational.core.sql.Visitable; + +/** + * Renderer for {@link From}. Uses a {@link RenderTarget} to call back for render results. + * + * @author Mark Paluch + * @author Jens Schauder + * @since 1.1 + */ +class FromClauseVisitor extends TypedSubtreeVisitor { + + private final FromTableVisitor visitor; + private final RenderTarget parent; + private final StringBuilder builder = new StringBuilder(); + private boolean first = true; + + FromClauseVisitor(RenderContext context, RenderTarget parent) { + + this.visitor = new FromTableVisitor(context, it -> { + + if (first) { + first = false; + } else { + builder.append(", "); + } + + builder.append(it); + }); + + this.parent = parent; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#enterNested(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + Delegation enterNested(Visitable segment) { + return Delegation.delegateTo(visitor); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#leaveMatched(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + Delegation leaveMatched(From segment) { + parent.onRendered(builder); + return super.leaveMatched(segment); + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/FromTableVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/FromTableVisitor.java new file mode 100644 index 00000000..7870c6cf --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/FromTableVisitor.java @@ -0,0 +1,59 @@ +/* + * Copyright 2019 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 + * + * http://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 org.springframework.data.relational.core.sql.Aliased; +import org.springframework.data.relational.core.sql.From; +import org.springframework.data.relational.core.sql.Table; + +/** + * Renderer for {@link Table} used within a {@link From} clause. Uses a {@link RenderTarget} to call back for render + * results. + * + * @author Mark Paluch + * @author Jens Schauder + * @since 1.1 + */ +class FromTableVisitor extends TypedSubtreeVisitor
{ + + private final RenderContext context; + private final RenderTarget parent; + + FromTableVisitor(RenderContext context, RenderTarget parent) { + super(); + this.context = context; + this.parent = parent; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#enterMatched(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + Delegation enterMatched(Table segment) { + + StringBuilder builder = new StringBuilder(); + + builder.append(context.getNamingStrategy().getName(segment)); + if (segment instanceof Aliased) { + builder.append(" AS ").append(((Aliased) segment).getAlias()); + } + + parent.onRendered(builder); + + return super.enterMatched(segment); + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/InVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/InVisitor.java new file mode 100644 index 00000000..3992f07f --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/InVisitor.java @@ -0,0 +1,77 @@ +/* + * Copyright 2019 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 + * + * http://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 org.springframework.data.relational.core.sql.In; +import org.springframework.data.relational.core.sql.Visitable; + +/** + * Renderer for {@link In}. Uses a {@link RenderTarget} to call back for render results. + * + * @author Mark Paluch + * @author Jens Schauder + * @since 1.1 + */ +class InVisitor extends TypedSingleConditionRenderSupport { + + private final RenderTarget target; + private final StringBuilder part = new StringBuilder(); + private boolean needsComma = false; + + InVisitor(RenderContext context, RenderTarget target) { + super(context); + this.target = target; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#leaveNested(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + Delegation leaveNested(Visitable segment) { + + if (hasDelegatedRendering()) { + CharSequence renderedPart = consumeRenderedPart(); + + if (needsComma) { + part.append(", "); + } + + if (part.length() == 0) { + part.append(renderedPart); + part.append(" IN ("); + } else { + part.append(renderedPart); + needsComma = true; + } + } + + return super.leaveNested(segment); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#leaveMatched(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + Delegation leaveMatched(In segment) { + + part.append(")"); + target.onRendered(part); + + return super.leaveMatched(segment); + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/IsNullVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/IsNullVisitor.java new file mode 100644 index 00000000..4a551154 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/IsNullVisitor.java @@ -0,0 +1,69 @@ +/* + * Copyright 2019 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 + * + * http://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 org.springframework.data.relational.core.sql.IsNull; +import org.springframework.data.relational.core.sql.Visitable; + +/** + * Renderer for {@link IsNull}. Uses a {@link RenderTarget} to call back for render results. + * + * @author Mark Paluch + * @author Jens Schauder + * @since 1.1 + */ +class IsNullVisitor extends TypedSingleConditionRenderSupport { + + private final RenderTarget target; + private final StringBuilder part = new StringBuilder(); + + IsNullVisitor(RenderContext context, RenderTarget target) { + super(context); + this.target = target; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#leaveNested(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + Delegation leaveNested(Visitable segment) { + + if (hasDelegatedRendering()) { + part.append(consumeRenderedPart()); + } + + return super.leaveNested(segment); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#leaveMatched(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + Delegation leaveMatched(IsNull segment) { + + if (segment.isNegated()) { + part.append(" IS NOT NULL"); + } else { + part.append(" IS NULL"); + } + + target.onRendered(part); + + return super.leaveMatched(segment); + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/JoinVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/JoinVisitor.java new file mode 100644 index 00000000..c174c35d --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/JoinVisitor.java @@ -0,0 +1,104 @@ +/* + * Copyright 2019 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 + * + * http://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 org.springframework.data.relational.core.sql.Aliased; +import org.springframework.data.relational.core.sql.Condition; +import org.springframework.data.relational.core.sql.Join; +import org.springframework.data.relational.core.sql.Table; +import org.springframework.data.relational.core.sql.Visitable; + +/** + * Renderer for {@link Join} segments. Uses a {@link RenderTarget} to call back for render results. + * + * @author Mark Paluch + * @author Jens Schauder + * @since 1.1 + */ +class JoinVisitor extends TypedSubtreeVisitor { + + private final RenderContext context; + private final RenderTarget parent; + private final StringBuilder joinClause = new StringBuilder(); + private boolean inCondition = false; + private boolean hasSeenCondition = false; + + JoinVisitor(RenderContext context, RenderTarget parent) { + this.context = context; + this.parent = parent; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#enterMatched(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + Delegation enterMatched(Join segment) { + + joinClause.append(segment.getType().getSql()).append(' '); + + return super.enterMatched(segment); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#enterNested(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + Delegation enterNested(Visitable segment) { + + if (segment instanceof Table && !inCondition) { + joinClause.append(context.getNamingStrategy().getName(((Table) segment))); + if (segment instanceof Aliased) { + joinClause.append(" AS ").append(((Aliased) segment).getAlias()); + } + } else if (segment instanceof Condition) { + + // TODO: Use proper delegation for condition rendering. + inCondition = true; + if (!hasSeenCondition) { + hasSeenCondition = true; + joinClause.append(" ON "); + joinClause.append(segment); + } + } + + return super.enterNested(segment); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#leaveNested(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + Delegation leaveNested(Visitable segment) { + + if (segment instanceof Condition) { + inCondition = false; + } + return super.leaveNested(segment); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#leaveMatched(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + Delegation leaveMatched(Join segment) { + parent.onRendered(joinClause); + return super.leaveMatched(segment); + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/LikeVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/LikeVisitor.java new file mode 100644 index 00000000..2ed8ef0d --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/LikeVisitor.java @@ -0,0 +1,97 @@ +/* + * Copyright 2019 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 + * + * http://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 org.springframework.data.relational.core.sql.Condition; +import org.springframework.data.relational.core.sql.Expression; +import org.springframework.data.relational.core.sql.Like; +import org.springframework.data.relational.core.sql.Visitable; +import org.springframework.lang.Nullable; + +/** + * {@link org.springframework.data.relational.core.sql.Visitor} rendering comparison {@link Condition}. Uses a + * {@link RenderTarget} to call back for render results. + * + * @author Mark Paluch + * @see Like + * @since 1.1 + */ +class LikeVisitor extends FilteredSubtreeVisitor { + + private final RenderContext context; + private final RenderTarget target; + private final StringBuilder part = new StringBuilder(); + private @Nullable PartRenderer current; + + LikeVisitor(Like condition, RenderContext context, RenderTarget target) { + super(it -> it == condition); + this.context = context; + this.target = target; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.FilteredSubtreeVisitor#enterNested(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + Delegation enterNested(Visitable segment) { + + if (segment instanceof Expression) { + ExpressionVisitor visitor = new ExpressionVisitor(context); + current = visitor; + return Delegation.delegateTo(visitor); + } + + if (segment instanceof Condition) { + ConditionVisitor visitor = new ConditionVisitor(context); + current = visitor; + return Delegation.delegateTo(visitor); + } + + throw new IllegalStateException("Cannot provide visitor for " + segment); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.FilteredSubtreeVisitor#leaveNested(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + Delegation leaveNested(Visitable segment) { + + if (current != null) { + if (part.length() != 0) { + part.append(" LIKE "); + } + + part.append(current.getRenderedPart()); + current = null; + } + + return super.leaveNested(segment); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.FilteredSubtreeVisitor#leaveMatched(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + Delegation leaveMatched(Visitable segment) { + + target.onRendered(part); + + return super.leaveMatched(segment); + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/MultiConcatConditionVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/MultiConcatConditionVisitor.java new file mode 100644 index 00000000..288725cc --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/MultiConcatConditionVisitor.java @@ -0,0 +1,77 @@ +/* + * Copyright 2019 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 + * + * http://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 org.springframework.data.relational.core.sql.AndCondition; +import org.springframework.data.relational.core.sql.OrCondition; +import org.springframework.data.relational.core.sql.Visitable; + +/** + * Renderer for {@link AndCondition} and {@link OrCondition}. Uses a {@link RenderTarget} to call back for render + * results. + * + * @author Mark Paluch + * @author Jens Schauder + * @since 1.1 + */ +class MultiConcatConditionVisitor extends FilteredSingleConditionRenderSupport { + + private final RenderTarget target; + private final String concat; + private final StringBuilder part = new StringBuilder(); + + MultiConcatConditionVisitor(RenderContext context, AndCondition condition, RenderTarget target) { + super(context, it -> it == condition); + this.target = target; + this.concat = " AND "; + } + + MultiConcatConditionVisitor(RenderContext context, OrCondition condition, RenderTarget target) { + super(context, it -> it == condition); + this.target = target; + this.concat = " OR "; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.FilteredSubtreeVisitor#leaveNested(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + Delegation leaveNested(Visitable segment) { + + if (hasDelegatedRendering()) { + if (part.length() != 0) { + part.append(concat); + } + + part.append(consumeRenderedPart()); + } + + return super.leaveNested(segment); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.FilteredSubtreeVisitor#leaveMatched(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + Delegation leaveMatched(Visitable segment) { + + target.onRendered(part); + + return super.leaveMatched(segment); + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/NamingStrategies.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/NamingStrategies.java new file mode 100644 index 00000000..32650a83 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/NamingStrategies.java @@ -0,0 +1,143 @@ +/* + * Copyright 2019 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 + * + * http://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 lombok.RequiredArgsConstructor; + +import java.util.Locale; +import java.util.function.Function; + +import org.springframework.data.relational.core.sql.Column; +import org.springframework.data.relational.core.sql.Table; +import org.springframework.util.Assert; + +/** + * Factory for {@link RenderNamingStrategy} objects. + * + * @author Mark Paluch + * @since 1.1 + */ +public abstract class NamingStrategies { + + private NamingStrategies() {} + + /** + * Creates a as-is {@link RenderNamingStrategy} that preserves {@link Column} and {@link Table} names as they were + * expressed during their declaration. + * + * @return as-is {@link RenderNamingStrategy}. + */ + public static RenderNamingStrategy asIs() { + return AsIs.INSTANCE; + } + + /** + * Creates a mapping {@link RenderNamingStrategy} that applies a {@link Function mapping function} to {@link Column} + * and {@link Table} names. + * + * @param mappingFunction the mapping {@link Function}, must not be {@literal null}. + * @return the mapping {@link RenderNamingStrategy}. + */ + public static RenderNamingStrategy mapWith(Function mappingFunction) { + return AsIs.INSTANCE.map(mappingFunction); + } + + /** + * Creates a mapping {@link RenderNamingStrategy} that converts {@link Column} and {@link Table} names to upper case + * using the default {@link Locale}. + * + * @return upper-casing {@link RenderNamingStrategy}. + * @see String#toUpperCase() + * @see Locale + */ + public static RenderNamingStrategy toUpper() { + return toUpper(Locale.getDefault()); + } + + /** + * Creates a mapping {@link RenderNamingStrategy} that converts {@link Column} and {@link Table} names to upper case + * using the given {@link Locale}. + * + * @param locale the locale to use. + * @return upper-casing {@link RenderNamingStrategy}. + * @see String#toUpperCase(Locale) + */ + public static RenderNamingStrategy toUpper(Locale locale) { + + Assert.notNull(locale, "Locale must not be null"); + + return AsIs.INSTANCE.map(it -> it.toUpperCase(locale)); + } + + /** + * Creates a mapping {@link RenderNamingStrategy} that converts {@link Column} and {@link Table} names to lower case + * using the default {@link Locale}. + * + * @return lower-casing {@link RenderNamingStrategy}. + * @see String#toLowerCase() + * @see Locale + */ + public static RenderNamingStrategy toLower() { + return toLower(Locale.getDefault()); + } + + /** + * Creates a mapping {@link RenderNamingStrategy} that converts {@link Column} and {@link Table} names to lower case + * using the given {@link Locale}. + * + * @param locale the locale to use. + * @return lower-casing {@link RenderNamingStrategy}. + * @see String#toLowerCase(Locale) + * @see Locale + */ + public static RenderNamingStrategy toLower(Locale locale) { + + Assert.notNull(locale, "Locale must not be null"); + + return AsIs.INSTANCE.map(it -> it.toLowerCase(locale)); + } + + enum AsIs implements RenderNamingStrategy { + INSTANCE; + } + + @RequiredArgsConstructor + static class DelegatingRenderNamingStrategy implements RenderNamingStrategy { + + private final RenderNamingStrategy delegate; + private final Function mappingFunction; + + @Override + public String getName(Column column) { + return mappingFunction.apply(delegate.getName(column)); + } + + @Override + public String getReferenceName(Column column) { + return mappingFunction.apply(delegate.getReferenceName(column)); + } + + @Override + public String getName(Table table) { + return mappingFunction.apply(delegate.getName(table)); + } + + @Override + public String getReferenceName(Table table) { + return mappingFunction.apply(delegate.getReferenceName(table)); + } + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/OrderByClauseVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/OrderByClauseVisitor.java new file mode 100644 index 00000000..d7573c07 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/OrderByClauseVisitor.java @@ -0,0 +1,94 @@ +/* + * Copyright 2019 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 + * + * http://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 org.springframework.data.relational.core.sql.Column; +import org.springframework.data.relational.core.sql.OrderByField; +import org.springframework.data.relational.core.sql.Visitable; + +/** + * {@link PartRenderer} for {@link OrderByField}s. + * + * @author Mark Paluch + * @author Jens Schauder + * @since 1.1 + */ +class OrderByClauseVisitor extends TypedSubtreeVisitor implements PartRenderer { + + private final RenderContext context; + + private final StringBuilder builder = new StringBuilder(); + private boolean first = true; + + OrderByClauseVisitor(RenderContext context) { + this.context = context; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#enterMatched(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + Delegation enterMatched(OrderByField segment) { + + if (!first) { + builder.append(", "); + } + first = false; + + return super.enterMatched(segment); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#leaveMatched(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + Delegation leaveMatched(OrderByField segment) { + + OrderByField field = segment; + + if (field.getDirection() != null) { + builder.append(" ") // + .append(field.getDirection()); + } + + return Delegation.leave(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#leaveNested(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + Delegation leaveNested(Visitable segment) { + + if (segment instanceof Column) { + builder.append(context.getNamingStrategy().getReferenceName(((Column) segment))); + } + + return super.leaveNested(segment); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.PartRenderer#getRenderedPart() + */ + @Override + public CharSequence getRenderedPart() { + return builder; + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/PartRenderer.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/PartRenderer.java new file mode 100644 index 00000000..15963631 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/PartRenderer.java @@ -0,0 +1,34 @@ +/* + * Copyright 2019 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 + * + * http://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 org.springframework.data.relational.core.sql.Visitor; + +/** + * {@link Visitor} that renders a specific partial clause or expression. + * + * @author Mark Paluch + * @since 1.1 + */ +interface PartRenderer extends Visitor { + + /** + * Returns the rendered part. + * + * @return the rendered part. + */ + CharSequence getRenderedPart(); +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/RenderContext.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/RenderContext.java new file mode 100644 index 00000000..161c6059 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/RenderContext.java @@ -0,0 +1,32 @@ +/* + * Copyright 2019 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 + * + * http://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; + +/** + * Render context providing {@link RenderNamingStrategy} and other resources that are required during rendering. + * + * @author Mark Paluch + * @since 1.1 + */ +public interface RenderContext { + + /** + * Returns the configured {@link RenderNamingStrategy}. + * + * @return the {@link RenderNamingStrategy}. + */ + RenderNamingStrategy getNamingStrategy(); +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/RenderNamingStrategy.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/RenderNamingStrategy.java new file mode 100644 index 00000000..b6182e25 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/RenderNamingStrategy.java @@ -0,0 +1,91 @@ +/* + * Copyright 2019 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 + * + * http://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.function.Function; + +import org.springframework.data.relational.core.sql.Column; +import org.springframework.data.relational.core.sql.Table; +import org.springframework.data.relational.core.sql.render.NamingStrategies.DelegatingRenderNamingStrategy; +import org.springframework.util.Assert; + +/** + * Naming strategy for SQL rendering. + * + * @author Mark Paluch + * @see NamingStrategies + * @since 1.1 + */ +public interface RenderNamingStrategy { + + /** + * Return the {@link Column#getName() column name}. + * + * @param column the column. + * @return the {@link Column#getName() column name}. + * @see Column#getName() + */ + default String getName(Column column) { + return column.getName(); + } + + /** + * Return the {@link Column#getName() column reference name}. + * + * @param column the column. + * @return the {@link Column#getName() column reference name}. + * @see Column#getReferenceName() () + */ + default String getReferenceName(Column column) { + return column.getReferenceName(); + } + + /** + * Return the {@link Table#getName() table name}. + * + * @param table the table. + * @return the {@link Table#getName() table name}. + * @see Table#getName() + */ + default String getName(Table table) { + return table.getName(); + } + + /** + * Return the {@link Table#getReferenceName() table reference name}. + * + * @param table the table. + * @return the {@link Table#getReferenceName() table name}. + * @see Table#getReferenceName() + */ + default String getReferenceName(Table table) { + return table.getReferenceName(); + } + + /** + * Applies a {@link Function mapping function} after retrieving the object (column name, column reference name, …) + * name. + * + * @param mappingFunction the function that maps an object name. + * @return a new {@link RenderNamingStrategy} applying {@link Function mapping function}. + */ + default RenderNamingStrategy map(Function mappingFunction) { + + Assert.notNull(mappingFunction, "Mapping function must not be null!"); + + return new DelegatingRenderNamingStrategy(this, mappingFunction); + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/RenderTarget.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/RenderTarget.java new file mode 100644 index 00000000..91e21efd --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/RenderTarget.java @@ -0,0 +1,37 @@ +/* + * Copyright 2019 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 + * + * http://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 org.springframework.data.relational.core.sql.Visitor; + +/** + * Callback interface for {@link Visitor visitors} that wish to notify a render target when they are complete with + * rendering. + * + * @author Mark Paluch + * @since 1.1 + */ +@FunctionalInterface +interface RenderTarget { + + /** + * Callback method that is invoked once the rendering for a part or expression is finished. When called multiple + * times, it's the responsibility of the implementor to ensure proper concatenation of render results. + * + * @param sequence the rendered part or expression. + */ + void onRendered(CharSequence sequence); +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SelectListVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SelectListVisitor.java new file mode 100644 index 00000000..00432d52 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SelectListVisitor.java @@ -0,0 +1,111 @@ +/* + * Copyright 2019 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 + * + * http://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 org.springframework.data.relational.core.sql.Aliased; +import org.springframework.data.relational.core.sql.Column; +import org.springframework.data.relational.core.sql.SelectList; +import org.springframework.data.relational.core.sql.SimpleFunction; +import org.springframework.data.relational.core.sql.Table; +import org.springframework.data.relational.core.sql.Visitable; + +/** + * {@link PartRenderer} for {@link SelectList}s. + * + * @author Mark Paluch + * @author Jens Schauder + * @since 1.1 + */ +class SelectListVisitor extends TypedSubtreeVisitor implements PartRenderer { + + private final RenderContext context; + private final StringBuilder builder = new StringBuilder(); + private final RenderTarget target; + private boolean requiresComma = false; + private boolean insideFunction = false; // this is hackery and should be fix with a proper visitor for + // subelements. + + SelectListVisitor(RenderContext context, RenderTarget target) { + this.context = context; + this.target = target; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#enterNested(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + Delegation enterNested(Visitable segment) { + + if (requiresComma) { + builder.append(", "); + requiresComma = false; + } + if (segment instanceof SimpleFunction) { + builder.append(((SimpleFunction) segment).getFunctionName()).append("("); + insideFunction = true; + } else { + insideFunction = false; + } + + return super.enterNested(segment); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#leaveMatched(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + Delegation leaveMatched(SelectList segment) { + + target.onRendered(builder); + return super.leaveMatched(segment); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#leaveNested(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + Delegation leaveNested(Visitable segment) { + + if (segment instanceof Table) { + builder.append(context.getNamingStrategy().getReferenceName((Table) segment)).append('.'); + } + + if (segment instanceof SimpleFunction) { + builder.append(")"); + requiresComma = true; + } else if (segment instanceof Column) { + builder.append(context.getNamingStrategy().getName((Column) segment)); + if (segment instanceof Aliased) { + builder.append(" AS ").append(((Aliased) segment).getAlias()); + } + requiresComma = true; + } + + return super.leaveNested(segment); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.PartRenderer#getRenderedPart() + */ + @Override + public CharSequence getRenderedPart() { + return builder; + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SelectStatementVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SelectStatementVisitor.java new file mode 100644 index 00000000..424bd656 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SelectStatementVisitor.java @@ -0,0 +1,160 @@ +/* + * Copyright 2019 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 + * + * http://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.OptionalLong; + +import org.springframework.data.relational.core.sql.From; +import org.springframework.data.relational.core.sql.Join; +import org.springframework.data.relational.core.sql.OrderByField; +import org.springframework.data.relational.core.sql.Select; +import org.springframework.data.relational.core.sql.SelectList; +import org.springframework.data.relational.core.sql.Visitable; +import org.springframework.data.relational.core.sql.Where; + +/** + * {@link PartRenderer} for {@link Select} statements. + * + * @author Mark Paluch + * @author Jens Schauder + * @since 1.1 + */ +class SelectStatementVisitor extends DelegatingVisitor implements PartRenderer { + + private final RenderContext context; + + private StringBuilder builder = new StringBuilder(); + private StringBuilder selectList = new StringBuilder(); + private StringBuilder from = new StringBuilder(); + private StringBuilder join = new StringBuilder(); + private StringBuilder where = new StringBuilder(); + + private SelectListVisitor selectListVisitor; + private OrderByClauseVisitor orderByClauseVisitor; + private FromClauseVisitor fromClauseVisitor; + private WhereClauseVisitor whereClauseVisitor; + + SelectStatementVisitor(RenderContext context) { + + this.context = context; + this.selectListVisitor = new SelectListVisitor(context, selectList::append); + this.orderByClauseVisitor = new OrderByClauseVisitor(context); + this.fromClauseVisitor = new FromClauseVisitor(context, it -> { + + if (from.length() != 0) { + from.append(", "); + } + + from.append(it); + }); + + this.whereClauseVisitor = new WhereClauseVisitor(context, where::append); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.DelegatingVisitor#doEnter(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + public Delegation doEnter(Visitable segment) { + + if (segment instanceof SelectList) { + return Delegation.delegateTo(selectListVisitor); + } + + if (segment instanceof OrderByField) { + return Delegation.delegateTo(orderByClauseVisitor); + } + + if (segment instanceof From) { + return Delegation.delegateTo(fromClauseVisitor); + } + + if (segment instanceof Join) { + return Delegation.delegateTo(new JoinVisitor(context, it -> { + + if (join.length() != 0) { + join.append(' '); + } + + join.append(it); + })); + } + + if (segment instanceof Where) { + return Delegation.delegateTo(whereClauseVisitor); + } + + return Delegation.retain(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.DelegatingVisitor#doLeave(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + public Delegation doLeave(Visitable segment) { + + if (segment instanceof Select) { + + builder.append("SELECT "); + if (((Select) segment).isDistinct()) { + builder.append("DISTINCT "); + } + + builder.append(selectList); + + if (from.length() != 0) { + builder.append(" FROM ").append(from); + } + + if (join.length() != 0) { + builder.append(' ').append(join); + } + + if (where.length() != 0) { + builder.append(" WHERE ").append(where); + } + + CharSequence orderBy = orderByClauseVisitor.getRenderedPart(); + if (orderBy.length() != 0) + builder.append(" ORDER BY ").append(orderBy); + + OptionalLong limit = ((Select) segment).getLimit(); + if (limit.isPresent()) { + builder.append(" LIMIT ").append(limit.getAsLong()); + } + + OptionalLong offset = ((Select) segment).getOffset(); + if (offset.isPresent()) { + builder.append(" OFFSET ").append(offset.getAsLong()); + } + + return Delegation.leave(); + } + + return Delegation.retain(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.PartRenderer#getRenderedPart() + */ + @Override + public CharSequence getRenderedPart() { + return builder; + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SimpleRenderContext.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SimpleRenderContext.java new file mode 100644 index 00000000..a9e065fb --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SimpleRenderContext.java @@ -0,0 +1,31 @@ +/* + * Copyright 2019 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 + * + * http://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 lombok.Value; + +/** + * Default {@link RenderContext} implementation. + * + * @author Mark Paluch + * @since 1.1 + */ +@Value +class SimpleRenderContext implements RenderContext { + + private final RenderNamingStrategy namingStrategy; + +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SqlRenderer.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SqlRenderer.java new file mode 100644 index 00000000..b114173c --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SqlRenderer.java @@ -0,0 +1,85 @@ +/* + * Copyright 2019 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 + * + * http://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 org.springframework.data.relational.core.sql.Select; +import org.springframework.util.Assert; + +/** + * Naive SQL renderer that does not consider dialect specifics. This class is to evaluate requirements of a SQL + * renderer. + * + * @author Mark Paluch + * @author Jens Schauder + * @since 1.1 + */ +public class SqlRenderer { + + private final Select select; + private final RenderContext context; + + private SqlRenderer(Select select, RenderContext context) { + this.context = context; + + Assert.notNull(select, "Select must not be null!"); + + this.select = select; + } + + /** + * Creates a new {@link SqlRenderer}. + * + * @param select must not be {@literal null}. + * @return the renderer. + */ + public static SqlRenderer create(Select select) { + return new SqlRenderer(select, new SimpleRenderContext(NamingStrategies.asIs())); + } + + /** + * Creates a new {@link SqlRenderer} using a {@link RenderContext}. + * + * @param select must not be {@literal null}. + * @param context must not be {@literal null}. + * @return the renderer. + */ + public static SqlRenderer create(Select select, RenderContext context) { + return new SqlRenderer(select, context); + } + + /** + * Renders a {@link Select} statement into its SQL representation. + * + * @param select must not be {@literal null}. + * @return the rendered statement. + */ + public static String render(Select select) { + return create(select).render(); + } + + /** + * Render the {@link Select} AST into a SQL statement. + * + * @return the rendered statement. + */ + public String render() { + + SelectStatementVisitor visitor = new SelectStatementVisitor(context); + select.visit(visitor); + + return visitor.getRenderedPart().toString(); + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/TypedSingleConditionRenderSupport.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/TypedSingleConditionRenderSupport.java new file mode 100644 index 00000000..9e8aedf2 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/TypedSingleConditionRenderSupport.java @@ -0,0 +1,86 @@ +/* + * Copyright 2019 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 + * + * http://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 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.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Support class for {@link TypedSubtreeVisitor typed visitors} that want to render a single {@link Condition} and + * delegate nested {@link Expression} and {@link Condition} rendering. + * + * @author Mark Paluch + * @since 1.1 + */ +abstract class TypedSingleConditionRenderSupport extends TypedSubtreeVisitor { + + private final RenderContext context; + private @Nullable PartRenderer current; + + TypedSingleConditionRenderSupport(RenderContext context) { + this.context = context; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#enterNested(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + Delegation enterNested(Visitable segment) { + + if (segment instanceof Expression) { + ExpressionVisitor visitor = new ExpressionVisitor(context); + current = visitor; + return Delegation.delegateTo(visitor); + } + + if (segment instanceof Condition) { + ConditionVisitor visitor = new ConditionVisitor(context); + current = visitor; + return Delegation.delegateTo(visitor); + } + + throw new IllegalStateException("Cannot provide visitor for " + segment); + } + + /** + * Returns whether rendering was delegated to a {@link ExpressionVisitor} or {@link ConditionVisitor}. + * + * @return {@literal true} when rendering was delegated to a {@link ExpressionVisitor} or {@link ConditionVisitor}. + */ + protected boolean hasDelegatedRendering() { + return current != null; + } + + /** + * Consumes the delegated rendering part. Call {@link #hasDelegatedRendering()} to check whether rendering was + * actually delegated. Consumption releases the delegated rendered. + * + * @return the delegated rendered part. + * @throws IllegalStateException if rendering was not delegate. + */ + protected CharSequence consumeRenderedPart() { + + Assert.state(hasDelegatedRendering(), "Rendering not delegated. Cannot consume delegated rendering part."); + + PartRenderer current = this.current; + this.current = null; + return current.getRenderedPart(); + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/TypedSubtreeVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/TypedSubtreeVisitor.java new file mode 100644 index 00000000..951b7bc8 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/TypedSubtreeVisitor.java @@ -0,0 +1,145 @@ +/* + * Copyright 2019 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 + * + * http://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.function.Predicate; + +import org.springframework.core.ResolvableType; +import org.springframework.data.relational.core.sql.Visitable; +import org.springframework.data.relational.core.sql.Visitor; +import org.springframework.lang.Nullable; + +/** + * Type-filtering {@link DelegatingVisitor visitor} applying a {@link Class type filter} derived from the generic type + * parameter. Typically used as base class for {@link Visitor visitors} that wish to apply hierarchical processing based + * on a well-defined entry {@link Visitor segment}. + *

+ * Filtering is a three-way process: + *

    + *
  1. Ignores elements that do not match the filter {@link Predicate}.
  2. + *
  3. {@link #enterMatched(Visitable) enter}/{@link #leaveMatched(Visitable) leave} matched callbacks for the + * {@link Visitable segment} that matches the {@link Predicate}.
  4. + *
  5. {@link #enterNested(Visitable) enter}/{@link #leaveNested(Visitable) leave} nested callbacks for direct/nested + * children of the matched {@link Visitable} until {@link #leaveMatched(Visitable) leaving the matched} + * {@link Visitable}.
  6. + *
+ * + * @author Mark Paluch + * @since 1.1 + * @see FilteredSubtreeVisitor + */ +abstract class TypedSubtreeVisitor extends DelegatingVisitor { + + private final ResolvableType type; + private @Nullable Visitable currentSegment; + + /** + * Creates a new {@link TypedSubtreeVisitor}. + */ + TypedSubtreeVisitor() { + this.type = ResolvableType.forClass(getClass()).as(TypedSubtreeVisitor.class).getGeneric(0); + } + + /** + * {@link Visitor#enter(Visitable) Enter} callback for a {@link Visitable} that this {@link Visitor} is responsible + * for. The default implementation retains delegation by default. + * + * @param segment the segment, must not be {@literal null}. + * @return delegation options. Can be either {@link Delegation#retain()} or + * {@link Delegation#delegateTo(DelegatingVisitor)}. + * @see Delegation#retain() + */ + Delegation enterMatched(T segment) { + return Delegation.retain(); + } + + /** + * {@link Visitor#enter(Visitable) Enter} callback for a nested {@link Visitable}. The default implementation retains + * delegation by default. + * + * @param segment the segment, must not be {@literal null}. + * @return delegation options. Can be either {@link Delegation#retain()} or + * {@link Delegation#delegateTo(DelegatingVisitor)}. + * @see Delegation#retain() + */ + Delegation enterNested(Visitable segment) { + return Delegation.retain(); + } + + /** + * {@link Visitor#leave(Visitable) Leave} callback for the matched {@link Visitable}. The default implementation steps + * back from delegation by default. + * + * @param segment the segment, must not be {@literal null}. + * @return delegation options. Can be either {@link Delegation#retain()} or {@link Delegation#leave()}. + * @see Delegation#leave() + */ + Delegation leaveMatched(T segment) { + return Delegation.leave(); + } + + /** + * {@link Visitor#leave(Visitable) Leave} callback for a nested {@link Visitable}. The default implementation retains + * delegation by default. + * + * @param segment the segment, must not be {@literal null}. + * @return delegation options. Can be either {@link Delegation#retain()} or {@link Delegation#leave()}. + * @see Delegation#retain() + */ + Delegation leaveNested(Visitable segment) { + return Delegation.retain(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.DelegatingVisitor#doEnter(org.springframework.data.relational.core.sql.Visitable) + */ + @SuppressWarnings("unchecked") + @Override + public final Delegation doEnter(Visitable segment) { + + if (currentSegment == null) { + + if (this.type.isInstance(segment)) { + + currentSegment = segment; + return enterMatched((T) segment); + } + } else { + return enterNested(segment); + } + + return Delegation.retain(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.DelegatingVisitor#doLeave(org.springframework.data.relational.core.sql.Visitable) + */ + @SuppressWarnings("unchecked") + @Override + public final Delegation doLeave(Visitable segment) { + + if (currentSegment == null) { + return Delegation.leave(); + } else if (segment == currentSegment) { + currentSegment = null; + return leaveMatched((T) segment); + } else { + return leaveNested(segment); + } + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/WhereClauseVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/WhereClauseVisitor.java new file mode 100644 index 00000000..fa8ea8ac --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/WhereClauseVisitor.java @@ -0,0 +1,63 @@ +/* + * Copyright 2019 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 + * + * http://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 org.springframework.data.relational.core.sql.Condition; +import org.springframework.data.relational.core.sql.Visitable; +import org.springframework.data.relational.core.sql.Where; + +/** + * Renderer for {@link Where} segments. Uses a {@link RenderTarget} to call back for render results. + * + * @author Mark Paluch + * @author Jens Schauder + * @since 1.1 + */ +class WhereClauseVisitor extends TypedSubtreeVisitor { + + private final RenderTarget parent; + private final ConditionVisitor conditionVisitor; + + WhereClauseVisitor(RenderContext context, RenderTarget parent) { + this.conditionVisitor = new ConditionVisitor(context); + this.parent = parent; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#enterNested(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + Delegation enterNested(Visitable segment) { + + if (segment instanceof Condition) { + return Delegation.delegateTo(conditionVisitor); + } + + return super.enterNested(segment); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#leaveMatched(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + Delegation leaveMatched(Where segment) { + + parent.onRendered(conditionVisitor.getRenderedPart()); + return super.leaveMatched(segment); + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/package-info.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/package-info.java new file mode 100644 index 00000000..34c541d4 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/package-info.java @@ -0,0 +1,9 @@ +/** + * SQL rendering utilities to render SQL from the Statement Builder API. + */ +@NonNullApi +@NonNullFields +package org.springframework.data.relational.core.sql.render; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/NaiveSqlRenderer.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/NaiveSqlRenderer.java deleted file mode 100644 index 200bb779..00000000 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/NaiveSqlRenderer.java +++ /dev/null @@ -1,655 +0,0 @@ -/* - * Copyright 2019 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 - * - * http://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 java.util.OptionalLong; -import java.util.Stack; - -import org.springframework.util.Assert; - -/** - * Naive SQL renderer that does not consider dialect specifics. This class is to evaluate requirements of a SQL - * renderer. - * - * @author Mark Paluch - * @author Jens Schauder - */ -public class NaiveSqlRenderer { - - private final Select select; - - private NaiveSqlRenderer(Select select) { - - Assert.notNull(select, "Select must not be null!"); - - this.select = select; - } - - /** - * Creates a new {@link NaiveSqlRenderer}. - * - * @param select must not be {@literal null}. - * @return the renderer. - */ - public static NaiveSqlRenderer create(Select select) { - return new NaiveSqlRenderer(select); - } - - /** - * Renders a {@link Select} statement into its SQL representation. - * - * @param select must not be {@literal null}. - * @return the rendered statement. - */ - public static String render(Select select) { - return create(select).render(); - } - - /** - * Render the {@link Select} AST into a SQL statement. - * - * @return the rendered statement. - */ - public String render() { - - StackBasedVisitor visitor = new StackBasedVisitor(); - select.visit(visitor); - - return visitor.selectStatementVisitor.getValue(); - } - - interface ValuedVisitor extends Visitor { - String getValue(); - } - - static class StackBasedVisitor implements Visitor { - - private Stack visitors = new Stack<>(); - - private SelectStatementVisitor selectStatementVisitor = new SelectStatementVisitor(); - - { - visitors.push(segment -> {}); - visitors.push(selectStatementVisitor); - } - - @Override - public void enter(Visitable segment) { - - Visitor delegate = visitors.peek(); - delegate.enter(segment); - } - - @Override - public void leave(Visitable segment) { - - Visitor delegate = visitors.peek(); - delegate.leave(segment); - } - - /** - * Handles a sequence of {@link Visitable} until encountering the first that does not matches the expectations. When - * a not matching element is encountered it pops itself from the stack and delegates the call to the now top most - * element of the stack. - */ - abstract class ReadWhileMatchesVisitor implements Visitor { - - private Visitable currentSegment = null; - private Visitor nextVisitor; - - abstract boolean matches(Visitable segment); - - void enterMatched(Visitable segment) {} - - void enterSub(Visitable segment) {} - - void leaveMatched(Visitable segment) {} - - void leaveSub(Visitable segment) {} - - @Override - public void enter(Visitable segment) { - - if (currentSegment == null) { - - if (matches(segment)) { - - currentSegment = segment; - enterMatched(segment); - } else { - - Visitor popped = visitors.pop(); - - Assert.isTrue(popped == this, "Popped the wrong visitor from the stack!"); - - nextVisitor = visitors.peek(); - nextVisitor.enter(segment); - } - - } else { - enterSub(segment); - } - } - - @Override - public void leave(Visitable segment) { - - if (currentSegment == null) { - // we are receiving the leave event of the element above - visitors.pop(); - nextVisitor = visitors.peek(); - nextVisitor.leave(segment); - } else if (segment == currentSegment) { - - currentSegment = null; - leaveMatched(segment); - } else { - leaveSub(segment); - } - } - } - - /** - * Visits exactly one element that must match the expectations as defined in {@link #matches(Visitable)}. Ones - * handled it pops itself from the stack. - */ - abstract class ReadOneVisitor implements Visitor { - - private Visitable currentSegment; - - abstract boolean matches(Visitable segment); - - void enterMatched(Visitable segment) {} - - void enterSub(Visitable segment) {} - - void leaveMatched(Visitable segment) {} - - void leaveSub(Visitable segment) {} - - @Override - public void enter(Visitable segment) { - - if (currentSegment == null) { - - if (matches(segment)) { - - currentSegment = segment; - enterMatched(segment); - } else { - Assert.isTrue(visitors.pop() == this, "Popped wrong visitor instance."); - visitors.peek().enter(segment); - } - } else { - enterSub(segment); - } - } - - @Override - public void leave(Visitable segment) { - - if (currentSegment == null) { - Assert.isTrue(visitors.pop() == this, "Popped wrong visitor instance."); - visitors.peek().leave(segment); - } else if (segment == currentSegment) { - leaveMatched(segment); - Assert.isTrue(visitors.pop() == this, "Popped wrong visitor instance."); - } else { - leaveSub(segment); - } - - } - } - - class SelectStatementVisitor extends ReadOneVisitor implements ValuedVisitor { - - private StringBuilder builder = new StringBuilder(); - - private SelectListVisitor selectListVisitor = new SelectListVisitor(); - private FromClauseVisitor fromClauseVisitor = new FromClauseVisitor(); - private JoinVisitor joinVisitor = new JoinVisitor(); - private WhereClauseVisitor whereClauseVisitor = new WhereClauseVisitor(); - private OrderByClauseVisitor orderByClauseVisitor = new OrderByClauseVisitor(); - - @Override - boolean matches(Visitable segment) { - return segment instanceof Select; - } - - @Override - void enterMatched(Visitable segment) { - - visitors.push(orderByClauseVisitor); - visitors.push(whereClauseVisitor); - visitors.push(joinVisitor); - visitors.push(fromClauseVisitor); - visitors.push(selectListVisitor); - } - - @Override - void leaveMatched(Visitable segment) { - - builder.append("SELECT "); - if (((Select) segment).isDistinct()) { - builder.append("DISTINCT "); - } - - builder.append(selectListVisitor.getValue()) // - .append(fromClauseVisitor.getValue()) // - .append(joinVisitor.getValue()) // - .append(whereClauseVisitor.getValue()); - - builder.append(orderByClauseVisitor.getValue()); - - OptionalLong limit = ((Select) segment).getLimit(); - if (limit.isPresent()) { - builder.append(" LIMIT ").append(limit.getAsLong()); - } - - OptionalLong offset = ((Select) segment).getOffset(); - if (offset.isPresent()) { - builder.append(" OFFSET ").append(offset.getAsLong()); - } - } - - @Override - public String getValue() { - return builder.toString(); - } - } - - class SelectListVisitor extends ReadWhileMatchesVisitor implements ValuedVisitor { - - private StringBuilder builder = new StringBuilder(); - private boolean first = true; - private boolean insideFunction = false; // this is hackery and should be fix with a proper visitor for - // subelements. - - @Override - boolean matches(Visitable segment) { - return segment instanceof Expression; - } - - @Override - void enterMatched(Visitable segment) { - - if (!first) { - builder.append(", "); - } - if (segment instanceof SimpleFunction) { - builder.append(((SimpleFunction) segment).getFunctionName()).append("("); - insideFunction = true; - } else { - insideFunction = false; - } - } - - @Override - void leaveMatched(Visitable segment) { - - first = false; - - if (segment instanceof SimpleFunction) { - builder.append(")"); - } else if (segment instanceof Column) { - builder.append(((Column) segment).getName()); - if (segment instanceof Column.AliasedColumn) { - builder.append(" AS ").append(((Column.AliasedColumn) segment).getAlias()); - } - } - } - - @Override - void leaveSub(Visitable segment) { - - if (segment instanceof Table) { - builder.append(((Table) segment).getReferenceName()).append('.'); - } - if (insideFunction) { - - if (segment instanceof SimpleFunction) { - builder.append(")"); - } else if (segment instanceof Column) { - builder.append(((Column) segment).getName()); - if (segment instanceof Column.AliasedColumn) { - builder.append(" AS ").append(((Column.AliasedColumn) segment).getAlias()); - } - first = false; - } - } - } - - @Override - public String getValue() { - return builder.toString(); - } - } - - private class FromClauseVisitor extends ReadOneVisitor implements ValuedVisitor { - - private FromTableVisitor fromTableVisitor = new FromTableVisitor(); - - @Override - boolean matches(Visitable segment) { - return segment instanceof From; - } - - @Override - void enterMatched(Visitable segment) { - visitors.push(fromTableVisitor); - } - - @Override - public String getValue() { - return " FROM " + fromTableVisitor.getValue(); - } - } - - private class FromTableVisitor extends ReadWhileMatchesVisitor implements ValuedVisitor { - - private final StringBuilder builder = new StringBuilder(); - private boolean first = true; - - @Override - boolean matches(Visitable segment) { - return segment instanceof Table; - } - - @Override - void enterMatched(Visitable segment) { - - if (!first) { - builder.append(", "); - } - first = false; - - builder.append(((Table) segment).getName()); - if (segment instanceof Table.AliasedTable) { - builder.append(" AS ").append(((Table.AliasedTable) segment).getAlias()); - } - } - - @Override - public String getValue() { - return builder.toString(); - } - } - - private class JoinVisitor extends ReadWhileMatchesVisitor implements ValuedVisitor { - - private StringBuilder internal = new StringBuilder(); - private JoinTableAndConditionVisitor subvisitor; - - @Override - boolean matches(Visitable segment) { - return segment instanceof Join; - } - - @Override - void enterMatched(Visitable segment) { - - subvisitor = new JoinTableAndConditionVisitor(); - visitors.push(subvisitor); - } - - @Override - void leaveMatched(Visitable segment) { - append(" JOIN "); - append(subvisitor.getValue()); - } - - @Override - public String getValue() { - return internal.toString(); - } - - void append(String s) { - internal.append(s); - } - - } - - private class JoinTableAndConditionVisitor extends ReadWhileMatchesVisitor implements ValuedVisitor { - - private final StringBuilder builder = new StringBuilder(); - boolean inCondition = false; - - @Override - boolean matches(Visitable segment) { - return segment instanceof Table || segment instanceof Condition; - } - - @Override - void enterMatched(Visitable segment) { - - if (segment instanceof Table && !inCondition) { - builder.append(((Table) segment).getName()); - if (segment instanceof Table.AliasedTable) { - builder.append(" AS ").append(((Table.AliasedTable) segment).getAlias()); - } - } else if (segment instanceof Condition && !inCondition) { - builder.append(" ON "); - builder.append(segment); - inCondition = true; - } - } - - @Override - public String getValue() { - return builder.toString(); - } - } - - private class WhereClauseVisitor extends ReadOneVisitor implements ValuedVisitor { - - private ValuedVisitor conditionVisitor = new ConditionVisitor(); - private StringBuilder internal = new StringBuilder(); - - @Override - boolean matches(Visitable segment) { - return segment instanceof Where; - } - - @Override - void enterMatched(Visitable segment) { - - internal.append(" WHERE "); - visitors.push(conditionVisitor); - } - - @Override - void leaveMatched(Visitable segment) { - - internal.append(conditionVisitor.getValue()); - // builder.append(internal); - } - - @Override - public String getValue() { - return internal.toString(); - } - } - - private class ConditionVisitor extends ReadOneVisitor implements ValuedVisitor { - - private StringBuilder builder = new StringBuilder(); - - ValuedVisitor left; - ValuedVisitor right; - - @Override - boolean matches(Visitable segment) { - return segment instanceof Condition; - } - - @Override - void enterMatched(Visitable segment) { - - if (segment instanceof MultipleCondition) { - - left = new ConditionVisitor(); - right = new ConditionVisitor(); - visitors.push(right); - visitors.push(left); - - } else if (segment instanceof IsNull) { - - left = new ExpressionVisitor(); - visitors.push(left); - - } else if (segment instanceof Equals || segment instanceof In) { - - left = new ExpressionVisitor(); - right = new ExpressionVisitor(); - visitors.push(right); - visitors.push(left); - } - } - - @Override - void leaveMatched(Visitable segment) { - - if (segment instanceof AndCondition) { - - builder.append(left.getValue()) // - .append(" AND ") // - .append(right.getValue()); - - } else if (segment instanceof OrCondition) { - - builder.append("(") // - .append(left.getValue()) // - .append(" OR ") // - .append(right.getValue()) // - .append(")"); - - } else if (segment instanceof IsNull) { - - builder.append(left.getValue()); - if (((IsNull) segment).isNegated()) { - builder.append(" IS NOT NULL"); - } else { - builder.append(" IS NULL"); - } - - } else if (segment instanceof Equals) { - - builder.append(left.getValue()).append(" = ").append(right.getValue()); - - } else if (segment instanceof In) { - - builder.append(left.getValue()).append(" IN ").append("(").append(right.getValue()).append(")"); - } - } - - @Override - public String getValue() { - return builder.toString(); - } - } - - private class ExpressionVisitor extends ReadOneVisitor implements ValuedVisitor { - - private String value = ""; - private SelectStatementVisitor valuedVisitor; - - @Override - boolean matches(Visitable segment) { - return segment instanceof Expression; - } - - @Override - void enterMatched(Visitable segment) { - - if (segment instanceof SubselectExpression) { - - valuedVisitor = new SelectStatementVisitor(); - visitors.push(valuedVisitor); - } else if (segment instanceof Column) { - value = ((Column) segment).getTable().getName() + "." + ((Column) segment).getName(); - } else if (segment instanceof BindMarker) { - - if (segment instanceof BindMarker.NamedBindMarker) { - value = ":" + ((BindMarker.NamedBindMarker) segment).getName(); - } else { - value = segment.toString(); - } - } - } - - @Override - void leaveMatched(Visitable segment) { - - if (valuedVisitor != null) { - value = valuedVisitor.getValue(); - } - } - - @Override - public String getValue() { - return value; - } - } - - private class OrderByClauseVisitor extends ReadWhileMatchesVisitor implements ValuedVisitor { - - StringBuilder builder = new StringBuilder(); - boolean first = true; - - @Override - boolean matches(Visitable segment) { - return segment instanceof OrderByField; - } - - @Override - void enterMatched(Visitable segment) { - - if (!first) { - builder.append(", "); - } else { - builder.append(" ORDER BY "); - } - first = false; - } - - @Override - void leaveMatched(Visitable segment) { - - OrderByField field = (OrderByField) segment; - - if (field.getDirection() != null) { - builder.append(" ") // - .append(field.getDirection()); - } - } - - @Override - void leaveSub(Visitable segment) { - - if (segment instanceof Column) { - builder.append(((Column) segment).getReferenceName()); - } - } - - @Override - public String getValue() { - return builder.toString(); - } - } - - } - -} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SelectBuilderUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SelectBuilderUnitTests.java index 1a34dc88..4a446310 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SelectBuilderUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SelectBuilderUnitTests.java @@ -34,7 +34,7 @@ public class SelectBuilderUnitTests { @Test // DATAJDBC-309 public void simpleSelect() { - SelectBuilder builder = SQL.select(); + SelectBuilder builder = StatementBuilder.select(); Table table = SQL.table("mytable"); Column foo = table.column("foo"); @@ -46,13 +46,12 @@ public class SelectBuilderUnitTests { select.visit(visitor); assertThat(visitor.enter).containsSequence(foo, table, bar, table, new From(table), table); - assertThat(visitor.leave).containsSequence(table, foo, table, bar, table, new From(table)); } @Test // DATAJDBC-309 public void selectTop() { - SelectBuilder builder = SQL.select(); + SelectBuilder builder = StatementBuilder.select(); Table table = SQL.table("mytable"); Column foo = table.column("foo"); @@ -69,7 +68,7 @@ public class SelectBuilderUnitTests { @Test // DATAJDBC-309 public void moreAdvancedSelect() { - SelectBuilder builder = SQL.select(); + SelectBuilder builder = StatementBuilder.select(); Table table1 = SQL.table("mytable1"); Table table2 = SQL.table("mytable2"); @@ -88,7 +87,7 @@ public class SelectBuilderUnitTests { @Test // DATAJDBC-309 public void orderBy() { - SelectBuilder builder = SQL.select(); + SelectBuilder builder = StatementBuilder.select(); Table table = SQL.table("mytable"); @@ -106,7 +105,7 @@ public class SelectBuilderUnitTests { @Test // DATAJDBC-309 public void joins() { - SelectBuilder builder = SQL.select(); + SelectBuilder builder = StatementBuilder.select(); Table employee = SQL.table("employee"); Table department = SQL.table("department"); diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SelectValidatorUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SelectValidatorUnitTests.java index 49029b51..d30525bb 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SelectValidatorUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SelectValidatorUnitTests.java @@ -32,7 +32,7 @@ public class SelectValidatorUnitTests { Column column = SQL.table("table").column("foo"); assertThatThrownBy(() -> { - SQL.newSelect(column).from(SQL.table("bar")).build(); + StatementBuilder.select(column).from(SQL.table("bar")).build(); }).isInstanceOf(IllegalStateException.class) .hasMessageContaining("Required table [table] by a SELECT column not imported by FROM [bar] or JOIN []"); } @@ -43,7 +43,7 @@ public class SelectValidatorUnitTests { Column column = SQL.table("table").column("foo"); assertThatThrownBy(() -> { - SQL.newSelect(Functions.count(column)).from(SQL.table("bar")).build(); + StatementBuilder.select(Functions.count(column)).from(SQL.table("bar")).build(); }).isInstanceOf(IllegalStateException.class) .hasMessageContaining("Required table [table] by a SELECT column not imported by FROM [bar] or JOIN []"); } @@ -54,7 +54,7 @@ public class SelectValidatorUnitTests { Column column = SQL.table("table").column("foo"); assertThatThrownBy(() -> { - SQL.newSelect(column).distinct().from(SQL.table("bar")).build(); + StatementBuilder.select(column).distinct().from(SQL.table("bar")).build(); }).isInstanceOf(IllegalStateException.class) .hasMessageContaining("Required table [table] by a SELECT column not imported by FROM [bar] or JOIN []"); } @@ -66,7 +66,7 @@ public class SelectValidatorUnitTests { Table bar = SQL.table("bar"); assertThatThrownBy(() -> { - SQL.newSelect(bar.column("foo")) // + StatementBuilder.select(bar.column("foo")) // .from(bar) // .orderBy(foo) // .build(); @@ -81,7 +81,7 @@ public class SelectValidatorUnitTests { Table bar = SQL.table("bar"); assertThatThrownBy(() -> { - SQL.newSelect(bar.column("foo")) // + StatementBuilder.select(bar.column("foo")) // .from(bar) // .where(new SimpleCondition(column, "=", "foo")) // .build(); diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/ConditionRendererUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/ConditionRendererUnitTests.java new file mode 100644 index 00000000..edf3dc80 --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/ConditionRendererUnitTests.java @@ -0,0 +1,129 @@ +/* + * Copyright 2019 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 + * + * http://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 static org.assertj.core.api.Assertions.*; + +import org.junit.Test; + +import org.springframework.data.relational.core.sql.Column; +import org.springframework.data.relational.core.sql.StatementBuilder; +import org.springframework.data.relational.core.sql.Table; + +/** + * Unit tests for rendered {@link org.springframework.data.relational.core.sql.Conditions}. + * + * @author Mark Paluch + */ +public class ConditionRendererUnitTests { + + Table table = Table.create("my_table"); + Column left = table.column("left"); + Column right = table.column("right"); + + @Test // DATAJDBC-309 + public void shouldRenderEquals() { + + String sql = SqlRenderer + .render(StatementBuilder.select(left).from(table).where(left.isEqualTo(right)).build()); + + assertThat(sql).endsWith("WHERE my_table.left = my_table.right"); + } + + @Test // DATAJDBC-309 + public void shouldRenderNotEquals() { + + String sql = SqlRenderer + .render(StatementBuilder.select(left).from(table).where(left.isNotEqualTo(right)).build()); + + assertThat(sql).endsWith("WHERE my_table.left != my_table.right"); + + sql = SqlRenderer.render(StatementBuilder.select(left).from(table).where(left.isEqualTo(right).not()).build()); + + assertThat(sql).endsWith("WHERE my_table.left != my_table.right"); + } + + @Test // DATAJDBC-309 + public void shouldRenderIsLess() { + + String sql = SqlRenderer.render(StatementBuilder.select(left).from(table).where(left.isLess(right)).build()); + + assertThat(sql).endsWith("WHERE my_table.left < my_table.right"); + } + + @Test // DATAJDBC-309 + public void shouldRenderIsLessOrEqualTo() { + + String sql = SqlRenderer + .render(StatementBuilder.select(left).from(table).where(left.isLessOrEqualTo(right)).build()); + + assertThat(sql).endsWith("WHERE my_table.left <= my_table.right"); + } + + @Test // DATAJDBC-309 + public void shouldRenderIsGreater() { + + String sql = SqlRenderer + .render(StatementBuilder.select(left).from(table).where(left.isGreater(right)).build()); + + assertThat(sql).endsWith("WHERE my_table.left > my_table.right"); + } + + @Test // DATAJDBC-309 + public void shouldRenderIsGreaterOrEqualTo() { + + String sql = SqlRenderer + .render(StatementBuilder.select(left).from(table).where(left.isGreaterOrEqualTo(right)).build()); + + assertThat(sql).endsWith("WHERE my_table.left >= my_table.right"); + } + + @Test // DATAJDBC-309 + public void shouldRenderIn() { + + String sql = SqlRenderer.render(StatementBuilder.select(left).from(table).where(left.in(right)).build()); + + assertThat(sql).endsWith("WHERE my_table.left IN (my_table.right)"); + } + + @Test // DATAJDBC-309 + public void shouldRenderLike() { + + String sql = SqlRenderer.render(StatementBuilder.select(left).from(table).where(left.like(right)).build()); + + assertThat(sql).endsWith("WHERE my_table.left LIKE my_table.right"); + } + + @Test // DATAJDBC-309 + public void shouldRenderIsNull() { + + String sql = SqlRenderer.render(StatementBuilder.select(left).from(table).where(left.isNull()).build()); + + assertThat(sql).endsWith("WHERE my_table.left IS NULL"); + } + + @Test // DATAJDBC-309 + public void shouldRenderIsNotNull() { + + String sql = SqlRenderer.render(StatementBuilder.select(left).from(table).where(left.isNotNull()).build()); + + assertThat(sql).endsWith("WHERE my_table.left IS NOT NULL"); + + sql = SqlRenderer.render(StatementBuilder.select(left).from(table).where(left.isNull().not()).build()); + + assertThat(sql).endsWith("WHERE my_table.left IS NOT NULL"); + } +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/OrderByClauseVisitorUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/OrderByClauseVisitorUnitTests.java new file mode 100644 index 00000000..2c7a2272 --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/OrderByClauseVisitorUnitTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2019 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 + * + * http://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 static org.assertj.core.api.Assertions.*; + +import org.junit.Test; +import org.springframework.data.relational.core.sql.Column; +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.Table; + +/** + * Unit tests for {@link OrderByClauseVisitor}. + * + * @author Mark Paluch + */ +public class OrderByClauseVisitorUnitTests { + + @Test // DATAJDBC-309 + public void shouldRenderOrderByName() { + + Table employee = SQL.table("employee").as("emp"); + Column column = employee.column("name").as("emp_name"); + + Select select = Select.builder().select(column).from(employee).orderBy(OrderByField.from(column).asc()).build(); + + OrderByClauseVisitor visitor = new OrderByClauseVisitor(new SimpleRenderContext(NamingStrategies.asIs())); + select.visit(visitor); + + assertThat(visitor.getRenderedPart().toString()).isEqualTo("emp_name ASC"); + } + + @Test // DATAJDBC-309 + public void shouldApplyNamingStrategy() { + + Table employee = SQL.table("employee").as("emp"); + Column column = employee.column("name").as("emp_name"); + + Select select = Select.builder().select(column).from(employee).orderBy(OrderByField.from(column).asc()).build(); + + OrderByClauseVisitor visitor = new OrderByClauseVisitor(new SimpleRenderContext(NamingStrategies.toUpper())); + select.visit(visitor); + + assertThat(visitor.getRenderedPart().toString()).isEqualTo("EMP_NAME ASC"); + } +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/NaiveSqlRendererUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/SqlRendererUnitTests.java similarity index 61% rename from spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/NaiveSqlRendererUnitTests.java rename to spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/SqlRendererUnitTests.java index 7cd83be4..42920236 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/NaiveSqlRendererUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/SqlRendererUnitTests.java @@ -13,19 +13,28 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.relational.core.sql; +package org.springframework.data.relational.core.sql.render; import static org.assertj.core.api.Assertions.*; -import org.junit.Ignore; import org.junit.Test; +import org.springframework.data.relational.core.sql.Column; +import org.springframework.data.relational.core.sql.Conditions; +import org.springframework.data.relational.core.sql.Functions; +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.Table; +import org.springframework.util.StringUtils; + /** - * Unit tests for {@link NaiveSqlRenderer}. + * Unit tests for {@link SqlRenderer}. * * @author Mark Paluch + * @author Jens Schauder */ -public class NaiveSqlRendererUnitTests { +public class SqlRendererUnitTests { @Test // DATAJDBC-309 public void shouldRenderSingleColumn() { @@ -35,7 +44,7 @@ public class NaiveSqlRendererUnitTests { Select select = Select.builder().select(foo).from(bar).build(); - assertThat(NaiveSqlRenderer.render(select)).isEqualTo("SELECT bar.foo FROM bar"); + assertThat(SqlRenderer.render(select)).isEqualTo("SELECT bar.foo FROM bar"); } @Test // DATAJDBC-309 @@ -45,7 +54,7 @@ public class NaiveSqlRendererUnitTests { Select select = Select.builder().select(table.column("foo").as("my_foo")).from(table).build(); - assertThat(NaiveSqlRenderer.render(select)).isEqualTo("SELECT my_bar.foo AS my_foo FROM bar AS my_bar"); + assertThat(SqlRenderer.render(select)).isEqualTo("SELECT my_bar.foo AS my_foo FROM bar AS my_bar"); } @Test // DATAJDBC-309 @@ -57,7 +66,7 @@ public class NaiveSqlRendererUnitTests { Select select = Select.builder().select(table1.column("col1")).select(table2.column("col2")).from(table1) .from(table2).build(); - assertThat(NaiveSqlRenderer.render(select)).isEqualTo("SELECT table1.col1, table2.col2 FROM table1, table2"); + assertThat(SqlRenderer.render(select)).isEqualTo("SELECT table1.col1, table2.col2 FROM table1, table2"); } @Test // DATAJDBC-309 @@ -69,7 +78,7 @@ public class NaiveSqlRendererUnitTests { Select select = Select.builder().distinct().select(foo, bar).from(table).build(); - assertThat(NaiveSqlRenderer.render(select)).isEqualTo("SELECT DISTINCT bar.foo, bar.bar FROM bar"); + assertThat(SqlRenderer.render(select)).isEqualTo("SELECT DISTINCT bar.foo, bar.bar FROM bar"); } @Test // DATAJDBC-309 @@ -81,7 +90,7 @@ public class NaiveSqlRendererUnitTests { Select select = Select.builder().select(Functions.count(foo), bar).from(table).build(); - assertThat(NaiveSqlRenderer.render(select)).isEqualTo("SELECT COUNT(bar.foo), bar.bar FROM bar"); + assertThat(SqlRenderer.render(select)).isEqualTo("SELECT COUNT(bar.foo), bar.bar FROM bar"); } @Test // DATAJDBC-309 @@ -94,7 +103,7 @@ public class NaiveSqlRendererUnitTests { .join(department).on(employee.column("department_id")).equals(department.column("id")) // .build(); - assertThat(NaiveSqlRenderer.render(select)).isEqualTo("SELECT employee.id, department.name FROM employee " + assertThat(SqlRenderer.render(select)).isEqualTo("SELECT employee.id, department.name FROM employee " + "JOIN department ON employee.department_id = department.id"); } @@ -109,7 +118,7 @@ public class NaiveSqlRendererUnitTests { .and(employee.column("tenant")).equals(department.column("tenant")) // .build(); - assertThat(NaiveSqlRenderer.render(select)).isEqualTo("SELECT employee.id, department.name FROM employee " + assertThat(SqlRenderer.render(select)).isEqualTo("SELECT employee.id, department.name FROM employee " + "JOIN department ON employee.department_id = department.id " + "AND employee.tenant = department.tenant"); } @@ -126,7 +135,7 @@ public class NaiveSqlRendererUnitTests { .join(tenant).on(tenant.column("tenant_id")).equals(department.column("tenant")) // .build(); - assertThat(NaiveSqlRenderer.render(select)).isEqualTo("SELECT employee.id, department.name FROM employee " + assertThat(SqlRenderer.render(select)).isEqualTo("SELECT employee.id, department.name FROM employee " + "JOIN department ON employee.department_id = department.id " + "AND employee.tenant = department.tenant " + "JOIN tenant AS tenant_base ON tenant_base.tenant_id = department.tenant"); } @@ -139,7 +148,7 @@ public class NaiveSqlRendererUnitTests { Select select = Select.builder().select(column).from(employee).orderBy(OrderByField.from(column).asc()).build(); - assertThat(NaiveSqlRenderer.render(select)) + assertThat(SqlRenderer.render(select)) .isEqualTo("SELECT emp.name AS emp_name FROM employee AS emp ORDER BY emp_name ASC"); } @@ -151,7 +160,7 @@ public class NaiveSqlRendererUnitTests { Select select = Select.builder().select(bar).from("foo").limitOffset(10, 20).build(); - assertThat(NaiveSqlRenderer.render(select)).isEqualTo("SELECT foo.bar FROM foo LIMIT 10 OFFSET 20"); + assertThat(SqlRenderer.render(select)).isEqualTo("SELECT foo.bar FROM foo LIMIT 10 OFFSET 20"); } @Test // DATAJDBC-309 @@ -162,7 +171,7 @@ public class NaiveSqlRendererUnitTests { Select select = Select.builder().select(bar).from(table).where(Conditions.isNull(bar)).build(); - assertThat(NaiveSqlRenderer.render(select)).isEqualTo("SELECT foo.bar FROM foo WHERE foo.bar IS NULL"); + assertThat(SqlRenderer.render(select)).isEqualTo("SELECT foo.bar FROM foo WHERE foo.bar IS NULL"); } @Test // DATAJDBC-309 @@ -173,7 +182,7 @@ public class NaiveSqlRendererUnitTests { Select select = Select.builder().select(bar).from(table).where(Conditions.isNull(bar).not()).build(); - assertThat(NaiveSqlRenderer.render(select)).isEqualTo("SELECT foo.bar FROM foo WHERE foo.bar IS NOT NULL"); + assertThat(SqlRenderer.render(select)).isEqualTo("SELECT foo.bar FROM foo WHERE foo.bar IS NOT NULL"); } @Test // DATAJDBC-309 @@ -182,9 +191,10 @@ public class NaiveSqlRendererUnitTests { Table table = SQL.table("foo"); Column bar = table.column("bar"); - Select select = Select.builder().select(bar).from(table).where(Conditions.isEqual(bar, new BindMarker.NamedBindMarker("name"))).build(); + Select select = Select.builder().select(bar).from(table).where(Conditions.isEqual(bar, SQL.bindMarker(":name"))) + .build(); - assertThat(NaiveSqlRenderer.render(select)).isEqualTo("SELECT foo.bar FROM foo WHERE foo.bar = :name"); + assertThat(SqlRenderer.render(select)).isEqualTo("SELECT foo.bar FROM foo WHERE foo.bar = :name"); } @Test // DATAJDBC-309 @@ -194,13 +204,11 @@ public class NaiveSqlRendererUnitTests { Column bar = table.column("bar"); Column baz = table.column("baz"); - Select select = Select.builder().select(bar).from(table).where( - Conditions.isEqual(bar, new BindMarker.NamedBindMarker("name")) - .or(Conditions.isEqual(bar, new BindMarker.NamedBindMarker("name2"))) - .and(Conditions.isNull(baz)) - ).build(); + Select select = Select.builder().select(bar).from(table).where(Conditions.isEqual(bar, SQL.bindMarker(":name")) + .or(Conditions.isEqual(bar, SQL.bindMarker(":name2"))).and(Conditions.isNull(baz))).build(); - assertThat(NaiveSqlRenderer.render(select)).isEqualTo("SELECT foo.bar FROM foo WHERE (foo.bar = :name OR foo.bar = :name2) AND foo.baz IS NULL"); + assertThat(SqlRenderer.render(select)) + .isEqualTo("SELECT foo.bar FROM foo WHERE foo.bar = :name OR foo.bar = :name2 AND foo.baz IS NULL"); } @Test // DATAJDBC-309 @@ -209,11 +217,21 @@ public class NaiveSqlRendererUnitTests { Table table = SQL.table("foo"); Column bar = table.column("bar"); - Select select = Select.builder().select(bar).from(table).where( - Conditions.in(bar, new BindMarker.NamedBindMarker("name")) - ).build(); + Select select = Select.builder().select(bar).from(table).where(Conditions.in(bar, SQL.bindMarker(":name"))).build(); - assertThat(NaiveSqlRenderer.render(select)).isEqualTo("SELECT foo.bar FROM foo WHERE foo.bar IN (:name)"); + assertThat(SqlRenderer.render(select)).isEqualTo("SELECT foo.bar FROM foo WHERE foo.bar IN (:name)"); + } + + @Test // DATAJDBC-309 + public void shouldInWithNamedParameters() { + + Table table = SQL.table("foo"); + Column bar = table.column("bar"); + + Select select = Select.builder().select(bar).from(table) + .where(Conditions.in(bar, SQL.bindMarker(":name"), SQL.bindMarker(":name2"))).build(); + + assertThat(SqlRenderer.render(select)).isEqualTo("SELECT foo.bar FROM foo WHERE foo.bar IN (:name, :name2)"); } @Test // DATAJDBC-309 @@ -227,8 +245,30 @@ public class NaiveSqlRendererUnitTests { Select subselect = Select.builder().select(bah).from(floo).build(); - Select select = Select.builder().select(bar).from(foo).where(Conditions.in(bar, new SubselectExpression(subselect))).build(); + Select select = Select.builder().select(bar).from(foo).where(Conditions.in(bar, subselect)).build(); - assertThat(NaiveSqlRenderer.render(select)).isEqualTo("SELECT foo.bar FROM foo WHERE foo.bar IN (SELECT floo.bah FROM floo)"); + assertThat(SqlRenderer.render(select)) + .isEqualTo("SELECT foo.bar FROM foo WHERE foo.bar IN (SELECT floo.bah FROM floo)"); } + + @Test // DATAJDBC-309 + public void shouldConsiderNamingStrategy() { + + Table foo = SQL.table("Foo"); + Column bar = foo.column("BaR"); + Column baz = foo.column("BaZ"); + + Select select = Select.builder().select(bar).from(foo).where(bar.isEqualTo(baz)).build(); + + String upper = SqlRenderer.create(select, new SimpleRenderContext(NamingStrategies.toUpper())).render(); + assertThat(upper).isEqualTo("SELECT FOO.BAR FROM FOO WHERE FOO.BAR = FOO.BAZ"); + + String lower = SqlRenderer.create(select, new SimpleRenderContext(NamingStrategies.toLower())).render(); + assertThat(lower).isEqualTo("SELECT foo.bar FROM foo WHERE foo.bar = foo.baz"); + + String mapped = SqlRenderer + .create(select, new SimpleRenderContext(NamingStrategies.mapWith(StringUtils::uncapitalize))).render(); + assertThat(mapped).isEqualTo("SELECT foo.baR FROM foo WHERE foo.baR = foo.baZ"); + } + }