DATAJDBC-309 - Polishing.

Javadoc, static factory methods, typos. Refactor SQL rendering from a shared stack-based implementation to independent delegating visitors. Introduce DelegatingVisitor and TypedSubtreeVisitor base classes. Introduce SelectList container. Extract nested renderes to top-level types.

Move SQL renderer to renderer package.

Extend In to multi-expression argument. Introduce helper methods in Table to create multiple columns. Introduce factory method on StatementBuilder to create a new builder given a collection of expressions.

Add support for comparison conditions and LIKE and equal/not equal/less with equals to/greater with equals to conditions.

 Add condition creation methods to Column so Column objects can now create conditions for a fluent DSL as in (.where(left.isGreater(right)).

StatementBuilder.select(left).from(table).where(left.isGreater(right)).build().

Introduce RenderContext and RenderNamingStrategy.

Add since tags. Improve Javadoc.

Original pull request: #119.
This commit is contained in:
Mark Paluch
2019-01-31 14:57:45 +01:00
parent 1c9fad853f
commit 307fe5198e
74 changed files with 3569 additions and 901 deletions

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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}.
* <p/>
* * Renders to: {@code <table>.*} as in {@code SELECT <table>.* FROM …}.
* * Renders to: {@code
*
<table>
* .*} as in {@code SELECT
*
<table>
* .* 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() {

View File

@@ -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 {

View File

@@ -24,6 +24,7 @@ import org.springframework.util.Assert;
* Renders to: {@code <name>} or {@code <table(alias)>.<name>}.
*
* @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()

View File

@@ -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.
* <p/>
* Results in a rendered condition: {@code <left> <comparator> <right>} (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();
}
}

View File

@@ -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);
}

View File

@@ -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<? extends 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, 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() {}
}

View File

@@ -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<Expression> 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<OrderByField> orderBy;
DefaultSelect(boolean distinct, List<Expression> selectList, List<Table> from, long limit, long offset,
List<Join> joins, @Nullable Condition where, List<OrderByField> orderBy) {
List<Join> joins, @Nullable Condition where, List<OrderByField> 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));

View File

@@ -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<Join> joins = new ArrayList<>();
private Condition where;
private @Nullable Condition where;
private List<OrderByField> 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);
}
}

View File

@@ -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 {}

View File

@@ -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 {
}
}
}

View File

@@ -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<Table> tables) {
super(tables.toArray(new Table[]{}));
super(tables.toArray(new Table[] {}));
this.tables = tables;
}

View File

@@ -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.

View File

@@ -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<Expression> expressions;
public In(Expression left, Expression right) {
private In(Expression left, Collection<Expression> expressions) {
super(left, right);
super(toArray(left, expressions));
this.left = left;
this.right = right;
this.expressions = expressions;
}
private static Segment[] toArray(Expression expression, Collection<Expression> 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<? extends 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, 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, ", ") + ")";
}
}

View File

@@ -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;
}
}

View File

@@ -19,10 +19,12 @@ package org.springframework.data.relational.core.sql;
* {@link Segment} for a {@code JOIN} declaration.
* <p/>
* Renders to: {@code JOIN
* <table>
*
<table>
* ON <condition>}.
*
* @author Mark Paluch
* @since 1.1
*/
public class Join extends AbstractSegment {

View File

@@ -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.
* <p/>
* Results in a rendered condition: {@code <left> = <right>}.
* Results in a rendered condition: {@code <left> LIKE <right>}.
*
* @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();
}
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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();

View File

@@ -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 {

View File

@@ -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);
}

View File

@@ -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.
* <p/>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.
* <p/>
* 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() {}
}

View File

@@ -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}.
* <p/>
* 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}.
* <p/>
* 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}.
*/

View File

@@ -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:
* <ol>
* <li>Self</li>
* <li>{@link Column SELECT columns} </li>
* <li>{@link Column SELECT columns}</li>
* <li>{@link Table FROM tables} clause</li>
* <li>{@link Join JOINs}</li>
* <li>{@link Condition WHERE} condition</li>
@@ -30,12 +29,13 @@ import java.util.OptionalLong;
* </ol>
*
* @author Mark Paluch
* @since 1.1
* @see StatementBuilder
* @see SelectBuilder
* @see SQL
*/
public interface Select extends Segment, Visitable {
/**
* Creates a new {@link SelectBuilder}.
*

View File

@@ -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<? extends OrderByField> 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<? extends Table> 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.
*/

View File

@@ -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<Expression> selectList;
SelectList(List<Expression> 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, ", ");
}
}

View File

@@ -19,7 +19,13 @@ import java.util.HashSet;
import java.util.Set;
/**
* Validator for {@link Select} statements.
* <p/>
* 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();

View File

@@ -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 {

View File

@@ -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<Expression> expressions;
SimpleFunction(String functionName, List<Expression> expressions) {
private SimpleFunction(String functionName, List<Expression> 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<Expression> 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;

View File

@@ -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;
}

View File

@@ -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<? extends 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();
}
private StatementBuilder() {
}
}

View File

@@ -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() + ")";

View File

@@ -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 <name>} or {@code <name> AS <name>}.
*
* @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}.
* <p/>
* 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}.
* <p/>
* 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}.
* <p/>
* 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<Column> columns(Collection<String> names) {
Assert.notNull(names, "Names must not be null");
List<Column> 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
* <table>
*
<table>
* .*}.
*
* @return the select all marker for this {@link Table}.

View File

@@ -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}.
*/

View File

@@ -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) {}
}

View File

@@ -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();

View File

@@ -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.
* <p/> 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.
* <p/>
* 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;

View File

@@ -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);
}
}

View File

@@ -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<Condition> 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;
}
}

View File

@@ -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.
* <p/>
* <h3>Delegation</h3> 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.
* <p/>
* 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.
* <p/>
* 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<DelegatingVisitor> delegation = new Stack<>();
/**
* Invoked for a {@link Visitable segment} when entering the segment.
* <p/>
* 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.
* <p/>
* 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;
}
}
}

View File

@@ -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<Expression> 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;
}
}

View File

@@ -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<Visitable> 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();
}
}

View File

@@ -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}.
* <p/>
* Filtering is a three-way process:
* <ol>
* <li>Ignores elements that do not match the filter {@link Predicate}.</li>
* <li>{@link #enterMatched(Visitable) enter}/{@link #leaveMatched(Visitable) leave} matched callbacks for the
* {@link Visitable segment} that matches the {@link Predicate}.</li>
* <li>{@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}.</li>
* </ol>
*
* @author Mark Paluch
* @see TypedSubtreeVisitor
* @since 1.1
*/
abstract class FilteredSubtreeVisitor extends DelegatingVisitor {
private final Predicate<Visitable> 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<Visitable> 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);
}
}
}

View File

@@ -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<From> {
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);
}
}

View File

@@ -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<Table> {
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);
}
}

View File

@@ -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<In> {
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);
}
}

View File

@@ -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<IsNull> {
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);
}
}

View File

@@ -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<Join> {
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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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<String, String> 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<String, String> 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));
}
}
}

View File

@@ -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<OrderByField> 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;
}
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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<String, String> mappingFunction) {
Assert.notNull(mappingFunction, "Mapping function must not be null!");
return new DelegatingRenderNamingStrategy(this, mappingFunction);
}
}

View File

@@ -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);
}

View File

@@ -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<SelectList> 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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<T extends Visitable & Condition> extends TypedSubtreeVisitor<T> {
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();
}
}

View File

@@ -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}.
* <p/>
* Filtering is a three-way process:
* <ol>
* <li>Ignores elements that do not match the filter {@link Predicate}.</li>
* <li>{@link #enterMatched(Visitable) enter}/{@link #leaveMatched(Visitable) leave} matched callbacks for the
* {@link Visitable segment} that matches the {@link Predicate}.</li>
* <li>{@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}.</li>
* </ol>
*
* @author Mark Paluch
* @since 1.1
* @see FilteredSubtreeVisitor
*/
abstract class TypedSubtreeVisitor<T extends Visitable> 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);
}
}
}

View File

@@ -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<Where> {
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);
}
}

View File

@@ -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;

View File

@@ -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<Visitor> 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();
}
}
}
}

View File

@@ -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");

View File

@@ -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();

View File

@@ -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");
}
}

View File

@@ -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");
}
}

View File

@@ -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");
}
}