diff --git a/pom.xml b/pom.xml
index 3d53c917f4..f17ed3190a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.springframework.data
spring-data-relational-parent
- 1.1.0.BUILD-SNAPSHOT
+ 1.1.0.DATAJDBC-309-SNAPSHOT
pom
Spring Data Relational Parent
diff --git a/spring-data-jdbc-distribution/pom.xml b/spring-data-jdbc-distribution/pom.xml
index 1753776a73..81ed77b86d 100644
--- a/spring-data-jdbc-distribution/pom.xml
+++ b/spring-data-jdbc-distribution/pom.xml
@@ -14,7 +14,7 @@
org.springframework.data
spring-data-relational-parent
- 1.1.0.BUILD-SNAPSHOT
+ 1.1.0.DATAJDBC-309-SNAPSHOT
../pom.xml
diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml
index 9458230173..c09017b952 100644
--- a/spring-data-jdbc/pom.xml
+++ b/spring-data-jdbc/pom.xml
@@ -5,7 +5,7 @@
4.0.0
spring-data-jdbc
- 1.1.0.BUILD-SNAPSHOT
+ 1.1.0.DATAJDBC-309-SNAPSHOT
Spring Data JDBC
Spring Data module for JDBC repositories.
@@ -14,7 +14,7 @@
org.springframework.data
spring-data-relational-parent
- 1.1.0.BUILD-SNAPSHOT
+ 1.1.0.DATAJDBC-309-SNAPSHOT
diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml
index 11e7db737c..d630ff1c76 100644
--- a/spring-data-relational/pom.xml
+++ b/spring-data-relational/pom.xml
@@ -5,7 +5,7 @@
4.0.0
spring-data-relational
- 1.1.0.BUILD-SNAPSHOT
+ 1.1.0.DATAJDBC-309-SNAPSHOT
Spring Data Relational
Spring Data Relational support
@@ -13,7 +13,7 @@
org.springframework.data
spring-data-relational-parent
- 1.1.0.BUILD-SNAPSHOT
+ 1.1.0.DATAJDBC-309-SNAPSHOT
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AbstractSegment.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AbstractSegment.java
new file mode 100644
index 0000000000..3307090573
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AbstractSegment.java
@@ -0,0 +1,67 @@
+/*
+ * 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;
+
+/**
+ * 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) {
+ 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) {
+
+ Assert.notNull(visitor, "Visitor must not be null!");
+
+ visitor.enter(this);
+ for (Segment child : children) {
+ child.visit(visitor);
+ }
+ visitor.leave(this);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.lang.Object#hashCode()
+ */
+ @Override
+ public int hashCode() {
+ return toString().hashCode();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.lang.Object#equals(java.lang.Object)
+ */
+ @Override
+ public boolean equals(Object obj) {
+ return obj instanceof Segment && toString().equals(obj.toString());
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Aliased.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Aliased.java
new file mode 100644
index 0000000000..829177fafc
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Aliased.java
@@ -0,0 +1,30 @@
+/*
+ * 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;
+
+/**
+ * Aliased element exposing an {@link #getAlias() alias}.
+ *
+ * @author Mark Paluch
+ * @since 1.1
+ */
+public interface Aliased {
+
+ /**
+ * @return the alias name.
+ */
+ String getAlias();
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AliasedExpression.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AliasedExpression.java
new file mode 100644
index 0000000000..1c926741eb
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AliasedExpression.java
@@ -0,0 +1,54 @@
+/*
+ * 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;
+
+/**
+ * An expression with an alias.
+ *
+ * @author Jens Schauder
+ * @since 1.1
+ */
+class AliasedExpression extends AbstractSegment implements Aliased, Expression {
+
+ private final Expression expression;
+ private final String alias;
+
+ public AliasedExpression(Expression expression, String alias) {
+
+ super(expression);
+
+ this.expression = expression;
+ this.alias = alias;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.Aliased#getAlias()
+ */
+ @Override
+ public String getAlias() {
+ return alias;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return expression.toString() + " AS " + alias;
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AndCondition.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AndCondition.java
new file mode 100644
index 0000000000..4447eed1e7
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AndCondition.java
@@ -0,0 +1,30 @@
+/*
+ * 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;
+
+/**
+ * {@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 {
+
+ AndCondition(Condition... conditions) {
+ super(" AND ", conditions);
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AsteriskFromTable.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AsteriskFromTable.java
new file mode 100644
index 0000000000..f2803ade0b
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AsteriskFromTable.java
@@ -0,0 +1,66 @@
+/*
+ * 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;
+
+/**
+ * {@link Segment} to select all columns from a {@link Table}.
+ *
+ * * Renders to: {@code
+ *
+
+ * .*} as in {@code SELECT
+ *
+
+ * .* FROM …}.
+ *
+ * @author Mark Paluch
+ * @since 1.1
+ * @see Table#asterisk()
+ */
+public class AsteriskFromTable extends AbstractSegment implements Expression {
+
+ private final Table table;
+
+ AsteriskFromTable(Table table) {
+ super(table);
+ this.table = table;
+ }
+
+ public static AsteriskFromTable create(Table table) {
+ return new AsteriskFromTable(table);
+ }
+
+ /**
+ * @return the associated {@link Table}.
+ */
+ public Table getTable() {
+ return table;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+
+ if (table instanceof Aliased) {
+ return ((Aliased) table).getAlias() + ".*";
+ }
+
+ return table.toString() + ".*";
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/BindMarker.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/BindMarker.java
new file mode 100644
index 0000000000..16957cdebe
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/BindMarker.java
@@ -0,0 +1,64 @@
+/*
+ * 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.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 {
+
+ /*
+ * (non-Javadoc)
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return "?";
+ }
+
+ static class NamedBindMarker extends BindMarker implements Named {
+
+ private final String name;
+
+ NamedBindMarker(String name) {
+ this.name = name;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.Named#getName()
+ */
+ @Nullable
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.BindMarker#toString()
+ */
+ @Override
+ public String toString() {
+ return "?[" + name + "]";
+ }
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Column.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Column.java
new file mode 100644
index 0000000000..356a8e83b4
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Column.java
@@ -0,0 +1,298 @@
+/*
+ * 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.lang.Nullable;
+import org.springframework.util.Assert;
+
+/**
+ * Column name within a {@code SELECT … FROM} clause.
+ *
+ * Renders to: {@code } or {@code .}.
+ *
+ * @author Mark Paluch
+ * @since 1.1
+ */
+public class Column extends AbstractSegment implements Expression, Named {
+
+ private final String name;
+ private final Table table;
+
+ Column(String name, Table table) {
+
+ super(table);
+ Assert.notNull(name, "Name must not be null");
+
+ this.name = name;
+ this.table = table;
+ }
+
+ /**
+ * Creates a new {@link Column} associated with a {@link Table}.
+ *
+ * @param name column name, must not {@literal null} or empty.
+ * @param table the table, must not be {@literal null}.
+ * @return the new {@link Column}.
+ */
+ public static Column create(String name, Table table) {
+
+ Assert.hasText(name, "Name must not be null or empty");
+ Assert.notNull(table, "Table must not be null");
+
+ return new Column(name, table);
+ }
+
+ /**
+ * Creates a new aliased {@link Column} associated with a {@link Table}.
+ *
+ * @param name column name, must not {@literal null} or empty.
+ * @param table the table, must not be {@literal null}.
+ * @param alias column alias name, must not {@literal null} or empty.
+ * @return the new {@link Column}.
+ */
+ public static Column aliased(String name, Table table, String alias) {
+
+ Assert.hasText(name, "Name must not be null or empty");
+ Assert.notNull(table, "Table must not be null");
+ Assert.hasText(alias, "Alias must not be null or empty");
+
+ return new AliasedColumn(name, table, alias);
+ }
+
+ /**
+ * Creates a new aliased {@link Column}.
+ *
+ * @param alias column alias name, must not {@literal null} or empty.
+ * @return the aliased {@link Column}.
+ */
+ public Column as(String alias) {
+
+ Assert.hasText(alias, "Alias must not be null or empty");
+
+ return new AliasedColumn(name, table, alias);
+ }
+
+ /**
+ * 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}.
+ */
+ public Column from(Table table) {
+
+ Assert.notNull(table, "Table must not be null");
+
+ 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()
+ */
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * @return the column name as it is used in references. This can be the actual {@link #getName() name} or an
+ * {@link Aliased#getAlias() alias}.
+ */
+ public String getReferenceName() {
+ return name;
+ }
+
+ /**
+ * @return the {@link Table}. Can be {@literal null} if the column was not referenced in the context of a
+ * {@link Table}.
+ */
+ @Nullable
+ public Table getTable() {
+ return table;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+
+ return getPrefix() + name;
+ }
+
+ String getPrefix() {
+ String prefix = "";
+ if (table != null) {
+ prefix = (table instanceof Aliased ? ((Aliased) table).getAlias() : table.getName()) + ".";
+ }
+ return prefix;
+ }
+
+ /**
+ * {@link Aliased} {@link Column} implementation.
+ */
+ static class AliasedColumn extends Column implements Aliased {
+
+ private final String alias;
+
+ private AliasedColumn(String name, Table table, String alias) {
+ super(name, table);
+ this.alias = alias;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.Aliased#getAlias()
+ */
+ @Override
+ public String getAlias() {
+ return alias;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.Column#getReferenceName()
+ */
+ @Override
+ public String getReferenceName() {
+ return getAlias();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.Column#from(org.springframework.data.relational.core.sql.Table)
+ */
+ @Override
+ public Column from(Table table) {
+
+ Assert.notNull(table, "Table must not be null");
+
+ return new AliasedColumn(getName(), table, getAlias());
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.Column#toString()
+ */
+ @Override
+ public String toString() {
+ return getPrefix() + getName() + " AS " + getAlias();
+ }
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Comparison.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Comparison.java
new file mode 100644
index 0000000000..a5641c2335
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Comparison.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.relational.core.sql;
+
+import org.springframework.util.Assert;
+
+/**
+ * Comparing {@link Condition} comparing two {@link Expression}s.
+ *
+ * Results in a rendered condition: {@code } (e.g. {@code col = 'predicate'}.
+ *
+ * @author Mark Paluch
+ * @since 1.1
+ */
+public class Comparison extends AbstractSegment implements Condition {
+
+ private final Expression left;
+ private final String comparator;
+ private final Expression right;
+
+ private Comparison(Expression left, String comparator, Expression right) {
+
+ super(left, right);
+
+ this.left = left;
+ this.comparator = comparator;
+ this.right = right;
+ }
+
+ /**
+ * Creates a new {@link Comparison} {@link Condition} given two {@link Expression}s.
+ *
+ * @param leftColumnOrExpression the left {@link Expression}.
+ * @param comparator the comparator.
+ * @param rightColumnOrExpression the right {@link Expression}.
+ * @return the {@link Comparison} condition.
+ */
+ public static Comparison create(Expression leftColumnOrExpression, String comparator,
+ Expression rightColumnOrExpression) {
+
+ Assert.notNull(leftColumnOrExpression, "Left expression must not be null!");
+ Assert.notNull(comparator, "Comparator must not be null!");
+ Assert.notNull(rightColumnOrExpression, "Right expression must not be null!");
+
+ return new Comparison(leftColumnOrExpression, comparator, rightColumnOrExpression);
+ }
+
+ @Override
+ public Condition not() {
+
+ if ("=".equals(comparator)) {
+ return new Comparison(left, "!=", right);
+ }
+
+ if ("!=".equals(comparator)) {
+ return new Comparison(left, "=", right);
+ }
+
+ return new Not(this);
+ }
+
+ /**
+ * @return the left {@link Expression}.
+ */
+ public Expression getLeft() {
+ return left;
+ }
+
+ /**
+ * @return the comparator.
+ */
+ public String getComparator() {
+ return comparator;
+ }
+
+ /**
+ * @return the right {@link Expression}.
+ */
+ public Expression getRight() {
+ return right;
+ }
+
+ @Override
+ public String toString() {
+ return left.toString() + " " + comparator + " " + right.toString();
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Condition.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Condition.java
new file mode 100644
index 0000000000..b7799392b5
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Condition.java
@@ -0,0 +1,56 @@
+/*
+ * 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;
+
+/**
+ * AST {@link Segment} for a condition.
+ *
+ * @author Mark Paluch
+ * @author Jens Schauder
+ * @since 1.1
+ * @see Conditions
+ */
+public interface Condition extends Segment {
+
+ /**
+ * Combine another {@link Condition} using {@code AND}.
+ *
+ * @param other the other {@link Condition}.
+ * @return the combined {@link Condition}.
+ */
+ default Condition and(Condition other) {
+ return new AndCondition(this, other);
+ }
+
+ /**
+ * Combine another {@link Condition} using {@code OR}.
+ *
+ * @param other the other {@link Condition}.
+ * @return the combined {@link Condition}.
+ */
+ default Condition or(Condition other) {
+ return new OrCondition(this, other);
+ }
+
+ /**
+ * Creates a {@link Condition} that negates this {@link Condition}.
+ *
+ * @return the negated {@link Condition}.
+ */
+ default Condition not() {
+ return new Not(this);
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Conditions.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Conditions.java
new file mode 100644
index 0000000000..bee428a941
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Conditions.java
@@ -0,0 +1,211 @@
+/*
+ * 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.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 {
+
+ /**
+ * Creates a plain {@code sql} {@link Condition}.
+ *
+ * @param sql the SQL, must not be {@literal null} or empty.
+ * @return a SQL {@link Expression}.
+ */
+ public static Condition just(String sql) {
+ return new ConstantCondition(sql);
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * 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 {
+
+ private final String condition;
+
+ ConstantCondition(String condition) {
+ this.condition = condition;
+ }
+
+ @Override
+ public String toString() {
+ return condition;
+ }
+ }
+
+ // Utility constructor.
+ private Conditions() {}
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelect.java
new file mode 100644
index 0000000000..6bd8f8d5af
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelect.java
@@ -0,0 +1,106 @@
+/*
+ * 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.ArrayList;
+import java.util.List;
+import java.util.OptionalLong;
+
+import org.springframework.lang.Nullable;
+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 SelectList selectList;
+ private final From from;
+ private final long limit;
+ private final long offset;
+ private final List joins;
+ private final @Nullable Where where;
+ private final List orderBy;
+
+ DefaultSelect(boolean distinct, List selectList, List from, long limit, long offset,
+ List joins, @Nullable Condition where, List orderBy) {
+
+ this.distinct = distinct;
+ this.selectList = new SelectList(new ArrayList<>(selectList));
+ this.from = new From(from);
+ this.limit = limit;
+ this.offset = offset;
+ this.joins = new ArrayList<>(joins);
+ this.orderBy = new ArrayList<>(orderBy);
+ this.where = where != null ? new Where(where) : null;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.Select#getLimit()
+ */
+ @Override
+ public OptionalLong getLimit() {
+ return limit == -1 ? OptionalLong.empty() : OptionalLong.of(limit);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.Select#getOffset()
+ */
+ @Override
+ public OptionalLong getOffset() {
+ return offset == -1 ? OptionalLong.empty() : OptionalLong.of(offset);
+ }
+
+ @Override
+ public boolean isDistinct() {
+ return distinct;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.Visitable#visit(org.springframework.data.relational.core.sql.Visitor)
+ */
+ @Override
+ public void visit(Visitor visitor) {
+
+ Assert.notNull(visitor, "Visitor must not be null!");
+
+ visitor.enter(this);
+
+ selectList.visit(visitor);
+ from.visit(visitor);
+ joins.forEach(it -> it.visit(visitor));
+
+ visitIfNotNull(where, visitor);
+
+ orderBy.forEach(it -> it.visit(visitor));
+
+ visitor.leave(this);
+ }
+
+ private void visitIfNotNull(@Nullable Visitable visitable, Visitor visitor) {
+
+ if (visitable != null) {
+ visitable.visit(visitor);
+ }
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelectBuilder.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelectBuilder.java
new file mode 100644
index 0000000000..b5434f1bdd
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelectBuilder.java
@@ -0,0 +1,434 @@
+/*
+ * 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.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+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 {
+
+ private boolean distinct = false;
+ private List selectList = new ArrayList<>();
+ private List from = new ArrayList<>();
+ private long limit = -1;
+ private long offset = -1;
+ private List joins = new ArrayList<>();
+ private @Nullable Condition where;
+ private List orderBy = new ArrayList<>();
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.SelectBuilder#top(int)
+ */
+ @Override
+ public SelectBuilder top(int count) {
+
+ limit = count;
+ return this;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.SelectBuilder#select(org.springframework.data.relational.core.sql.Expression)
+ */
+ @Override
+ public DefaultSelectBuilder select(Expression expression) {
+ selectList.add(expression);
+ return this;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.SelectBuilder#select(org.springframework.data.relational.core.sql.Expression[])
+ */
+ @Override
+ public DefaultSelectBuilder select(Expression... expressions) {
+ selectList.addAll(Arrays.asList(expressions));
+ return this;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.SelectBuilder#select(java.util.Collection)
+ */
+ @Override
+ public DefaultSelectBuilder select(Collection extends Expression> expressions) {
+ selectList.addAll(expressions);
+ return this;
+ }
+
+ @Override
+ public DefaultSelectBuilder distinct() {
+ distinct = true;
+ return this;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.SelectBuilder.SelectFrom#from(java.lang.String)
+ */
+ @Override
+ public SelectFromAndJoin from(String table) {
+ return from(Table.create(table));
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.SelectBuilder.SelectAndFrom#from(org.springframework.data.relational.core.sql.Table)
+ */
+ @Override
+ public SelectFromAndJoin from(Table table) {
+ from.add(table);
+ return this;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.SelectBuilder.SelectAndFrom#from(org.springframework.data.relational.core.sql.Table[])
+ */
+ @Override
+ public SelectFromAndJoin from(Table... tables) {
+ from.addAll(Arrays.asList(tables));
+ return this;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.SelectBuilder.SelectAndFrom#from(java.util.Collection)
+ */
+ @Override
+ public SelectFromAndJoin from(Collection extends Table> tables) {
+ from.addAll(tables);
+ return this;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.SelectBuilder.SelectFromAndJoin#limitOffset(long, long)
+ */
+ @Override
+ public SelectFromAndJoin limitOffset(long limit, long offset) {
+ this.limit = limit;
+ this.offset = offset;
+ return this;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.SelectBuilder.SelectFromAndJoin#limit(long)
+ */
+ @Override
+ public SelectFromAndJoin limit(long limit) {
+ this.limit = limit;
+ return this;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.SelectBuilder.SelectFromAndJoin#offset(long)
+ */
+ @Override
+ public SelectFromAndJoin offset(long offset) {
+ this.offset = offset;
+ return this;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.SelectBuilder.SelectFromAndOrderBy#orderBy(org.springframework.data.relational.core.sql.OrderByField[])
+ */
+ @Override
+ public DefaultSelectBuilder orderBy(OrderByField... orderByFields) {
+
+ this.orderBy.addAll(Arrays.asList(orderByFields));
+
+ return this;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.SelectBuilder.SelectFromAndOrderBy#orderBy(java.util.Collection)
+ */
+ @Override
+ public DefaultSelectBuilder orderBy(Collection extends OrderByField> orderByFields) {
+
+ this.orderBy.addAll(orderByFields);
+
+ return this;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.SelectBuilder.SelectFromAndOrderBy#orderBy(org.springframework.data.relational.core.sql.Column[])
+ */
+ @Override
+ public DefaultSelectBuilder orderBy(Column... columns) {
+
+ for (Column column : columns) {
+ this.orderBy.add(OrderByField.from(column));
+ }
+
+ return this;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.SelectBuilder.SelectWhere#where(org.springframework.data.relational.core.sql.Condition)
+ */
+ @Override
+ public SelectWhereAndOr where(Condition condition) {
+
+ where = condition;
+ return this;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.SelectBuilder.SelectWhereAndOr#and(org.springframework.data.relational.core.sql.Condition)
+ */
+ @Override
+ public SelectWhereAndOr and(Condition condition) {
+
+ where = where.and(condition);
+ return this;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.SelectBuilder.SelectWhereAndOr#or(org.springframework.data.relational.core.sql.Condition)
+ */
+ @Override
+ public SelectWhereAndOr or(Condition condition) {
+
+ where = where.or(condition);
+ return this;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.SelectBuilder.SelectJoin#join(java.lang.String)
+ */
+ @Override
+ public SelectOn join(String table) {
+ return join(Table.create(table));
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.SelectBuilder.SelectJoin#join(org.springframework.data.relational.core.sql.Table)
+ */
+ @Override
+ public SelectOn join(Table table) {
+ return new JoinBuilder(table, this);
+ }
+
+ public DefaultSelectBuilder join(Join join) {
+ this.joins.add(join);
+
+ return this;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.SelectBuilder.BuildSelect#build()
+ */
+ @Override
+ public Select build() {
+ DefaultSelect select = new DefaultSelect(distinct, selectList, from, limit, offset, joins, where, orderBy);
+ SelectValidator.validate(select);
+ return select;
+ }
+
+ /**
+ * Delegation builder to construct JOINs.
+ */
+ static class JoinBuilder implements SelectOn, SelectOnConditionComparison, SelectFromAndJoinCondition {
+
+ private final Table table;
+ private final DefaultSelectBuilder selectBuilder;
+ private Expression from;
+ private Expression to;
+ private @Nullable Condition condition;
+
+ JoinBuilder(Table table, DefaultSelectBuilder selectBuilder) {
+ this.table = table;
+ this.selectBuilder = selectBuilder;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.SelectBuilder.SelectOn#on(org.springframework.data.relational.core.sql.Expression)
+ */
+ @Override
+ public SelectOnConditionComparison on(Expression column) {
+
+ this.from = column;
+ return this;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.SelectBuilder.SelectOnConditionComparison#equals(org.springframework.data.relational.core.sql.Expression)
+ */
+ @Override
+ public JoinBuilder equals(Expression column) {
+ this.to = column;
+ return this;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.SelectBuilder.SelectOnCondition#and(org.springframework.data.relational.core.sql.Expression)
+ */
+ @Override
+ public SelectOnConditionComparison and(Expression column) {
+
+ finishCondition();
+ this.from = column;
+ return this;
+ }
+
+ private void finishCondition() {
+ Comparison comparison = Comparison.create(from, "=", to);
+
+ if (condition == null) {
+ condition = comparison;
+ } else {
+ condition = condition.and(comparison);
+ }
+ }
+
+ private Join finishJoin() {
+
+ finishCondition();
+ return new Join(JoinType.JOIN, table, condition);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.SelectBuilder.SelectOrdered#orderBy(org.springframework.data.relational.core.sql.OrderByField[])
+ */
+ @Override
+ public SelectOrdered orderBy(OrderByField... orderByFields) {
+ selectBuilder.join(finishJoin());
+ return selectBuilder.orderBy(orderByFields);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.SelectBuilder.SelectOrdered#orderBy(java.util.Collection)
+ */
+ @Override
+ public SelectOrdered orderBy(Collection extends OrderByField> orderByFields) {
+ selectBuilder.join(finishJoin());
+ return selectBuilder.orderBy(orderByFields);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.SelectBuilder.SelectOrdered#orderBy(org.springframework.data.relational.core.sql.Column[])
+ */
+ @Override
+ public SelectOrdered orderBy(Column... columns) {
+ selectBuilder.join(finishJoin());
+ return selectBuilder.orderBy(columns);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.SelectBuilder.SelectWhere#where(org.springframework.data.relational.core.sql.Condition)
+ */
+ @Override
+ public SelectWhereAndOr where(Condition condition) {
+ selectBuilder.join(finishJoin());
+ return selectBuilder.where(condition);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.SelectBuilder.SelectJoin#join(java.lang.String)
+ */
+ @Override
+ public SelectOn join(String table) {
+ selectBuilder.join(finishJoin());
+ return selectBuilder.join(table);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.SelectBuilder.SelectJoin#join(org.springframework.data.relational.core.sql.Table)
+ */
+ @Override
+ public SelectOn join(Table table) {
+ selectBuilder.join(finishJoin());
+ return selectBuilder.join(table);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.SelectBuilder.SelectFromAndJoinCondition#limitOffset(long, long)
+ */
+ @Override
+ public SelectFromAndJoin limitOffset(long limit, long offset) {
+ selectBuilder.join(finishJoin());
+ return selectBuilder.limitOffset(limit, offset);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.SelectBuilder.SelectFromAndJoinCondition#limit(long)
+ */
+ @Override
+ public SelectFromAndJoin limit(long limit) {
+ selectBuilder.join(finishJoin());
+ return selectBuilder.limit(limit);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.SelectBuilder.SelectFromAndJoinCondition#offset(long)
+ */
+ @Override
+ public SelectFromAndJoin offset(long offset) {
+ selectBuilder.join(finishJoin());
+ return selectBuilder.offset(offset);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.SelectBuilder.BuildSelect#build()
+ */
+ @Override
+ public Select build() {
+ selectBuilder.join(finishJoin());
+ return selectBuilder.build();
+ }
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Expression.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Expression.java
new file mode 100644
index 0000000000..07e083cee3
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Expression.java
@@ -0,0 +1,26 @@
+/*
+ * 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;
+
+/**
+ * Expression that can be used in select lists.
+ *
+ * @author Mark Paluch
+ * @since 1.1
+ * @see SQL
+ * @see Expressions
+ */
+public interface Expression extends Segment {}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Expressions.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Expressions.java
new file mode 100644
index 0000000000..be56122a5f
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Expressions.java
@@ -0,0 +1,71 @@
+/*
+ * 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;
+
+/**
+ * Factory for common {@link Expression}s.
+ *
+ * @author Mark Paluch
+ * @since 1.1
+ * @see SQL
+ * @see Conditions
+ * @see Functions
+ */
+public abstract class Expressions {
+
+ private static Expression ASTERISK = new SimpleExpression("*");
+
+ /**
+ * @return a new asterisk {@code *} expression.
+ */
+ public static Expression asterisk() {
+ return ASTERISK;
+ }
+
+ /**
+ * Creates a plain {@code sql} {@link Expression}.
+ *
+ * @param sql the SQL, must not be {@literal null} or empty.
+ * @return a SQL {@link Expression}.
+ */
+ public static Expression just(String sql) {
+ return new SimpleExpression(sql);
+ }
+
+ /**
+ * @return a new {@link Table}.scoped asterisk {@code .*} expression.
+ */
+ public static Expression asterisk(Table table) {
+ return table.asterisk();
+ }
+
+ // Utility constructor.
+ private Expressions() {}
+
+ static class SimpleExpression extends AbstractSegment implements Expression {
+
+ private final String expression;
+
+ SimpleExpression(String expression) {
+ this.expression = expression;
+ }
+
+ @Override
+ public String toString() {
+ return expression;
+ }
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/From.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/From.java
new file mode 100644
index 0000000000..6c6bfc4050
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/From.java
@@ -0,0 +1,52 @@
+/*
+ * 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.Arrays;
+import java.util.List;
+
+import org.springframework.util.StringUtils;
+
+/**
+ * {@code FROM} clause.
+ *
+ * @author Mark Paluch
+ * @since 1.1
+ */
+public class From extends AbstractSegment {
+
+ private final List tables;
+
+ From(Table... tables) {
+ this(Arrays.asList(tables));
+ }
+
+ From(List tables) {
+
+ super(tables.toArray(new Table[] {}));
+
+ this.tables = tables;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return "FROM " + StringUtils.collectionToDelimitedString(tables, ", ");
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Functions.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Functions.java
new file mode 100644
index 0000000000..651b545561
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Functions.java
@@ -0,0 +1,64 @@
+/*
+ * 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.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+
+import org.springframework.util.Assert;
+
+/**
+ * Factory for common {@link Expression function expressions}.
+ *
+ * @author Mark Paluch
+ * @since 1.1
+ * @see SQL
+ * @see Expressions
+ * @see Functions
+ */
+public class Functions {
+
+ /**
+ * Creates a new {@code COUNT} function.
+ *
+ * @param columns columns to apply count, must not be {@literal null}.
+ * @return the new {@link SimpleFunction count function} for {@code columns}.
+ */
+ public static SimpleFunction count(Column... columns) {
+
+ Assert.notNull(columns, "Columns must not be null!");
+ Assert.notEmpty(columns, "Columns must contains at least one column");
+
+ return SimpleFunction.create("COUNT", Arrays.asList(columns));
+ }
+
+ /**
+ * Creates a new {@code COUNT} function.
+ *
+ * @param columns columns to apply count, must not be {@literal null}.
+ * @return the new {@link SimpleFunction count function} for {@code columns}.
+ */
+ public static SimpleFunction count(Collection extends Expression> columns) {
+
+ Assert.notNull(columns, "Columns must not be null!");
+
+ return SimpleFunction.create("COUNT", new ArrayList<>(columns));
+ }
+
+ // Utility constructor.
+ private Functions() {}
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/In.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/In.java
new file mode 100644
index 0000000000..23d7282394
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/In.java
@@ -0,0 +1,113 @@
+/*
+ * 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.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 Collection expressions;
+
+ private In(Expression left, Collection expressions) {
+
+ super(toArray(left, expressions));
+
+ this.left = left;
+ this.expressions = expressions;
+ }
+
+ private static Segment[] toArray(Expression expression, Collection expressions) {
+
+ Segment[] segments = new Segment[1 + expressions.size()];
+ segments[0] = expression;
+
+ int index = 1;
+
+ for (Expression e : expressions) {
+ segments[index++] = e;
+ }
+
+ return segments;
+ }
+
+ /**
+ * Creates a new {@link In} {@link Condition} given left and right {@link Expression}s.
+ *
+ * @param columnOrExpression left hand side of the {@link Condition} must not be {@literal null}.
+ * @param arg right hand side (collection {@link Expression}) must not be {@literal null}.
+ * @return the {@link In} {@link Condition}.
+ */
+ public static In create(Expression columnOrExpression, Expression arg) {
+
+ Assert.notNull(columnOrExpression, "Comparison column or expression must not be null");
+ Assert.notNull(arg, "Expression argument must not be null");
+
+ return new In(columnOrExpression, Collections.singletonList(arg));
+ }
+
+ /**
+ * Creates a new {@link In} {@link Condition} given left and right {@link Expression}s.
+ *
+ * @param columnOrExpression left hand side of the {@link Condition} must not be {@literal null}.
+ * @param expressions right hand side (collection {@link Expression}) must not be {@literal null}.
+ * @return the {@link In} {@link Condition}.
+ */
+ public static In create(Expression columnOrExpression, Collection 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 (" + StringUtils.collectionToDelimitedString(expressions, ", ") + ")";
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/IsNull.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/IsNull.java
new file mode 100644
index 0000000000..99d950c643
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/IsNull.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.relational.core.sql;
+
+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;
+
+ private IsNull(Expression expression) {
+ this(expression, false);
+ }
+
+ private IsNull(Expression expression, boolean negated) {
+
+ super(expression);
+
+ this.expression = expression;
+ this.negated = negated;
+ }
+
+ /**
+ * 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");
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Join.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Join.java
new file mode 100644
index 0000000000..867d29e2ea
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Join.java
@@ -0,0 +1,116 @@
+/*
+ * 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;
+
+/**
+ * {@link Segment} for a {@code JOIN} declaration.
+ *
+ * Renders to: {@code JOIN
+ *
+
+ * ON }.
+ *
+ * @author Mark Paluch
+ * @since 1.1
+ */
+public class Join extends AbstractSegment {
+
+ private final JoinType type;
+ private final Table joinTable;
+ private final Condition on;
+
+ Join(JoinType type, Table joinTable, Condition on) {
+
+ super(joinTable, on);
+
+ this.joinTable = joinTable;
+ this.type = type;
+ this.on = on;
+ }
+
+ /**
+ * @return join type.
+ */
+ public JoinType getType() {
+ return type;
+ }
+
+ /**
+ * @return the joined {@link Table}.
+ */
+ public Table getJoinTable() {
+ return joinTable;
+ }
+
+ /**
+ * @return join condition (the ON or USING part).
+ */
+ public Condition getOn() {
+ return on;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return type + " " + joinTable + " ON " + on;
+ }
+
+ public enum JoinType {
+
+ /**
+ * {@code INNER JOIN} for two tables.
+ */
+
+ JOIN("JOIN"),
+
+ /**
+ * {@code CROSS JOIN} for two tables.
+ */
+
+ CROSS_JOIN("CROSS JOIN"),
+
+ /**
+ * {@code LEFT OUTER JOIN} two tables.
+ */
+
+ LEFT_OUTER_JOIN("LEFT OUTER JOIN"),
+
+ /**
+ * {@code RIGHT OUTER JOIN} two tables.
+ */
+
+ RIGHT_OUTER_JOIN("RIGHT OUTER JOIN"),
+
+ /**
+ * {@code FULL OUTER JOIN} two tables.
+ */
+
+ FULL_OUTER_JOIN("FULL OUTER JOIN");
+
+ private final String sql;
+
+ JoinType(String sql) {
+ this.sql = sql;
+ }
+
+ public String getSql() {
+ return sql;
+ }
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Like.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Like.java
new file mode 100644
index 0000000000..1a6c3ab372
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Like.java
@@ -0,0 +1,74 @@
+/*
+ * 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;
+
+/**
+ * LIKE {@link Condition} comparing two {@link Expression}s.
+ *
+ * Results in a rendered condition: {@code LIKE }.
+ *
+ * @author Mark Paluch
+ * @since 1.1
+ */
+public class Like extends AbstractSegment implements Condition {
+
+ private final Expression left;
+ private final Expression right;
+
+ private Like(Expression left, Expression right) {
+
+ super(left, right);
+
+ this.left = left;
+ this.right = right;
+ }
+
+ /**
+ * Creates a new {@link Like} {@link Condition} given two {@link Expression}s.
+ *
+ * @param leftColumnOrExpression the left {@link Expression}.
+ * @param rightColumnOrExpression the right {@link Expression}.
+ * @return the {@link Like} condition.
+ */
+ public static Like create(Expression leftColumnOrExpression, Expression rightColumnOrExpression) {
+
+ Assert.notNull(leftColumnOrExpression, "Left expression must not be null!");
+ Assert.notNull(rightColumnOrExpression, "Right expression must not be null!");
+
+ return new Like(leftColumnOrExpression, rightColumnOrExpression);
+ }
+
+ /**
+ * @return the left {@link Expression}.
+ */
+ public Expression getLeft() {
+ return left;
+ }
+
+ /**
+ * @return the right {@link Expression}.
+ */
+ public Expression getRight() {
+ return right;
+ }
+
+ @Override
+ public String toString() {
+ return left.toString() + " LIKE " + right.toString();
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/MultipleCondition.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/MultipleCondition.java
new file mode 100644
index 0000000000..82077288ab
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/MultipleCondition.java
@@ -0,0 +1,52 @@
+/*
+ * 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.Arrays;
+import java.util.List;
+import java.util.StringJoiner;
+
+/**
+ * Wrapper for multiple {@link Condition}s.
+ *
+ * @author Jens Schauder
+ * @since 1.1
+ */
+public abstract class MultipleCondition extends AbstractSegment implements Condition {
+
+ private final List conditions;
+ private final String delimiter;
+
+ MultipleCondition(String delimiter, Condition... conditions) {
+
+ super(conditions);
+
+ this.delimiter = delimiter;
+ this.conditions = Arrays.asList(conditions);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+
+ StringJoiner joiner = new StringJoiner(delimiter);
+ conditions.forEach(c -> joiner.add(c.toString()));
+ return joiner.toString();
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Named.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Named.java
new file mode 100644
index 0000000000..44f7449cf3
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Named.java
@@ -0,0 +1,30 @@
+/*
+ * 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;
+
+/**
+ * Named element exposing a {@link #getName() name}.
+ *
+ * @author Mark Paluch
+ * @since 1.1
+ */
+public interface Named {
+
+ /**
+ * @return the name of the underlying element.
+ */
+ String getName();
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Not.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Not.java
new file mode 100644
index 0000000000..a691b6dd27
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Not.java
@@ -0,0 +1,50 @@
+/*
+ * 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;
+
+/**
+ * @author Jens Schauder
+ * @since 1.1
+ */
+public class Not extends AbstractSegment implements Condition {
+
+ private final Condition condition;
+
+ Not(Condition condition) {
+
+ super(condition);
+
+ this.condition = condition;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.Condition#not()
+ */
+ @Override
+ public Condition not() {
+ return condition;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return "NOT " + condition.toString();
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/OrCondition.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/OrCondition.java
new file mode 100644
index 0000000000..94e872fa73
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/OrCondition.java
@@ -0,0 +1,30 @@
+/*
+ * 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;
+
+/**
+ * {@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 {
+
+ OrCondition(Condition... conditions) {
+ super(" OR ", conditions);
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/OrderByField.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/OrderByField.java
new file mode 100644
index 0000000000..ae09b5fa35
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/OrderByField.java
@@ -0,0 +1,108 @@
+/*
+ * 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.data.domain.Sort;
+import org.springframework.data.domain.Sort.Direction;
+import org.springframework.data.domain.Sort.NullHandling;
+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 {
+
+ private final Expression expression;
+ private final @Nullable Sort.Direction direction;
+ private final Sort.NullHandling nullHandling;
+
+ private OrderByField(Expression expression, @Nullable Direction direction, NullHandling nullHandling) {
+
+ super(expression);
+ Assert.notNull(expression, "Order by expression must not be null");
+ Assert.notNull(nullHandling, "NullHandling by expression must not be null");
+
+ this.expression = expression;
+ this.direction = direction;
+ 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);
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * 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);
+ }
+
+ public Expression getExpression() {
+ return expression;
+ }
+
+ @Nullable
+ public Direction getDirection() {
+ return direction;
+ }
+
+ public NullHandling getNullHandling() {
+ return nullHandling;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return direction != null ? expression.toString() + " " + direction : expression.toString();
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SQL.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SQL.java
new file mode 100644
index 0000000000..0f45a8637e
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SQL.java
@@ -0,0 +1,82 @@
+/*
+ * 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.data.relational.core.sql.BindMarker.NamedBindMarker;
+import org.springframework.util.Assert;
+
+/**
+ * Utility to create SQL {@link Segment}s. Typically used as entry point to the Statement Builder. Objects and dependent
+ * objects created by the Query AST are immutable except for builders.
+ *
+ * The Statement Builder API is intended for framework usage to produce SQL required for framework operations.
+ *
+ * @author Mark Paluch
+ * @author Jens Schauder
+ * @since 1.1
+ * @see Expressions
+ * @see Conditions
+ * @see Functions
+ * @see StatementBuilder
+ */
+public abstract class SQL {
+
+ /**
+ * Creates a new {@link Column} associated with a source {@link Table}.
+ *
+ * @param name column name, must not be {@literal null} or empty.
+ * @param table table name, must not be {@literal null}.
+ * @return the column with {@code name} associated with {@link Table}.
+ */
+ public static Column column(String name, Table table) {
+ return Column.create(name, table);
+ }
+
+ /**
+ * Creates a new {@link Table}.
+ *
+ * @param name table name, must not be {@literal null} or empty.
+ * @return the column with {@code name}.
+ */
+ public static Table table(String name) {
+ return Table.create(name);
+ }
+
+ /**
+ * Creates a new parameter bind marker.
+ *
+ * @return a new {@link BindMarker}.
+ */
+ public static BindMarker bindMarker() {
+ return new BindMarker();
+ }
+
+ /**
+ * Creates a new parameter bind marker associated with a {@code name} hint.
+ *
+ * @param name name hint, must not be {@literal null} or empty.
+ * @return a new {@link BindMarker}.
+ */
+ public static BindMarker bindMarker(String name) {
+
+ Assert.hasText(name, "Name must not be null or empty!");
+
+ return new NamedBindMarker(name);
+ }
+
+ // Utility constructor.
+ private SQL() {}
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Segment.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Segment.java
new file mode 100644
index 0000000000..138d7cf0d3
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Segment.java
@@ -0,0 +1,60 @@
+/*
+ * 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;
+
+/**
+ * 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 {
+
+ /**
+ * Check whether this {@link Segment} is equal to another {@link Segment}.
+ *
+ * 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.
+ */
+ @Override
+ boolean equals(Object other);
+
+ /**
+ * Generate a hash code from this{@link Segment}.
+ *
+ * Hashcode typically derives from the {@link #toString()} representation so two {@link Segment}s yield the same
+ * {@link #hashCode()} if their {@link #toString()} representation matches.
+ *
+ * @return a hash code value for this object.
+ */
+ @Override
+ int hashCode();
+
+ /**
+ * Return a SQL string representation of this {@link Segment}.
+ *
+ * The representation is intended for debugging purposes and an approximation to the generated SQL. While it might
+ * work in the context of a specific dialect, you should not that the {@link #toString()} representation works across
+ * multiple databases.
+ *
+ * @return a SQL string representation of this {@link Segment}.
+ */
+ @Override
+ String toString();
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Select.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Select.java
new file mode 100644
index 0000000000..a45c01b891
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Select.java
@@ -0,0 +1,68 @@
+/*
+ * 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;
+
+/**
+ * AST for a {@code SELECT} statement. Visiting order:
+ *
+ * - Self
+ * - {@link Column SELECT columns}
+ * - {@link Table FROM tables} clause
+ * - {@link Join JOINs}
+ * - {@link Condition WHERE} condition
+ * - {@link OrderByField ORDER BY fields}
+ *
+ *
+ * @author Mark Paluch
+ * @since 1.1
+ * @see StatementBuilder
+ * @see SelectBuilder
+ * @see SQL
+ */
+public interface Select extends Segment, Visitable {
+
+ /**
+ * Creates a new {@link SelectBuilder}.
+ *
+ * @return a new {@link SelectBuilder}.
+ */
+ static SelectBuilder builder() {
+ return new DefaultSelectBuilder();
+ }
+
+ /**
+ * Optional limit. Used for limit/offset paging.
+ *
+ * @return
+ */
+ OptionalLong getLimit();
+
+ /**
+ * Optional offset. Used for limit/offset paging.
+ *
+ * @return
+ */
+ OptionalLong getOffset();
+
+ /**
+ * Flag if this select is to return distinct rows.
+ *
+ * @return
+ */
+ boolean isDistinct();
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SelectBuilder.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SelectBuilder.java
new file mode 100644
index 0000000000..b1ea5ceb94
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SelectBuilder.java
@@ -0,0 +1,522 @@
+/*
+ * 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;
+
+/**
+ * Entry point to construct a {@link Select} statement.
+ *
+ * @author Mark Paluch
+ * @since 1.1
+ */
+public interface SelectBuilder {
+
+ /**
+ * Apply a {@code TOP} clause given {@code count}.
+ *
+ * @param count the top count.
+ * @return {@code this} {@link SelectBuilder}.
+ */
+ SelectBuilder top(int count);
+
+ /**
+ * Include a {@link Expression} in the select list.
+ *
+ * @param expression the expression to include.
+ * @return {@code this} builder.
+ * @see Table#column(String)
+ */
+ SelectAndFrom select(Expression expression);
+
+ /**
+ * 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...)
+ */
+ SelectAndFrom select(Expression... 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...)
+ */
+ SelectAndFrom select(Collection extends Expression> expressions);
+
+ /**
+ * Makes the select statement distinct
+ *
+ * @return {@code this} builder.
+ */
+ SelectAndFrom distinct();
+
+ /**
+ * 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.
+ *
+ * @param expression the expression to include.
+ * @return {@code this} builder.
+ * @see Table#column(String)
+ */
+ 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.
+ *
+ * @param expressions the expressions to include.
+ * @return {@code this} builder.
+ * @see Table#columns(String...)
+ */
+ 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.
+ *
+ * @param expressions the expressions to include.
+ * @return {@code this} builder.
+ * @see Table#columns(String...)
+ */
+ SelectFrom select(Collection extends Expression> expressions);
+
+ /**
+ * Makes the select statement distinct
+ *
+ * @return {@code this} builder.
+ */
+ 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.
+ *
+ * @param table the table to {@code SELECT … FROM} must not be {@literal null}.
+ * @return {@code this} builder.
+ * @see From
+ * @see SQL#table(String)
+ */
+ @Override
+ 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.
+ *
+ * @param tables the tables to {@code SELECT … FROM} must not be {@literal null}.
+ * @return {@code this} builder.
+ * @see From
+ * @see SQL#table(String)
+ */
+ @Override
+ 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.
+ *
+ * @param tables the tables to {@code SELECT … FROM} must not be {@literal null}.
+ * @return {@code this} builder.
+ * @see From
+ * @see SQL#table(String)
+ */
+ @Override
+ SelectFromAndJoin from(Collection extends Table> tables);
+ }
+
+ /**
+ * 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.
+ *
+ * @param table the table name to {@code SELECT … FROM} must not be {@literal null} or empty.
+ * @return {@code this} builder.
+ * @see From
+ * @see SQL#table(String)
+ */
+ 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.
+ *
+ * @param table the table to {@code SELECT … FROM} must not be {@literal null}.
+ * @return {@code this} builder.
+ * @see From
+ * @see SQL#table(String)
+ */
+ 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.
+ *
+ * @param tables the tables to {@code SELECT … FROM} must not be {@literal null}.
+ * @return {@code this} builder.
+ * @see From
+ * @see SQL#table(String)
+ */
+ 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.
+ *
+ * @param tables the tables to {@code SELECT … FROM} must not be {@literal null}.
+ * @return {@code this} builder.
+ * @see From
+ * @see SQL#table(String)
+ */
+ SelectFromAndOrderBy from(Collection extends Table> tables);
+ }
+
+ /**
+ * Builder exposing {@code FROM}, {@code JOIN}, {@code WHERE} and {@code LIMIT/OFFSET} methods.
+ */
+ interface SelectFromAndOrderBy extends SelectFrom, SelectOrdered, SelectLimitOffset, BuildSelect {
+
+ @Override
+ SelectFromAndOrderBy limitOffset(long limit, long offset);
+
+ @Override
+ SelectFromAndOrderBy limit(long limit);
+
+ @Override
+ SelectFromAndOrderBy offset(long offset);
+
+ @Override
+ SelectFromAndOrderBy from(String table);
+
+ @Override
+ SelectFromAndOrderBy from(Table table);
+
+ @Override
+ SelectFromAndOrderBy from(Table... tables);
+
+ @Override
+ SelectFromAndOrderBy from(Collection extends Table> tables);
+
+ @Override
+ SelectFromAndOrderBy orderBy(Column... columns);
+
+ @Override
+ SelectFromAndOrderBy orderBy(OrderByField... orderByFields);
+
+ @Override
+ 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.
+ *
+ * @param table the table to {@code SELECT … FROM} must not be {@literal null}.
+ * @return {@code this} builder.
+ * @see From
+ * @see SQL#table(String)
+ */
+ @Override
+ 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.
+ *
+ * @param tables the tables to {@code SELECT … FROM} must not be {@literal null}.
+ * @return {@code this} builder.
+ * @see From
+ * @see SQL#table(String)
+ */
+ @Override
+ 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.
+ *
+ * @param tables the tables to {@code SELECT … FROM} must not be {@literal null}.
+ * @return {@code this} builder.
+ * @see From
+ * @see SQL#table(String)
+ */
+ @Override
+ 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)}.
+ *
+ * @param limit rows to read.
+ * @param offset row offset, zero-based.
+ * @return {@code this} builder.
+ */
+ SelectFromAndJoin limitOffset(long limit, long offset);
+
+ /**
+ * Apply a limit of rows to read.
+ *
+ * @param limit rows to read.
+ * @return {@code this} builder.
+ */
+ SelectFromAndJoin limit(long limit);
+
+ /**
+ * Apply an offset where to start reading rows.
+ *
+ * @param offset start offset.
+ * @return {@code this} builder.
+ */
+ SelectFromAndJoin offset(long offset);
+ }
+
+ /**
+ * Builder exposing {@code FROM}, {@code WHERE}, {@code LIMIT/OFFSET}, and JOIN {@code AND} continuation methods.
+ */
+ 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)}.
+ *
+ * @param limit rows to read.
+ * @param offset row offset, zero-based.
+ * @return {@code this} builder.
+ */
+ SelectFromAndJoin limitOffset(long limit, long offset);
+
+ /**
+ * Apply a limit of rows to read.
+ *
+ * @param limit rows to read.
+ * @return {@code this} builder.
+ */
+ SelectFromAndJoin limit(long limit);
+
+ /**
+ * Apply an offset where to start reading rows.
+ *
+ * @param offset start offset.
+ * @return {@code this} builder.
+ */
+ SelectFromAndJoin offset(long offset);
+ }
+
+ /**
+ * Limit/offset methods.
+ */
+ 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)}.
+ *
+ * @param limit rows to read.
+ * @param offset row offset, zero-based.
+ * @return {@code this} builder.
+ */
+ SelectLimitOffset limitOffset(long limit, long offset);
+
+ /**
+ * Apply a limit of rows to read.
+ *
+ * @param limit rows to read.
+ * @return {@code this} builder.
+ */
+ SelectLimitOffset limit(long limit);
+
+ /**
+ * Apply an offset where to start reading rows.
+ *
+ * @param offset start offset.
+ * @return {@code this} builder.
+ */
+ SelectLimitOffset offset(long offset);
+ }
+
+ /**
+ * Builder exposing {@code ORDER BY} methods.
+ */
+ interface SelectOrdered extends BuildSelect {
+
+ /**
+ * Add one or more {@link Column columns} to order by.
+ *
+ * @param columns the columns to order by.
+ * @return {@code this} builder.
+ */
+ SelectOrdered orderBy(Column... columns);
+
+ /**
+ * Add one or more {@link OrderByField order by fields}.
+ *
+ * @param orderByFields the fields to order by.
+ * @return {@code this} builder.
+ */
+ SelectOrdered orderBy(OrderByField... orderByFields);
+
+ /**
+ * Add one or more {@link OrderByField order by fields}.
+ *
+ * @param orderByFields the fields to order by.
+ * @return {@code this} builder.
+ */
+ SelectOrdered orderBy(Collection extends OrderByField> orderByFields);
+ }
+
+ /**
+ * Interface exposing {@code WHERE} methods.
+ */
+ interface SelectWhere extends SelectOrdered, BuildSelect {
+
+ /**
+ * Apply a {@code WHERE} clause.
+ *
+ * @param condition the {@code WHERE} condition.
+ * @return {@code this} builder.
+ * @see Where
+ * @see Condition
+ */
+ SelectWhereAndOr where(Condition condition);
+ }
+
+ /**
+ * Interface exposing {@code AND}/{@code OR} combinator methods for {@code WHERE} {@link Condition}s.
+ */
+ interface SelectWhereAndOr extends SelectOrdered, BuildSelect {
+
+ /**
+ * Combine the previous {@code WHERE} {@link Condition} using {@code AND}.
+ *
+ * @param condition the condition, must not be {@literal null}.
+ * @return {@code this} builder.
+ * @see Condition#and(Condition)
+ */
+ SelectWhereAndOr and(Condition condition);
+
+ /**
+ * Combine the previous {@code WHERE} {@link Condition} using {@code OR}.
+ *
+ * @param condition the condition, must not be {@literal null}.
+ * @return {@code this} builder.
+ * @see Condition#or(Condition)
+ */
+ SelectWhereAndOr or(Condition condition);
+ }
+
+ /**
+ * Interface exposing {@code JOIN} methods.
+ */
+ interface SelectJoin extends BuildSelect {
+
+ /**
+ * Declare a {@code JOIN} {@code table}.
+ *
+ * @param table name of the table, must not be {@literal null} or empty.
+ * @return {@code this} builder.
+ * @see Join
+ * @see SQL#table(String)
+ */
+ SelectOn join(String table);
+
+ /**
+ * Declare a {@code JOIN} {@link Table}.
+ *
+ * @param table name of the table, must not be {@literal null}.
+ * @return {@code this} builder.
+ * @see Join
+ * @see SQL#table(String)
+ */
+ SelectOn join(Table table);
+ }
+
+ /**
+ * Interface exposing {@code ON} methods to declare {@code JOIN} relationships.
+ */
+ interface SelectOn {
+
+ /**
+ * Declare the source column in the {@code JOIN}.
+ *
+ * @param column the source column, must not be {@literal null} or empty.
+ * @return {@code this} builder.
+ * @see Table#column(String)
+ */
+ SelectOnConditionComparison on(Expression column);
+ }
+
+ /**
+ * Interface declaring the target column comparison relationship.
+ */
+ interface SelectOnConditionComparison {
+
+ /**
+ * Declare an equals {@link Condition} between the source column and the target {@link Column}.
+ *
+ * @param column the target column, must not be {@literal null}.
+ * @return {@code this} builder.
+ * @see Table#column(String)
+ */
+ SelectFromAndJoinCondition equals(Expression column);
+ }
+
+ /**
+ * Builder exposing JOIN and {@code JOIN … ON} continuation methods.
+ */
+ interface SelectOnCondition extends SelectJoin, BuildSelect {
+
+ /**
+ * Declare an additional source column in the {@code JOIN}.
+ *
+ * @param column the column, must not be {@literal null}.
+ * @return {@code this} builder.
+ * @see Table#column(String)
+ */
+ SelectOnConditionComparison and(Expression column);
+ }
+
+ /**
+ * Interface exposing the {@link Select} build method.
+ */
+ 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.
+ *
+ * @return the build and immutable {@link Select} statement.
+ */
+ Select build();
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SelectList.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SelectList.java
new file mode 100644
index 0000000000..1e8681cebb
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SelectList.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.relational.core.sql;
+
+import java.util.List;
+
+import org.springframework.util.StringUtils;
+
+/**
+ * Value object representing the select list (selected columns, functions).
+ *
+ * @author Mark Paluch
+ * @since 1.1
+ */
+public class SelectList extends AbstractSegment {
+
+ private final List selectList;
+
+ SelectList(List selectList) {
+ super(selectList.toArray(new Expression[0]));
+ this.selectList = selectList;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return StringUtils.collectionToDelimitedString(selectList, ", ");
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SelectValidator.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SelectValidator.java
new file mode 100644
index 0000000000..e1f9257818
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SelectValidator.java
@@ -0,0 +1,139 @@
+/*
+ * 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.HashSet;
+import java.util.Set;
+
+/**
+ * Validator for {@link Select} statements.
+ *
+ * Validates that all {@link Column}s using a table qualifier have a table import from either the {@code FROM} or
+ * {@code JOIN} clause.
+ *
+ * @author Mark Paluch
+ * @since 1.1
+ */
+class SelectValidator implements Visitor {
+
+ private int selectFieldCount;
+ private Set requiredBySelect = new HashSet<>();
+ private Set requiredByWhere = new HashSet<>();
+ private Set requiredByOrderBy = new HashSet<>();
+
+ private Set from = new HashSet<>();
+ private Set join = new HashSet<>();
+
+ private Visitable parent;
+
+ public static void validate(Select select) {
+ new SelectValidator().doValidate(select);
+ }
+
+ private void doValidate(Select select) {
+
+ select.visit(this);
+
+ if (selectFieldCount == 0) {
+ throw new IllegalStateException("SELECT does not declare a select list");
+ }
+
+ for (Table table : requiredBySelect) {
+ if (!join.contains(table) && !from.contains(table)) {
+ throw new IllegalStateException(String
+ .format("Required table [%s] by a SELECT column not imported by FROM %s or JOIN %s", table, from, join));
+ }
+ }
+
+ for (Table table : requiredByWhere) {
+ if (!join.contains(table) && !from.contains(table)) {
+ throw new IllegalStateException(String
+ .format("Required table [%s] by a WHERE predicate not imported by FROM %s or JOIN %s", table, from, join));
+ }
+ }
+
+ for (Table table : requiredByOrderBy) {
+ if (!join.contains(table) && !from.contains(table)) {
+ throw new IllegalStateException(String
+ .format("Required table [%s] by a ORDER BY column not imported by FROM %s or JOIN %s", table, from, join));
+ }
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.Visitor#enter(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ public void enter(Visitable segment) {
+
+ if (segment instanceof AsteriskFromTable && parent instanceof Select) {
+
+ Table table = ((AsteriskFromTable) segment).getTable();
+ requiredBySelect.add(table);
+ selectFieldCount++;
+ }
+
+ if (segment instanceof Column && (parent instanceof Select || parent instanceof SimpleFunction)) {
+
+ selectFieldCount++;
+ Table table = ((Column) segment).getTable();
+
+ if (table != null) {
+ requiredBySelect.add(table);
+ }
+ }
+
+ if (segment instanceof Table && parent instanceof From) {
+ from.add((Table) segment);
+ }
+
+ if (segment instanceof Column && parent instanceof OrderByField) {
+
+ Table table = ((Column) segment).getTable();
+
+ if (table != null) {
+ requiredByOrderBy.add(table);
+ }
+ }
+
+ if (segment instanceof Table && parent instanceof Join) {
+ join.add((Table) segment);
+ }
+
+ if (segment instanceof Where) {
+
+ segment.visit(item -> {
+
+ if (item instanceof Table) {
+ requiredByWhere.add((Table) item);
+ }
+ });
+ }
+
+ if (segment instanceof Join || segment instanceof OrderByField || segment instanceof From
+ || segment instanceof Select || segment instanceof Where || segment instanceof SimpleFunction) {
+ parent = segment;
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.Visitor#leave(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ public void leave(Visitable segment) {}
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SimpleCondition.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SimpleCondition.java
new file mode 100644
index 0000000000..517029ed47
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SimpleCondition.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.relational.core.sql;
+
+/**
+ * Simple condition consisting of {@link Expression}, {@code comparator} and {@code predicate}.
+ *
+ * @author Mark Paluch
+ * @since 1.1
+ */
+public class SimpleCondition extends AbstractSegment implements Condition {
+
+ private final Expression expression;
+
+ private final String comparator;
+
+ private final String predicate;
+
+ SimpleCondition(Expression expression, String comparator, String predicate) {
+
+ super(expression);
+
+ this.expression = expression;
+ this.comparator = comparator;
+ this.predicate = predicate;
+ }
+
+ /**
+ * Creates a simple {@link Condition} given {@code column}, {@code comparator} and {@code predicate}.
+ *
+ * @param column
+ * @param comparator
+ * @param predicate
+ * @return
+ */
+ public static SimpleCondition create(String column, String comparator, String predicate) {
+ return new SimpleCondition(new Column(column, null), comparator, predicate);
+ }
+
+ public String getComparator() {
+ return comparator;
+ }
+
+ public String getPredicate() {
+ return predicate;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return expression.toString() + " " + comparator + " " + predicate;
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SimpleFunction.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SimpleFunction.java
new file mode 100644
index 0000000000..c5c7d31ae2
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SimpleFunction.java
@@ -0,0 +1,107 @@
+/*
+ * 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.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * Simple function accepting one or more {@link Expression}s.
+ *
+ * @author Mark Paluch
+ * @since 1.1
+ */
+public class SimpleFunction extends AbstractSegment implements Expression {
+
+ private String functionName;
+ private List expressions;
+
+ private SimpleFunction(String functionName, List expressions) {
+
+ super(expressions.toArray(new Expression[0]));
+
+ this.functionName = functionName;
+ this.expressions = expressions;
+ }
+
+ /**
+ * Creates a new {@link SimpleFunction} given {@code functionName} and {@link List} of {@link Expression}s.
+ *
+ * @param functionName must not be {@literal null}.
+ * @param expressions zero or many {@link Expression}s, must not be {@literal null}.
+ * @return
+ */
+ public static SimpleFunction create(String functionName, List expressions) {
+
+ Assert.hasText(functionName, "Function name must not be null or empty");
+ Assert.notNull(expressions, "Expressions name must not be null");
+
+ return new SimpleFunction(functionName, expressions);
+ }
+
+ /**
+ * Expose this function result under a column {@code alias}.
+ *
+ * @param alias column alias name, must not {@literal null} or empty.
+ * @return the aliased {@link SimpleFunction}.
+ */
+ public SimpleFunction as(String alias) {
+
+ Assert.hasText(alias, "Alias must not be null or empty");
+
+ return new AliasedFunction(functionName, expressions, alias);
+ }
+
+ /**
+ * @return the function name.
+ */
+ public String getFunctionName() {
+ return functionName;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return functionName + "(" + StringUtils.collectionToDelimitedString(expressions, ", ") + ")";
+ }
+
+ /**
+ * {@link Aliased} {@link SimpleFunction} implementation.
+ */
+ static class AliasedFunction extends SimpleFunction implements Aliased {
+
+ private final String alias;
+
+ AliasedFunction(String functionName, List expressions, String alias) {
+ super(functionName, expressions);
+ this.alias = alias;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.Aliased#getAlias()
+ */
+ @Override
+ public String getAlias() {
+ return alias;
+ }
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SimpleSegment.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SimpleSegment.java
new file mode 100644
index 0000000000..58a37a3794
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SimpleSegment.java
@@ -0,0 +1,42 @@
+/*
+ * 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;
+
+/**
+ * @author Mark Paluch
+ * @since 1.1
+ */
+public class SimpleSegment extends AbstractSegment {
+
+ private final String sql;
+
+ SimpleSegment(String sql) {
+ this.sql = sql;
+ }
+
+ public String getSql() {
+ return sql;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return getSql();
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/StatementBuilder.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/StatementBuilder.java
new file mode 100644
index 0000000000..9116886048
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/StatementBuilder.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.relational.core.sql;
+
+import java.util.Collection;
+
+import org.springframework.data.relational.core.sql.SelectBuilder.SelectAndFrom;
+
+/**
+ * Entrypoint to build SQL statements.
+ *
+ * @author Mark Paluch
+ * @since 1.1
+ * @see SQL
+ * @see Expressions
+ * @see Conditions
+ * @see Functions
+ */
+public abstract class StatementBuilder {
+
+ /**
+ * Creates a new {@link SelectBuilder} by specifying a {@code SELECT} column.
+ *
+ * @param expression the select list expression.
+ * @return the {@link SelectBuilder} containing {@link Expression}.
+ * @see SelectBuilder#select(Expression)
+ */
+ public static SelectAndFrom select(Expression expression) {
+ return Select.builder().select(expression);
+ }
+
+ /**
+ * Creates a new {@link SelectBuilder} by specifying one or more {@code SELECT} columns.
+ *
+ * @param expressions the select list expressions.
+ * @return the {@link SelectBuilder} containing {@link Expression}s.
+ * @see SelectBuilder#select(Expression...)
+ */
+ public static SelectAndFrom select(Expression... expressions) {
+ return Select.builder().select(expressions);
+ }
+
+ /**
+ * Include one or more {@link Expression}s in the select list.
+ *
+ * @param expressions the expressions to include.
+ * @return {@code this} builder.
+ * @see Table#columns(String...)
+ */
+ public static SelectAndFrom select(Collection 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() {
+
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SubselectExpression.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SubselectExpression.java
new file mode 100644
index 0000000000..1b5b61edf2
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SubselectExpression.java
@@ -0,0 +1,43 @@
+/*
+ * 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;
+
+/**
+ * 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;
+
+ SubselectExpression(Select subselect) {
+
+ super(subselect);
+
+ this.subselect = subselect;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return "(" + subselect.toString() + ")";
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Table.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Table.java
new file mode 100644
index 0000000000..357cc5d6d2
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Table.java
@@ -0,0 +1,219 @@
+/*
+ * 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.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import org.springframework.util.Assert;
+
+/**
+ * Represents a table reference within an SQL statement. Typically used to denote {@code FROM} or {@code JOIN} or to
+ * prefix a {@link Column}.
+ *
+ * Renders to: {@code } or {@code AS }.
+ *
+ * @author Mark Paluch
+ * @since 1.1
+ */
+public class Table extends AbstractSegment {
+
+ private final String name;
+
+ Table(String name) {
+ super();
+ this.name = name;
+ }
+
+ /**
+ * Creates a new {@link Table} given {@code name}.
+ *
+ * @param name must not be {@literal null} or empty.
+ * @return the new {@link Table}.
+ */
+ public static Table create(String name) {
+
+ Assert.hasText(name, "Name must not be null or empty!");
+
+ return new Table(name);
+ }
+
+ /**
+ * Creates a new {@link Table} using an {@code alias}.
+ *
+ * @param name must not be {@literal null} or empty.
+ * @param alias must not be {@literal null} or empty.
+ * @return the new {@link Table} using the {@code alias}.
+ */
+ public static Table aliased(String name, String alias) {
+
+ Assert.hasText(name, "Name must not be null or empty!");
+ Assert.hasText(alias, "Alias must not be null or empty!");
+
+ return new AliasedTable(name, 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}.
+ */
+ public Table as(String alias) {
+
+ Assert.hasText(alias, "Alias must not be null or empty!");
+
+ return new AliasedTable(name, alias);
+ }
+
+ /**
+ * Creates a new {@link Column} associated with this {@link Table}.
+ *
+ * Note: This {@link Table} does not track column creation and there is no possibility to enumerate all
+ * {@link Column}s that were created for this table.
+ *
+ * @param name column name, must not be {@literal null} or empty.
+ * @return a new {@link Column} associated with this {@link Table}.
+ */
+ public Column column(String name) {
+
+ Assert.hasText(name, "Name must not be null or empty!");
+
+ return new Column(name, this);
+ }
+
+ /**
+ * Creates a {@link List} of {@link Column}s associated with this {@link Table}.
+ *
+ * Note: This {@link Table} does not track column creation and there is no possibility to enumerate all
+ * {@link Column}s that were created for this table.
+ *
+ * @param names column names, must not be {@literal null} or empty.
+ * @return a new {@link List} of {@link Column}s associated with this {@link Table}.
+ */
+ public List columns(String... names) {
+
+ Assert.notNull(names, "Names must not be null");
+
+ return columns(Arrays.asList(names));
+ }
+
+ /**
+ * Creates a {@link List} of {@link Column}s associated with this {@link Table}.
+ *
+ * Note: This {@link Table} does not track column creation and there is no possibility to enumerate all
+ * {@link Column}s that were created for this table.
+ *
+ * @param names column names, must not be {@literal null} or empty.
+ * @return a new {@link List} of {@link Column}s associated with this {@link Table}.
+ */
+ public List columns(Collection names) {
+
+ Assert.notNull(names, "Names must not be null");
+
+ List columns = new ArrayList<>();
+ for (String name : names) {
+ columns.add(column(name));
+ }
+
+ return columns;
+ }
+
+ /**
+ * Creates a {@link AsteriskFromTable} maker selecting all columns from this {@link Table} (e.g. {@code SELECT
+ *
+
+ * .*}.
+ *
+ * @return the select all marker for this {@link Table}.
+ */
+ public AsteriskFromTable asterisk() {
+ return new AsteriskFromTable(this);
+ }
+
+ /**
+ * @return the table name.
+ */
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.Named#getName()
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * @return the table name as it is used in references. This can be the actual {@link #getName() name} or an
+ * {@link Aliased#getAlias() alias}.
+ */
+ public String getReferenceName() {
+ return name;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ /**
+ * {@link Aliased} {@link Table} implementation.
+ */
+ static class AliasedTable extends Table implements Aliased {
+
+ private final String alias;
+
+ AliasedTable(String name, String alias) {
+ super(name);
+
+ Assert.hasText(alias, "Alias must not be null or empty!");
+
+ this.alias = alias;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.Aliased#getAlias()
+ */
+ @Override
+ public String getAlias() {
+ return alias;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.Table#getReferenceName()
+ */
+ @Override
+ public String getReferenceName() {
+ return getAlias();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.Table#toString()
+ */
+ @Override
+ public String toString() {
+ return getName() + " AS " + getAlias();
+ }
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Visitable.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Visitable.java
new file mode 100644
index 0000000000..4ebb993e52
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Visitable.java
@@ -0,0 +1,41 @@
+/*
+ * 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;
+
+/**
+ * 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 Visitable} and its nested {@link Visitable}s if applicable.
+ *
+ * @param visitor the visitor to notify, must not be {@literal null}.
+ */
+ default void visit(Visitor visitor) {
+
+ Assert.notNull(visitor, "Visitor must not be null!");
+
+ visitor.enter(this);
+ visitor.leave(this);
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Visitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Visitor.java
new file mode 100644
index 0000000000..0b6d959131
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Visitor.java
@@ -0,0 +1,41 @@
+/*
+ * 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;
+
+/**
+ * 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 Visitable}.
+ *
+ * @param segment the segment to visit.
+ */
+ void enter(Visitable segment);
+
+ /**
+ * Leave a {@link Visitable}.
+ *
+ * @param segment the visited segment.
+ */
+ default void leave(Visitable segment) {}
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Where.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Where.java
new file mode 100644
index 0000000000..e6d0b34df8
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Where.java
@@ -0,0 +1,43 @@
+/*
+ * 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;
+
+/**
+ * {@code Where} clause.
+ *
+ * @author Mark Paluch
+ * @since 1.1
+ */
+public class Where extends AbstractSegment {
+
+ private final Condition condition;
+
+ Where(Condition condition) {
+
+ super(condition);
+
+ this.condition = condition;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return "WHERE " + condition.toString();
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/package-info.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/package-info.java
new file mode 100644
index 0000000000..a68018609b
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/package-info.java
@@ -0,0 +1,16 @@
+
+/**
+ * Statement Builder implementation. Use {@link org.springframework.data.relational.core.sql.StatementBuilder} to create
+ * statements and {@link org.springframework.data.relational.core.sql.SQL} to create SQL objects. Objects and dependent
+ * objects created by the Statement Builder are immutable except for builders.
+ *
+ * The Statement Builder API is intended for framework usage to produce SQL required for framework operations.
+ *
+ * @since 1.1
+ */
+@NonNullApi
+@NonNullFields
+package org.springframework.data.relational.core.sql;
+
+import org.springframework.lang.NonNullApi;
+import org.springframework.lang.NonNullFields;
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ComparisonVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ComparisonVisitor.java
new file mode 100644
index 0000000000..cd5f882e4e
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ComparisonVisitor.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.relational.core.sql.render;
+
+import org.springframework.data.relational.core.sql.Comparison;
+import org.springframework.data.relational.core.sql.Condition;
+import org.springframework.data.relational.core.sql.Expression;
+import org.springframework.data.relational.core.sql.Visitable;
+import org.springframework.lang.Nullable;
+
+/**
+ * {@link org.springframework.data.relational.core.sql.Visitor} rendering comparison {@link Condition}. Uses a
+ * {@link RenderTarget} to call back for render results.
+ *
+ * @author Mark Paluch
+ * @author Jens Schauder
+ * @since 1.1
+ * @see Comparison
+ */
+class ComparisonVisitor extends FilteredSubtreeVisitor {
+
+ private final RenderContext context;
+ private final Comparison condition;
+ private final RenderTarget target;
+ private final StringBuilder part = new StringBuilder();
+ private @Nullable PartRenderer current;
+
+ ComparisonVisitor(RenderContext context, Comparison condition, RenderTarget target) {
+ super(it -> it == condition);
+ this.condition = condition;
+ this.target = target;
+ this.context = context;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.FilteredSubtreeVisitor#enterNested(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ Delegation enterNested(Visitable segment) {
+
+ if (segment instanceof Expression) {
+ ExpressionVisitor visitor = new ExpressionVisitor(context);
+ current = visitor;
+ return Delegation.delegateTo(visitor);
+ }
+
+ if (segment instanceof Condition) {
+ ConditionVisitor visitor = new ConditionVisitor(context);
+ current = visitor;
+ return Delegation.delegateTo(visitor);
+ }
+
+ throw new IllegalStateException("Cannot provide visitor for " + segment);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.FilteredSubtreeVisitor#leaveNested(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ Delegation leaveNested(Visitable segment) {
+
+ if (current != null) {
+ if (part.length() != 0) {
+ part.append(' ').append(condition.getComparator()).append(' ');
+ }
+
+ part.append(current.getRenderedPart());
+ current = null;
+ }
+
+ return super.leaveNested(segment);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.FilteredSubtreeVisitor#leaveMatched(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ Delegation leaveMatched(Visitable segment) {
+
+ target.onRendered(part);
+
+ return super.leaveMatched(segment);
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ConditionVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ConditionVisitor.java
new file mode 100644
index 0000000000..a6b22eec5d
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ConditionVisitor.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.relational.core.sql.render;
+
+import org.springframework.data.relational.core.sql.AndCondition;
+import org.springframework.data.relational.core.sql.Comparison;
+import org.springframework.data.relational.core.sql.Condition;
+import org.springframework.data.relational.core.sql.In;
+import org.springframework.data.relational.core.sql.IsNull;
+import org.springframework.data.relational.core.sql.Like;
+import org.springframework.data.relational.core.sql.OrCondition;
+import org.springframework.lang.Nullable;
+
+/**
+ * {@link org.springframework.data.relational.core.sql.Visitor} delegating {@link Condition} rendering to condition
+ * {@link org.springframework.data.relational.core.sql.Visitor}s.
+ *
+ * @author Mark Paluch
+ * @author Jens Schauder
+ * @since 1.1
+ * @see AndCondition
+ * @see OrCondition
+ * @see IsNull
+ * @see Comparison
+ * @see Like
+ * @see In
+ */
+class ConditionVisitor extends TypedSubtreeVisitor implements PartRenderer {
+
+ private final RenderContext context;
+ private StringBuilder builder = new StringBuilder();
+
+ ConditionVisitor(RenderContext context) {
+ this.context = context;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#enterMatched(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ Delegation enterMatched(Condition segment) {
+
+ DelegatingVisitor visitor = getDelegation(segment);
+
+ return visitor != null ? Delegation.delegateTo(visitor) : Delegation.retain();
+ }
+
+ @Nullable
+ private DelegatingVisitor getDelegation(Condition segment) {
+
+ if (segment instanceof AndCondition) {
+ return new MultiConcatConditionVisitor(context, (AndCondition) segment, builder::append);
+ }
+
+ if (segment instanceof OrCondition) {
+ return new MultiConcatConditionVisitor(context, (OrCondition) segment, builder::append);
+ }
+
+ if (segment instanceof IsNull) {
+ return new IsNullVisitor(context, builder::append);
+ }
+
+ if (segment instanceof Comparison) {
+ return new ComparisonVisitor(context, (Comparison) segment, builder::append);
+ }
+
+ if (segment instanceof Like) {
+ return new LikeVisitor((Like) segment, context, builder::append);
+ }
+
+ if (segment instanceof In) {
+ return new InVisitor(context, builder::append);
+ }
+
+ return null;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.PartRenderer#getRenderedPart()
+ */
+ @Override
+ public CharSequence getRenderedPart() {
+ return builder;
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/DelegatingVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/DelegatingVisitor.java
new file mode 100644
index 0000000000..a04be55cf9
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/DelegatingVisitor.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.relational.core.sql.render;
+
+import java.util.Stack;
+
+import org.springframework.data.relational.core.sql.Visitable;
+import org.springframework.data.relational.core.sql.Visitor;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+
+/**
+ * Abstract base class for delegating {@link Visitor} implementations. This class implements a delegation pattern using
+ * visitors. A delegating {@link Visitor} can implement {@link #doEnter(Visitable)} and {@link #doLeave(Visitable)}
+ * methods to provide its functionality.
+ *
+ * Delegation
Typically, a {@link Visitor} is scoped to a single responsibility. If a {@link Visitor segment}
+ * requires {@link #doEnter(Visitable) processing} that is not directly implemented by the visitor itself, the current
+ * {@link Visitor} can delegate processing to a {@link DelegatingVisitor delegate}. Once a delegation is installed, the
+ * {@link DelegatingVisitor delegate} is used as {@link Visitor} for the current and all subsequent items until it
+ * {@link #doLeave(Visitable) signals} that it is no longer responsible.
+ *
+ * Nested visitors are required to properly signal once they are no longer responsible for a {@link Visitor segment} to
+ * step back from the delegation. Otherwise, parents are no longer involved in the visitation.
+ *
+ * Delegation is recursive and limited by the stack size.
+ *
+ * @author Mark Paluch
+ * @since 1.1
+ * @see FilteredSubtreeVisitor
+ * @see TypedSubtreeVisitor
+ */
+abstract class DelegatingVisitor implements Visitor {
+
+ private Stack delegation = new Stack<>();
+
+ /**
+ * Invoked for a {@link Visitable segment} when entering the segment.
+ *
+ * This method can signal whether it is responsible for handling the {@link Visitor segment} or whether the segment
+ * requires delegation to a sub-{@link Visitor}. When delegating to a sub-{@link Visitor}, {@link #doEnter(Visitable)}
+ * is called on the {@link DelegatingVisitor delegate}.
+ *
+ * @param segment must not be {@literal null}.
+ * @return
+ */
+ @Nullable
+ public abstract Delegation doEnter(Visitable segment);
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.Visitor#enter(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ public final void enter(Visitable segment) {
+
+ if (delegation.isEmpty()) {
+
+ Delegation visitor = doEnter(segment);
+ Assert.notNull(visitor,
+ () -> String.format("Visitor must not be null. Caused by %s.doEnter(…)", getClass().getName()));
+ Assert.state(!visitor.isLeave(),
+ () -> String.format("Delegation indicates leave. Caused by %s.doEnter(…)", getClass().getName()));
+
+ if (visitor.isDelegate()) {
+ delegation.push(visitor.getDelegate());
+ visitor.getDelegate().enter(segment);
+ }
+ } else {
+ delegation.peek().enter(segment);
+ }
+ }
+
+ /**
+ * Invoked for a {@link Visitable segment} when leaving the segment.
+ *
+ * This method can signal whether this {@link Visitor} should remain responsible for handling subsequent
+ * {@link Visitor segments} or whether it should step back from delegation. When stepping back from delegation,
+ * {@link #doLeave(Visitable)} is called on the {@link DelegatingVisitor parent delegate}.
+ *
+ * @param segment must not be {@literal null}.
+ * @return
+ */
+ public abstract Delegation doLeave(Visitable segment);
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.Visitor#leave(org.springframework.data.relational.core.sql.Visitable)
+ */
+ public final void leave(Visitable segment) {
+ doLeave0(segment);
+ }
+
+ private Delegation doLeave0(Visitable segment) {
+
+ if (delegation.isEmpty()) {
+ return doLeave(segment);
+ } else {
+
+ DelegatingVisitor visitor = delegation.peek();
+ while (visitor != null) {
+
+ Delegation result = visitor.doLeave0(segment);
+ Assert.notNull(visitor,
+ () -> String.format("Visitor must not be null. Caused by %s.doLeave(…)", getClass().getName()));
+
+ if (visitor == this) {
+ if (result.isLeave()) {
+ return delegation.isEmpty() ? Delegation.leave() : Delegation.retain();
+ }
+ return Delegation.retain();
+ }
+
+ if (result.isRetain()) {
+ return result;
+ }
+
+ if (result.isLeave()) {
+
+ if (!delegation.isEmpty()) {
+ delegation.pop();
+ }
+
+ if (!delegation.isEmpty()) {
+ visitor = delegation.peek();
+ } else {
+ visitor = this;
+ }
+ }
+ }
+ }
+
+ return Delegation.leave();
+ }
+
+ /**
+ * Value object to control delegation.
+ */
+ static class Delegation {
+
+ private static Delegation RETAIN = new Delegation(true, false, null);
+ private static Delegation LEAVE = new Delegation(false, true, null);
+
+ private final boolean retain;
+ private final boolean leave;
+
+ private final @Nullable DelegatingVisitor delegate;
+
+ private Delegation(boolean retain, boolean leave, @Nullable DelegatingVisitor delegate) {
+ this.retain = retain;
+ this.leave = leave;
+ this.delegate = delegate;
+ }
+
+ public static Delegation retain() {
+ return RETAIN;
+ }
+
+ public static Delegation leave() {
+ return LEAVE;
+ }
+
+ public static Delegation delegateTo(DelegatingVisitor visitor) {
+ return new Delegation(false, false, visitor);
+ }
+
+ boolean isDelegate() {
+ return delegate != null;
+ }
+
+ boolean isRetain() {
+ return retain;
+ }
+
+ boolean isLeave() {
+ return leave;
+ }
+
+ DelegatingVisitor getDelegate() {
+
+ Assert.state(isDelegate(), "No delegate available");
+ return delegate;
+ }
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java
new file mode 100644
index 0000000000..604190e079
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.relational.core.sql.render;
+
+import org.springframework.data.relational.core.sql.BindMarker;
+import org.springframework.data.relational.core.sql.Column;
+import org.springframework.data.relational.core.sql.Condition;
+import org.springframework.data.relational.core.sql.Expression;
+import org.springframework.data.relational.core.sql.Named;
+import org.springframework.data.relational.core.sql.SubselectExpression;
+import org.springframework.data.relational.core.sql.Visitable;
+import org.springframework.lang.Nullable;
+
+/**
+ * {@link PartRenderer} for {@link Expression}s.
+ *
+ * @author Mark Paluch
+ * @author Jens Schauder
+ * @since 1.1
+ * @see Column
+ * @see SubselectExpression
+ */
+class ExpressionVisitor extends TypedSubtreeVisitor implements PartRenderer {
+
+ private final RenderContext context;
+
+ private CharSequence value = "";
+ private @Nullable PartRenderer partRenderer;
+
+ ExpressionVisitor(RenderContext context) {
+ this.context = context;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#enterMatched(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ Delegation enterMatched(Expression segment) {
+
+ if (segment instanceof SubselectExpression) {
+
+ SelectStatementVisitor visitor = new SelectStatementVisitor(context);
+ partRenderer = visitor;
+ return Delegation.delegateTo(visitor);
+ }
+
+ if (segment instanceof Column) {
+
+ RenderNamingStrategy namingStrategy = context.getNamingStrategy();
+ Column column = (Column) segment;
+
+ value = namingStrategy.getReferenceName(column.getTable()) + "." + namingStrategy.getReferenceName(column);
+ } else if (segment instanceof BindMarker) {
+
+ if (segment instanceof Named) {
+ value = ((Named) segment).getName();
+ } else {
+ value = segment.toString();
+ }
+ }
+
+ return Delegation.retain();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#enterNested(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ Delegation enterNested(Visitable segment) {
+
+ if (segment instanceof Condition) {
+ ConditionVisitor visitor = new ConditionVisitor(context);
+ partRenderer = visitor;
+ return Delegation.delegateTo(visitor);
+ }
+
+ return super.enterNested(segment);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#leaveMatched(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ Delegation leaveMatched(Expression segment) {
+
+ if (partRenderer != null) {
+ value = partRenderer.getRenderedPart();
+ partRenderer = null;
+ }
+
+ return super.leaveMatched(segment);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.PartRenderer#getRenderedPart()
+ */
+ @Override
+ public CharSequence getRenderedPart() {
+ return value;
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/FilteredSingleConditionRenderSupport.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/FilteredSingleConditionRenderSupport.java
new file mode 100644
index 0000000000..99f9b5dff6
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/FilteredSingleConditionRenderSupport.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.relational.core.sql.render;
+
+import java.util.function.Predicate;
+
+import org.springframework.data.relational.core.sql.Condition;
+import org.springframework.data.relational.core.sql.Expression;
+import org.springframework.data.relational.core.sql.Visitable;
+import org.springframework.util.Assert;
+
+/**
+ * Support class for {@link FilteredSubtreeVisitor filtering visitors} that want to render a single {@link Condition}
+ * and delegate nested {@link Expression} and {@link Condition} rendering.
+ *
+ * @author Mark Paluch
+ * @since 1.1
+ */
+abstract class FilteredSingleConditionRenderSupport extends FilteredSubtreeVisitor {
+
+ private final RenderContext context;
+ private PartRenderer current;
+
+ /**
+ * Creates a new {@link FilteredSingleConditionRenderSupport} given the filter {@link Predicate}.
+ *
+ * @param context
+ * @param filter filter predicate to identify when to {@link #enterMatched(Visitable)
+ * enter}/{@link #leaveMatched(Visitable) leave} the {@link Visitable segment} that this visitor is
+ * responsible for.
+ */
+ FilteredSingleConditionRenderSupport(RenderContext context, Predicate filter) {
+ super(filter);
+ this.context = context;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.FilteredSubtreeVisitor#enterNested(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ Delegation enterNested(Visitable segment) {
+
+ if (segment instanceof Expression) {
+ ExpressionVisitor visitor = new ExpressionVisitor(context);
+ current = visitor;
+ return Delegation.delegateTo(visitor);
+ }
+
+ if (segment instanceof Condition) {
+ ConditionVisitor visitor = new ConditionVisitor(context);
+ current = visitor;
+ return Delegation.delegateTo(visitor);
+ }
+
+ throw new IllegalStateException("Cannot provide visitor for " + segment);
+ }
+
+ /**
+ * Returns whether rendering was delegated to a {@link ExpressionVisitor} or {@link ConditionVisitor}.
+ *
+ * @return {@literal true} when rendering was delegated to a {@link ExpressionVisitor} or {@link ConditionVisitor}.
+ */
+ protected boolean hasDelegatedRendering() {
+ return current != null;
+ }
+
+ /**
+ * Consumes the delegated rendering part. Call {@link #hasDelegatedRendering()} to check whether rendering was
+ * actually delegated. Consumption releases the delegated rendered.
+ *
+ * @return the delegated rendered part.
+ * @throws IllegalStateException if rendering was not delegate.
+ */
+ protected CharSequence consumeRenderedPart() {
+
+ Assert.state(hasDelegatedRendering(), "Rendering not delegated. Cannot consume delegated rendering part.");
+
+ PartRenderer current = this.current;
+ this.current = null;
+ return current.getRenderedPart();
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/FilteredSubtreeVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/FilteredSubtreeVisitor.java
new file mode 100644
index 0000000000..6b370616ac
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/FilteredSubtreeVisitor.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.relational.core.sql.render;
+
+import java.util.function.Predicate;
+
+import org.springframework.data.relational.core.sql.Visitable;
+import org.springframework.data.relational.core.sql.Visitor;
+import org.springframework.lang.Nullable;
+
+/**
+ * Filtering {@link DelegatingVisitor visitor} applying a {@link Predicate filter}. Typically used as base class for
+ * {@link Visitor visitors} that wish to apply hierarchical processing based on a well-defined entry {@link Visitor
+ * segment}.
+ *
+ * Filtering is a three-way process:
+ *
+ * - Ignores elements that do not match the filter {@link Predicate}.
+ * - {@link #enterMatched(Visitable) enter}/{@link #leaveMatched(Visitable) leave} matched callbacks for the
+ * {@link Visitable segment} that matches the {@link Predicate}.
+ * - {@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}.
+ *
+ *
+ * @author Mark Paluch
+ * @see TypedSubtreeVisitor
+ * @since 1.1
+ */
+abstract class FilteredSubtreeVisitor extends DelegatingVisitor {
+
+ private final Predicate filter;
+
+ private @Nullable Visitable currentSegment;
+
+ /**
+ * Creates a new {@link FilteredSubtreeVisitor} given the filter {@link Predicate}.
+ *
+ * @param filter filter predicate to identify when to {@link #enterMatched(Visitable)
+ * enter}/{@link #leaveMatched(Visitable) leave} the {@link Visitable segment} that this visitor is
+ * responsible for.
+ */
+ FilteredSubtreeVisitor(Predicate filter) {
+ this.filter = filter;
+ }
+
+ /**
+ * {@link Visitor#enter(Visitable) Enter} callback for a {@link Visitable} that this {@link Visitor} is responsible
+ * for. The default implementation retains delegation by default.
+ *
+ * @param segment the segment, must not be {@literal null}.
+ * @return delegation options. Can be either {@link Delegation#retain()} or
+ * {@link Delegation#delegateTo(DelegatingVisitor)}.
+ * @see Delegation#retain()
+ */
+ Delegation enterMatched(Visitable segment) {
+ return Delegation.retain();
+ }
+
+ /**
+ * {@link Visitor#enter(Visitable) Enter} callback for a nested {@link Visitable}. The default implementation retains
+ * delegation by default.
+ *
+ * @param segment the segment, must not be {@literal null}.
+ * @return delegation options. Can be either {@link Delegation#retain()} or
+ * {@link Delegation#delegateTo(DelegatingVisitor)}.
+ * @see Delegation#retain()
+ */
+ Delegation enterNested(Visitable segment) {
+ return Delegation.retain();
+ }
+
+ /**
+ * {@link Visitor#leave(Visitable) Leave} callback for the matched {@link Visitable}. The default implementation steps
+ * back from delegation by default.
+ *
+ * @param segment the segment, must not be {@literal null}.
+ * @return delegation options. Can be either {@link Delegation#retain()} or {@link Delegation#leave()}.
+ * @see Delegation#leave()
+ */
+ Delegation leaveMatched(Visitable segment) {
+ return Delegation.leave();
+ }
+
+ /**
+ * {@link Visitor#leave(Visitable) Leave} callback for a nested {@link Visitable}. The default implementation retains
+ * delegation by default.
+ *
+ * @param segment the segment, must not be {@literal null}.
+ * @return delegation options. Can be either {@link Delegation#retain()} or {@link Delegation#leave()}.
+ * @see Delegation#retain()
+ */
+ Delegation leaveNested(Visitable segment) {
+ return Delegation.retain();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.DelegatingVisitor#doEnter(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ public final Delegation doEnter(Visitable segment) {
+
+ if (currentSegment == null) {
+
+ if (filter.test(segment)) {
+ currentSegment = segment;
+ return enterMatched(segment);
+ }
+ } else {
+ return enterNested(segment);
+ }
+
+ return Delegation.retain();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.DelegatingVisitor#doLeave(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ public final Delegation doLeave(Visitable segment) {
+
+ if (currentSegment == null) {
+ return Delegation.leave();
+ } else if (segment == currentSegment) {
+ currentSegment = null;
+ return leaveMatched(segment);
+ } else {
+ return leaveNested(segment);
+ }
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/FromClauseVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/FromClauseVisitor.java
new file mode 100644
index 0000000000..2a334ac4cf
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/FromClauseVisitor.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.relational.core.sql.render;
+
+import org.springframework.data.relational.core.sql.From;
+import org.springframework.data.relational.core.sql.Visitable;
+
+/**
+ * Renderer for {@link From}. Uses a {@link RenderTarget} to call back for render results.
+ *
+ * @author Mark Paluch
+ * @author Jens Schauder
+ * @since 1.1
+ */
+class FromClauseVisitor extends TypedSubtreeVisitor {
+
+ private final FromTableVisitor visitor;
+ private final RenderTarget parent;
+ private final StringBuilder builder = new StringBuilder();
+ private boolean first = true;
+
+ FromClauseVisitor(RenderContext context, RenderTarget parent) {
+
+ this.visitor = new FromTableVisitor(context, it -> {
+
+ if (first) {
+ first = false;
+ } else {
+ builder.append(", ");
+ }
+
+ builder.append(it);
+ });
+
+ this.parent = parent;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#enterNested(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ Delegation enterNested(Visitable segment) {
+ return Delegation.delegateTo(visitor);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#leaveMatched(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ Delegation leaveMatched(From segment) {
+ parent.onRendered(builder);
+ return super.leaveMatched(segment);
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/FromTableVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/FromTableVisitor.java
new file mode 100644
index 0000000000..7870c6cfb9
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/FromTableVisitor.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.relational.core.sql.render;
+
+import org.springframework.data.relational.core.sql.Aliased;
+import org.springframework.data.relational.core.sql.From;
+import org.springframework.data.relational.core.sql.Table;
+
+/**
+ * Renderer for {@link Table} used within a {@link From} clause. Uses a {@link RenderTarget} to call back for render
+ * results.
+ *
+ * @author Mark Paluch
+ * @author Jens Schauder
+ * @since 1.1
+ */
+class FromTableVisitor extends TypedSubtreeVisitor {
+
+ private final RenderContext context;
+ private final RenderTarget parent;
+
+ FromTableVisitor(RenderContext context, RenderTarget parent) {
+ super();
+ this.context = context;
+ this.parent = parent;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#enterMatched(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ Delegation enterMatched(Table segment) {
+
+ StringBuilder builder = new StringBuilder();
+
+ builder.append(context.getNamingStrategy().getName(segment));
+ if (segment instanceof Aliased) {
+ builder.append(" AS ").append(((Aliased) segment).getAlias());
+ }
+
+ parent.onRendered(builder);
+
+ return super.enterMatched(segment);
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/InVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/InVisitor.java
new file mode 100644
index 0000000000..3992f07f68
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/InVisitor.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.relational.core.sql.render;
+
+import org.springframework.data.relational.core.sql.In;
+import org.springframework.data.relational.core.sql.Visitable;
+
+/**
+ * Renderer for {@link In}. Uses a {@link RenderTarget} to call back for render results.
+ *
+ * @author Mark Paluch
+ * @author Jens Schauder
+ * @since 1.1
+ */
+class InVisitor extends TypedSingleConditionRenderSupport {
+
+ private final RenderTarget target;
+ private final StringBuilder part = new StringBuilder();
+ private boolean needsComma = false;
+
+ InVisitor(RenderContext context, RenderTarget target) {
+ super(context);
+ this.target = target;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#leaveNested(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ Delegation leaveNested(Visitable segment) {
+
+ if (hasDelegatedRendering()) {
+ CharSequence renderedPart = consumeRenderedPart();
+
+ if (needsComma) {
+ part.append(", ");
+ }
+
+ if (part.length() == 0) {
+ part.append(renderedPart);
+ part.append(" IN (");
+ } else {
+ part.append(renderedPart);
+ needsComma = true;
+ }
+ }
+
+ return super.leaveNested(segment);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#leaveMatched(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ Delegation leaveMatched(In segment) {
+
+ part.append(")");
+ target.onRendered(part);
+
+ return super.leaveMatched(segment);
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/IsNullVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/IsNullVisitor.java
new file mode 100644
index 0000000000..4a551154b4
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/IsNullVisitor.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.relational.core.sql.render;
+
+import org.springframework.data.relational.core.sql.IsNull;
+import org.springframework.data.relational.core.sql.Visitable;
+
+/**
+ * Renderer for {@link IsNull}. Uses a {@link RenderTarget} to call back for render results.
+ *
+ * @author Mark Paluch
+ * @author Jens Schauder
+ * @since 1.1
+ */
+class IsNullVisitor extends TypedSingleConditionRenderSupport {
+
+ private final RenderTarget target;
+ private final StringBuilder part = new StringBuilder();
+
+ IsNullVisitor(RenderContext context, RenderTarget target) {
+ super(context);
+ this.target = target;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#leaveNested(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ Delegation leaveNested(Visitable segment) {
+
+ if (hasDelegatedRendering()) {
+ part.append(consumeRenderedPart());
+ }
+
+ return super.leaveNested(segment);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#leaveMatched(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ Delegation leaveMatched(IsNull segment) {
+
+ if (segment.isNegated()) {
+ part.append(" IS NOT NULL");
+ } else {
+ part.append(" IS NULL");
+ }
+
+ target.onRendered(part);
+
+ return super.leaveMatched(segment);
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/JoinVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/JoinVisitor.java
new file mode 100644
index 0000000000..c174c35d3f
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/JoinVisitor.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.relational.core.sql.render;
+
+import org.springframework.data.relational.core.sql.Aliased;
+import org.springframework.data.relational.core.sql.Condition;
+import org.springframework.data.relational.core.sql.Join;
+import org.springframework.data.relational.core.sql.Table;
+import org.springframework.data.relational.core.sql.Visitable;
+
+/**
+ * Renderer for {@link Join} segments. Uses a {@link RenderTarget} to call back for render results.
+ *
+ * @author Mark Paluch
+ * @author Jens Schauder
+ * @since 1.1
+ */
+class JoinVisitor extends TypedSubtreeVisitor {
+
+ private final RenderContext context;
+ private final RenderTarget parent;
+ private final StringBuilder joinClause = new StringBuilder();
+ private boolean inCondition = false;
+ private boolean hasSeenCondition = false;
+
+ JoinVisitor(RenderContext context, RenderTarget parent) {
+ this.context = context;
+ this.parent = parent;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#enterMatched(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ Delegation enterMatched(Join segment) {
+
+ joinClause.append(segment.getType().getSql()).append(' ');
+
+ return super.enterMatched(segment);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#enterNested(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ Delegation enterNested(Visitable segment) {
+
+ if (segment instanceof Table && !inCondition) {
+ joinClause.append(context.getNamingStrategy().getName(((Table) segment)));
+ if (segment instanceof Aliased) {
+ joinClause.append(" AS ").append(((Aliased) segment).getAlias());
+ }
+ } else if (segment instanceof Condition) {
+
+ // TODO: Use proper delegation for condition rendering.
+ inCondition = true;
+ if (!hasSeenCondition) {
+ hasSeenCondition = true;
+ joinClause.append(" ON ");
+ joinClause.append(segment);
+ }
+ }
+
+ return super.enterNested(segment);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#leaveNested(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ Delegation leaveNested(Visitable segment) {
+
+ if (segment instanceof Condition) {
+ inCondition = false;
+ }
+ return super.leaveNested(segment);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#leaveMatched(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ Delegation leaveMatched(Join segment) {
+ parent.onRendered(joinClause);
+ return super.leaveMatched(segment);
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/LikeVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/LikeVisitor.java
new file mode 100644
index 0000000000..2ed8ef0d44
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/LikeVisitor.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.relational.core.sql.render;
+
+import org.springframework.data.relational.core.sql.Condition;
+import org.springframework.data.relational.core.sql.Expression;
+import org.springframework.data.relational.core.sql.Like;
+import org.springframework.data.relational.core.sql.Visitable;
+import org.springframework.lang.Nullable;
+
+/**
+ * {@link org.springframework.data.relational.core.sql.Visitor} rendering comparison {@link Condition}. Uses a
+ * {@link RenderTarget} to call back for render results.
+ *
+ * @author Mark Paluch
+ * @see Like
+ * @since 1.1
+ */
+class LikeVisitor extends FilteredSubtreeVisitor {
+
+ private final RenderContext context;
+ private final RenderTarget target;
+ private final StringBuilder part = new StringBuilder();
+ private @Nullable PartRenderer current;
+
+ LikeVisitor(Like condition, RenderContext context, RenderTarget target) {
+ super(it -> it == condition);
+ this.context = context;
+ this.target = target;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.FilteredSubtreeVisitor#enterNested(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ Delegation enterNested(Visitable segment) {
+
+ if (segment instanceof Expression) {
+ ExpressionVisitor visitor = new ExpressionVisitor(context);
+ current = visitor;
+ return Delegation.delegateTo(visitor);
+ }
+
+ if (segment instanceof Condition) {
+ ConditionVisitor visitor = new ConditionVisitor(context);
+ current = visitor;
+ return Delegation.delegateTo(visitor);
+ }
+
+ throw new IllegalStateException("Cannot provide visitor for " + segment);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.FilteredSubtreeVisitor#leaveNested(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ Delegation leaveNested(Visitable segment) {
+
+ if (current != null) {
+ if (part.length() != 0) {
+ part.append(" LIKE ");
+ }
+
+ part.append(current.getRenderedPart());
+ current = null;
+ }
+
+ return super.leaveNested(segment);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.FilteredSubtreeVisitor#leaveMatched(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ Delegation leaveMatched(Visitable segment) {
+
+ target.onRendered(part);
+
+ return super.leaveMatched(segment);
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/MultiConcatConditionVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/MultiConcatConditionVisitor.java
new file mode 100644
index 0000000000..288725ccb9
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/MultiConcatConditionVisitor.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.relational.core.sql.render;
+
+import org.springframework.data.relational.core.sql.AndCondition;
+import org.springframework.data.relational.core.sql.OrCondition;
+import org.springframework.data.relational.core.sql.Visitable;
+
+/**
+ * Renderer for {@link AndCondition} and {@link OrCondition}. Uses a {@link RenderTarget} to call back for render
+ * results.
+ *
+ * @author Mark Paluch
+ * @author Jens Schauder
+ * @since 1.1
+ */
+class MultiConcatConditionVisitor extends FilteredSingleConditionRenderSupport {
+
+ private final RenderTarget target;
+ private final String concat;
+ private final StringBuilder part = new StringBuilder();
+
+ MultiConcatConditionVisitor(RenderContext context, AndCondition condition, RenderTarget target) {
+ super(context, it -> it == condition);
+ this.target = target;
+ this.concat = " AND ";
+ }
+
+ MultiConcatConditionVisitor(RenderContext context, OrCondition condition, RenderTarget target) {
+ super(context, it -> it == condition);
+ this.target = target;
+ this.concat = " OR ";
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.FilteredSubtreeVisitor#leaveNested(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ Delegation leaveNested(Visitable segment) {
+
+ if (hasDelegatedRendering()) {
+ if (part.length() != 0) {
+ part.append(concat);
+ }
+
+ part.append(consumeRenderedPart());
+ }
+
+ return super.leaveNested(segment);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.FilteredSubtreeVisitor#leaveMatched(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ Delegation leaveMatched(Visitable segment) {
+
+ target.onRendered(part);
+
+ return super.leaveMatched(segment);
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/NamingStrategies.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/NamingStrategies.java
new file mode 100644
index 0000000000..32650a8376
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/NamingStrategies.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.relational.core.sql.render;
+
+import lombok.RequiredArgsConstructor;
+
+import java.util.Locale;
+import java.util.function.Function;
+
+import org.springframework.data.relational.core.sql.Column;
+import org.springframework.data.relational.core.sql.Table;
+import org.springframework.util.Assert;
+
+/**
+ * Factory for {@link RenderNamingStrategy} objects.
+ *
+ * @author Mark Paluch
+ * @since 1.1
+ */
+public abstract class NamingStrategies {
+
+ private NamingStrategies() {}
+
+ /**
+ * Creates a as-is {@link RenderNamingStrategy} that preserves {@link Column} and {@link Table} names as they were
+ * expressed during their declaration.
+ *
+ * @return as-is {@link RenderNamingStrategy}.
+ */
+ public static RenderNamingStrategy asIs() {
+ return AsIs.INSTANCE;
+ }
+
+ /**
+ * Creates a mapping {@link RenderNamingStrategy} that applies a {@link Function mapping function} to {@link Column}
+ * and {@link Table} names.
+ *
+ * @param mappingFunction the mapping {@link Function}, must not be {@literal null}.
+ * @return the mapping {@link RenderNamingStrategy}.
+ */
+ public static RenderNamingStrategy mapWith(Function mappingFunction) {
+ return AsIs.INSTANCE.map(mappingFunction);
+ }
+
+ /**
+ * Creates a mapping {@link RenderNamingStrategy} that converts {@link Column} and {@link Table} names to upper case
+ * using the default {@link Locale}.
+ *
+ * @return upper-casing {@link RenderNamingStrategy}.
+ * @see String#toUpperCase()
+ * @see Locale
+ */
+ public static RenderNamingStrategy toUpper() {
+ return toUpper(Locale.getDefault());
+ }
+
+ /**
+ * Creates a mapping {@link RenderNamingStrategy} that converts {@link Column} and {@link Table} names to upper case
+ * using the given {@link Locale}.
+ *
+ * @param locale the locale to use.
+ * @return upper-casing {@link RenderNamingStrategy}.
+ * @see String#toUpperCase(Locale)
+ */
+ public static RenderNamingStrategy toUpper(Locale locale) {
+
+ Assert.notNull(locale, "Locale must not be null");
+
+ return AsIs.INSTANCE.map(it -> it.toUpperCase(locale));
+ }
+
+ /**
+ * Creates a mapping {@link RenderNamingStrategy} that converts {@link Column} and {@link Table} names to lower case
+ * using the default {@link Locale}.
+ *
+ * @return lower-casing {@link RenderNamingStrategy}.
+ * @see String#toLowerCase()
+ * @see Locale
+ */
+ public static RenderNamingStrategy toLower() {
+ return toLower(Locale.getDefault());
+ }
+
+ /**
+ * Creates a mapping {@link RenderNamingStrategy} that converts {@link Column} and {@link Table} names to lower case
+ * using the given {@link Locale}.
+ *
+ * @param locale the locale to use.
+ * @return lower-casing {@link RenderNamingStrategy}.
+ * @see String#toLowerCase(Locale)
+ * @see Locale
+ */
+ public static RenderNamingStrategy toLower(Locale locale) {
+
+ Assert.notNull(locale, "Locale must not be null");
+
+ return AsIs.INSTANCE.map(it -> it.toLowerCase(locale));
+ }
+
+ enum AsIs implements RenderNamingStrategy {
+ INSTANCE;
+ }
+
+ @RequiredArgsConstructor
+ static class DelegatingRenderNamingStrategy implements RenderNamingStrategy {
+
+ private final RenderNamingStrategy delegate;
+ private final Function mappingFunction;
+
+ @Override
+ public String getName(Column column) {
+ return mappingFunction.apply(delegate.getName(column));
+ }
+
+ @Override
+ public String getReferenceName(Column column) {
+ return mappingFunction.apply(delegate.getReferenceName(column));
+ }
+
+ @Override
+ public String getName(Table table) {
+ return mappingFunction.apply(delegate.getName(table));
+ }
+
+ @Override
+ public String getReferenceName(Table table) {
+ return mappingFunction.apply(delegate.getReferenceName(table));
+ }
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/OrderByClauseVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/OrderByClauseVisitor.java
new file mode 100644
index 0000000000..d7573c0772
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/OrderByClauseVisitor.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.relational.core.sql.render;
+
+import org.springframework.data.relational.core.sql.Column;
+import org.springframework.data.relational.core.sql.OrderByField;
+import org.springframework.data.relational.core.sql.Visitable;
+
+/**
+ * {@link PartRenderer} for {@link OrderByField}s.
+ *
+ * @author Mark Paluch
+ * @author Jens Schauder
+ * @since 1.1
+ */
+class OrderByClauseVisitor extends TypedSubtreeVisitor implements PartRenderer {
+
+ private final RenderContext context;
+
+ private final StringBuilder builder = new StringBuilder();
+ private boolean first = true;
+
+ OrderByClauseVisitor(RenderContext context) {
+ this.context = context;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#enterMatched(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ Delegation enterMatched(OrderByField segment) {
+
+ if (!first) {
+ builder.append(", ");
+ }
+ first = false;
+
+ return super.enterMatched(segment);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#leaveMatched(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ Delegation leaveMatched(OrderByField segment) {
+
+ OrderByField field = segment;
+
+ if (field.getDirection() != null) {
+ builder.append(" ") //
+ .append(field.getDirection());
+ }
+
+ return Delegation.leave();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#leaveNested(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ Delegation leaveNested(Visitable segment) {
+
+ if (segment instanceof Column) {
+ builder.append(context.getNamingStrategy().getReferenceName(((Column) segment)));
+ }
+
+ return super.leaveNested(segment);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.PartRenderer#getRenderedPart()
+ */
+ @Override
+ public CharSequence getRenderedPart() {
+ return builder;
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/PartRenderer.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/PartRenderer.java
new file mode 100644
index 0000000000..15963631f0
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/PartRenderer.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.relational.core.sql.render;
+
+import org.springframework.data.relational.core.sql.Visitor;
+
+/**
+ * {@link Visitor} that renders a specific partial clause or expression.
+ *
+ * @author Mark Paluch
+ * @since 1.1
+ */
+interface PartRenderer extends Visitor {
+
+ /**
+ * Returns the rendered part.
+ *
+ * @return the rendered part.
+ */
+ CharSequence getRenderedPart();
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/RenderContext.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/RenderContext.java
new file mode 100644
index 0000000000..161c6059c3
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/RenderContext.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.relational.core.sql.render;
+
+/**
+ * Render context providing {@link RenderNamingStrategy} and other resources that are required during rendering.
+ *
+ * @author Mark Paluch
+ * @since 1.1
+ */
+public interface RenderContext {
+
+ /**
+ * Returns the configured {@link RenderNamingStrategy}.
+ *
+ * @return the {@link RenderNamingStrategy}.
+ */
+ RenderNamingStrategy getNamingStrategy();
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/RenderNamingStrategy.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/RenderNamingStrategy.java
new file mode 100644
index 0000000000..b6182e2570
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/RenderNamingStrategy.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.relational.core.sql.render;
+
+import java.util.function.Function;
+
+import org.springframework.data.relational.core.sql.Column;
+import org.springframework.data.relational.core.sql.Table;
+import org.springframework.data.relational.core.sql.render.NamingStrategies.DelegatingRenderNamingStrategy;
+import org.springframework.util.Assert;
+
+/**
+ * Naming strategy for SQL rendering.
+ *
+ * @author Mark Paluch
+ * @see NamingStrategies
+ * @since 1.1
+ */
+public interface RenderNamingStrategy {
+
+ /**
+ * Return the {@link Column#getName() column name}.
+ *
+ * @param column the column.
+ * @return the {@link Column#getName() column name}.
+ * @see Column#getName()
+ */
+ default String getName(Column column) {
+ return column.getName();
+ }
+
+ /**
+ * Return the {@link Column#getName() column reference name}.
+ *
+ * @param column the column.
+ * @return the {@link Column#getName() column reference name}.
+ * @see Column#getReferenceName() ()
+ */
+ default String getReferenceName(Column column) {
+ return column.getReferenceName();
+ }
+
+ /**
+ * Return the {@link Table#getName() table name}.
+ *
+ * @param table the table.
+ * @return the {@link Table#getName() table name}.
+ * @see Table#getName()
+ */
+ default String getName(Table table) {
+ return table.getName();
+ }
+
+ /**
+ * Return the {@link Table#getReferenceName() table reference name}.
+ *
+ * @param table the table.
+ * @return the {@link Table#getReferenceName() table name}.
+ * @see Table#getReferenceName()
+ */
+ default String getReferenceName(Table table) {
+ return table.getReferenceName();
+ }
+
+ /**
+ * Applies a {@link Function mapping function} after retrieving the object (column name, column reference name, …)
+ * name.
+ *
+ * @param mappingFunction the function that maps an object name.
+ * @return a new {@link RenderNamingStrategy} applying {@link Function mapping function}.
+ */
+ default RenderNamingStrategy map(Function mappingFunction) {
+
+ Assert.notNull(mappingFunction, "Mapping function must not be null!");
+
+ return new DelegatingRenderNamingStrategy(this, mappingFunction);
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/RenderTarget.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/RenderTarget.java
new file mode 100644
index 0000000000..91e21efd5f
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/RenderTarget.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.relational.core.sql.render;
+
+import org.springframework.data.relational.core.sql.Visitor;
+
+/**
+ * Callback interface for {@link Visitor visitors} that wish to notify a render target when they are complete with
+ * rendering.
+ *
+ * @author Mark Paluch
+ * @since 1.1
+ */
+@FunctionalInterface
+interface RenderTarget {
+
+ /**
+ * Callback method that is invoked once the rendering for a part or expression is finished. When called multiple
+ * times, it's the responsibility of the implementor to ensure proper concatenation of render results.
+ *
+ * @param sequence the rendered part or expression.
+ */
+ void onRendered(CharSequence sequence);
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SelectListVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SelectListVisitor.java
new file mode 100644
index 0000000000..00432d52f5
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SelectListVisitor.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.relational.core.sql.render;
+
+import org.springframework.data.relational.core.sql.Aliased;
+import org.springframework.data.relational.core.sql.Column;
+import org.springframework.data.relational.core.sql.SelectList;
+import org.springframework.data.relational.core.sql.SimpleFunction;
+import org.springframework.data.relational.core.sql.Table;
+import org.springframework.data.relational.core.sql.Visitable;
+
+/**
+ * {@link PartRenderer} for {@link SelectList}s.
+ *
+ * @author Mark Paluch
+ * @author Jens Schauder
+ * @since 1.1
+ */
+class SelectListVisitor extends TypedSubtreeVisitor implements PartRenderer {
+
+ private final RenderContext context;
+ private final StringBuilder builder = new StringBuilder();
+ private final RenderTarget target;
+ private boolean requiresComma = false;
+ private boolean insideFunction = false; // this is hackery and should be fix with a proper visitor for
+ // subelements.
+
+ SelectListVisitor(RenderContext context, RenderTarget target) {
+ this.context = context;
+ this.target = target;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#enterNested(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ Delegation enterNested(Visitable segment) {
+
+ if (requiresComma) {
+ builder.append(", ");
+ requiresComma = false;
+ }
+ if (segment instanceof SimpleFunction) {
+ builder.append(((SimpleFunction) segment).getFunctionName()).append("(");
+ insideFunction = true;
+ } else {
+ insideFunction = false;
+ }
+
+ return super.enterNested(segment);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#leaveMatched(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ Delegation leaveMatched(SelectList segment) {
+
+ target.onRendered(builder);
+ return super.leaveMatched(segment);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#leaveNested(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ Delegation leaveNested(Visitable segment) {
+
+ if (segment instanceof Table) {
+ builder.append(context.getNamingStrategy().getReferenceName((Table) segment)).append('.');
+ }
+
+ if (segment instanceof SimpleFunction) {
+ builder.append(")");
+ requiresComma = true;
+ } else if (segment instanceof Column) {
+ builder.append(context.getNamingStrategy().getName((Column) segment));
+ if (segment instanceof Aliased) {
+ builder.append(" AS ").append(((Aliased) segment).getAlias());
+ }
+ requiresComma = true;
+ }
+
+ return super.leaveNested(segment);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.PartRenderer#getRenderedPart()
+ */
+ @Override
+ public CharSequence getRenderedPart() {
+ return builder;
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SelectStatementVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SelectStatementVisitor.java
new file mode 100644
index 0000000000..424bd65678
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SelectStatementVisitor.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.relational.core.sql.render;
+
+import java.util.OptionalLong;
+
+import org.springframework.data.relational.core.sql.From;
+import org.springframework.data.relational.core.sql.Join;
+import org.springframework.data.relational.core.sql.OrderByField;
+import org.springframework.data.relational.core.sql.Select;
+import org.springframework.data.relational.core.sql.SelectList;
+import org.springframework.data.relational.core.sql.Visitable;
+import org.springframework.data.relational.core.sql.Where;
+
+/**
+ * {@link PartRenderer} for {@link Select} statements.
+ *
+ * @author Mark Paluch
+ * @author Jens Schauder
+ * @since 1.1
+ */
+class SelectStatementVisitor extends DelegatingVisitor implements PartRenderer {
+
+ private final RenderContext context;
+
+ private StringBuilder builder = new StringBuilder();
+ private StringBuilder selectList = new StringBuilder();
+ private StringBuilder from = new StringBuilder();
+ private StringBuilder join = new StringBuilder();
+ private StringBuilder where = new StringBuilder();
+
+ private SelectListVisitor selectListVisitor;
+ private OrderByClauseVisitor orderByClauseVisitor;
+ private FromClauseVisitor fromClauseVisitor;
+ private WhereClauseVisitor whereClauseVisitor;
+
+ SelectStatementVisitor(RenderContext context) {
+
+ this.context = context;
+ this.selectListVisitor = new SelectListVisitor(context, selectList::append);
+ this.orderByClauseVisitor = new OrderByClauseVisitor(context);
+ this.fromClauseVisitor = new FromClauseVisitor(context, it -> {
+
+ if (from.length() != 0) {
+ from.append(", ");
+ }
+
+ from.append(it);
+ });
+
+ this.whereClauseVisitor = new WhereClauseVisitor(context, where::append);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.DelegatingVisitor#doEnter(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ public Delegation doEnter(Visitable segment) {
+
+ if (segment instanceof SelectList) {
+ return Delegation.delegateTo(selectListVisitor);
+ }
+
+ if (segment instanceof OrderByField) {
+ return Delegation.delegateTo(orderByClauseVisitor);
+ }
+
+ if (segment instanceof From) {
+ return Delegation.delegateTo(fromClauseVisitor);
+ }
+
+ if (segment instanceof Join) {
+ return Delegation.delegateTo(new JoinVisitor(context, it -> {
+
+ if (join.length() != 0) {
+ join.append(' ');
+ }
+
+ join.append(it);
+ }));
+ }
+
+ if (segment instanceof Where) {
+ return Delegation.delegateTo(whereClauseVisitor);
+ }
+
+ return Delegation.retain();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.DelegatingVisitor#doLeave(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ public Delegation doLeave(Visitable segment) {
+
+ if (segment instanceof Select) {
+
+ builder.append("SELECT ");
+ if (((Select) segment).isDistinct()) {
+ builder.append("DISTINCT ");
+ }
+
+ builder.append(selectList);
+
+ if (from.length() != 0) {
+ builder.append(" FROM ").append(from);
+ }
+
+ if (join.length() != 0) {
+ builder.append(' ').append(join);
+ }
+
+ if (where.length() != 0) {
+ builder.append(" WHERE ").append(where);
+ }
+
+ CharSequence orderBy = orderByClauseVisitor.getRenderedPart();
+ if (orderBy.length() != 0)
+ builder.append(" ORDER BY ").append(orderBy);
+
+ OptionalLong limit = ((Select) segment).getLimit();
+ if (limit.isPresent()) {
+ builder.append(" LIMIT ").append(limit.getAsLong());
+ }
+
+ OptionalLong offset = ((Select) segment).getOffset();
+ if (offset.isPresent()) {
+ builder.append(" OFFSET ").append(offset.getAsLong());
+ }
+
+ return Delegation.leave();
+ }
+
+ return Delegation.retain();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.PartRenderer#getRenderedPart()
+ */
+ @Override
+ public CharSequence getRenderedPart() {
+ return builder;
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SimpleRenderContext.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SimpleRenderContext.java
new file mode 100644
index 0000000000..a9e065fb0a
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SimpleRenderContext.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.relational.core.sql.render;
+
+import lombok.Value;
+
+/**
+ * Default {@link RenderContext} implementation.
+ *
+ * @author Mark Paluch
+ * @since 1.1
+ */
+@Value
+class SimpleRenderContext implements RenderContext {
+
+ private final RenderNamingStrategy namingStrategy;
+
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SqlRenderer.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SqlRenderer.java
new file mode 100644
index 0000000000..b114173c7f
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SqlRenderer.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.relational.core.sql.render;
+
+import org.springframework.data.relational.core.sql.Select;
+import org.springframework.util.Assert;
+
+/**
+ * Naive SQL renderer that does not consider dialect specifics. This class is to evaluate requirements of a SQL
+ * renderer.
+ *
+ * @author Mark Paluch
+ * @author Jens Schauder
+ * @since 1.1
+ */
+public class SqlRenderer {
+
+ private final Select select;
+ private final RenderContext context;
+
+ private SqlRenderer(Select select, RenderContext context) {
+ this.context = context;
+
+ Assert.notNull(select, "Select must not be null!");
+
+ this.select = select;
+ }
+
+ /**
+ * Creates a new {@link SqlRenderer}.
+ *
+ * @param select must not be {@literal null}.
+ * @return the renderer.
+ */
+ public static SqlRenderer create(Select select) {
+ return new SqlRenderer(select, new SimpleRenderContext(NamingStrategies.asIs()));
+ }
+
+ /**
+ * Creates a new {@link SqlRenderer} using a {@link RenderContext}.
+ *
+ * @param select must not be {@literal null}.
+ * @param context must not be {@literal null}.
+ * @return the renderer.
+ */
+ public static SqlRenderer create(Select select, RenderContext context) {
+ return new SqlRenderer(select, context);
+ }
+
+ /**
+ * Renders a {@link Select} statement into its SQL representation.
+ *
+ * @param select must not be {@literal null}.
+ * @return the rendered statement.
+ */
+ public static String render(Select select) {
+ return create(select).render();
+ }
+
+ /**
+ * Render the {@link Select} AST into a SQL statement.
+ *
+ * @return the rendered statement.
+ */
+ public String render() {
+
+ SelectStatementVisitor visitor = new SelectStatementVisitor(context);
+ select.visit(visitor);
+
+ return visitor.getRenderedPart().toString();
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/TypedSingleConditionRenderSupport.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/TypedSingleConditionRenderSupport.java
new file mode 100644
index 0000000000..9e8aedf28f
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/TypedSingleConditionRenderSupport.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.relational.core.sql.render;
+
+import org.springframework.data.relational.core.sql.Condition;
+import org.springframework.data.relational.core.sql.Expression;
+import org.springframework.data.relational.core.sql.Visitable;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+
+/**
+ * Support class for {@link TypedSubtreeVisitor typed visitors} that want to render a single {@link Condition} and
+ * delegate nested {@link Expression} and {@link Condition} rendering.
+ *
+ * @author Mark Paluch
+ * @since 1.1
+ */
+abstract class TypedSingleConditionRenderSupport extends TypedSubtreeVisitor {
+
+ private final RenderContext context;
+ private @Nullable PartRenderer current;
+
+ TypedSingleConditionRenderSupport(RenderContext context) {
+ this.context = context;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#enterNested(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ Delegation enterNested(Visitable segment) {
+
+ if (segment instanceof Expression) {
+ ExpressionVisitor visitor = new ExpressionVisitor(context);
+ current = visitor;
+ return Delegation.delegateTo(visitor);
+ }
+
+ if (segment instanceof Condition) {
+ ConditionVisitor visitor = new ConditionVisitor(context);
+ current = visitor;
+ return Delegation.delegateTo(visitor);
+ }
+
+ throw new IllegalStateException("Cannot provide visitor for " + segment);
+ }
+
+ /**
+ * Returns whether rendering was delegated to a {@link ExpressionVisitor} or {@link ConditionVisitor}.
+ *
+ * @return {@literal true} when rendering was delegated to a {@link ExpressionVisitor} or {@link ConditionVisitor}.
+ */
+ protected boolean hasDelegatedRendering() {
+ return current != null;
+ }
+
+ /**
+ * Consumes the delegated rendering part. Call {@link #hasDelegatedRendering()} to check whether rendering was
+ * actually delegated. Consumption releases the delegated rendered.
+ *
+ * @return the delegated rendered part.
+ * @throws IllegalStateException if rendering was not delegate.
+ */
+ protected CharSequence consumeRenderedPart() {
+
+ Assert.state(hasDelegatedRendering(), "Rendering not delegated. Cannot consume delegated rendering part.");
+
+ PartRenderer current = this.current;
+ this.current = null;
+ return current.getRenderedPart();
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/TypedSubtreeVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/TypedSubtreeVisitor.java
new file mode 100644
index 0000000000..951b7bc857
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/TypedSubtreeVisitor.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.relational.core.sql.render;
+
+import java.util.function.Predicate;
+
+import org.springframework.core.ResolvableType;
+import org.springframework.data.relational.core.sql.Visitable;
+import org.springframework.data.relational.core.sql.Visitor;
+import org.springframework.lang.Nullable;
+
+/**
+ * Type-filtering {@link DelegatingVisitor visitor} applying a {@link Class type filter} derived from the generic type
+ * parameter. Typically used as base class for {@link Visitor visitors} that wish to apply hierarchical processing based
+ * on a well-defined entry {@link Visitor segment}.
+ *
+ * Filtering is a three-way process:
+ *
+ * - Ignores elements that do not match the filter {@link Predicate}.
+ * - {@link #enterMatched(Visitable) enter}/{@link #leaveMatched(Visitable) leave} matched callbacks for the
+ * {@link Visitable segment} that matches the {@link Predicate}.
+ * - {@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}.
+ *
+ *
+ * @author Mark Paluch
+ * @since 1.1
+ * @see FilteredSubtreeVisitor
+ */
+abstract class TypedSubtreeVisitor extends DelegatingVisitor {
+
+ private final ResolvableType type;
+ private @Nullable Visitable currentSegment;
+
+ /**
+ * Creates a new {@link TypedSubtreeVisitor}.
+ */
+ TypedSubtreeVisitor() {
+ this.type = ResolvableType.forClass(getClass()).as(TypedSubtreeVisitor.class).getGeneric(0);
+ }
+
+ /**
+ * {@link Visitor#enter(Visitable) Enter} callback for a {@link Visitable} that this {@link Visitor} is responsible
+ * for. The default implementation retains delegation by default.
+ *
+ * @param segment the segment, must not be {@literal null}.
+ * @return delegation options. Can be either {@link Delegation#retain()} or
+ * {@link Delegation#delegateTo(DelegatingVisitor)}.
+ * @see Delegation#retain()
+ */
+ Delegation enterMatched(T segment) {
+ return Delegation.retain();
+ }
+
+ /**
+ * {@link Visitor#enter(Visitable) Enter} callback for a nested {@link Visitable}. The default implementation retains
+ * delegation by default.
+ *
+ * @param segment the segment, must not be {@literal null}.
+ * @return delegation options. Can be either {@link Delegation#retain()} or
+ * {@link Delegation#delegateTo(DelegatingVisitor)}.
+ * @see Delegation#retain()
+ */
+ Delegation enterNested(Visitable segment) {
+ return Delegation.retain();
+ }
+
+ /**
+ * {@link Visitor#leave(Visitable) Leave} callback for the matched {@link Visitable}. The default implementation steps
+ * back from delegation by default.
+ *
+ * @param segment the segment, must not be {@literal null}.
+ * @return delegation options. Can be either {@link Delegation#retain()} or {@link Delegation#leave()}.
+ * @see Delegation#leave()
+ */
+ Delegation leaveMatched(T segment) {
+ return Delegation.leave();
+ }
+
+ /**
+ * {@link Visitor#leave(Visitable) Leave} callback for a nested {@link Visitable}. The default implementation retains
+ * delegation by default.
+ *
+ * @param segment the segment, must not be {@literal null}.
+ * @return delegation options. Can be either {@link Delegation#retain()} or {@link Delegation#leave()}.
+ * @see Delegation#retain()
+ */
+ Delegation leaveNested(Visitable segment) {
+ return Delegation.retain();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.DelegatingVisitor#doEnter(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @SuppressWarnings("unchecked")
+ @Override
+ public final Delegation doEnter(Visitable segment) {
+
+ if (currentSegment == null) {
+
+ if (this.type.isInstance(segment)) {
+
+ currentSegment = segment;
+ return enterMatched((T) segment);
+ }
+ } else {
+ return enterNested(segment);
+ }
+
+ return Delegation.retain();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.DelegatingVisitor#doLeave(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @SuppressWarnings("unchecked")
+ @Override
+ public final Delegation doLeave(Visitable segment) {
+
+ if (currentSegment == null) {
+ return Delegation.leave();
+ } else if (segment == currentSegment) {
+ currentSegment = null;
+ return leaveMatched((T) segment);
+ } else {
+ return leaveNested(segment);
+ }
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/WhereClauseVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/WhereClauseVisitor.java
new file mode 100644
index 0000000000..fa8ea8acb4
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/WhereClauseVisitor.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.relational.core.sql.render;
+
+import org.springframework.data.relational.core.sql.Condition;
+import org.springframework.data.relational.core.sql.Visitable;
+import org.springframework.data.relational.core.sql.Where;
+
+/**
+ * Renderer for {@link Where} segments. Uses a {@link RenderTarget} to call back for render results.
+ *
+ * @author Mark Paluch
+ * @author Jens Schauder
+ * @since 1.1
+ */
+class WhereClauseVisitor extends TypedSubtreeVisitor {
+
+ private final RenderTarget parent;
+ private final ConditionVisitor conditionVisitor;
+
+ WhereClauseVisitor(RenderContext context, RenderTarget parent) {
+ this.conditionVisitor = new ConditionVisitor(context);
+ this.parent = parent;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#enterNested(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ Delegation enterNested(Visitable segment) {
+
+ if (segment instanceof Condition) {
+ return Delegation.delegateTo(conditionVisitor);
+ }
+
+ return super.enterNested(segment);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#leaveMatched(org.springframework.data.relational.core.sql.Visitable)
+ */
+ @Override
+ Delegation leaveMatched(Where segment) {
+
+ parent.onRendered(conditionVisitor.getRenderedPart());
+ return super.leaveMatched(segment);
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/package-info.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/package-info.java
new file mode 100644
index 0000000000..34c541d4c3
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/package-info.java
@@ -0,0 +1,9 @@
+/**
+ * SQL rendering utilities to render SQL from the Statement Builder API.
+ */
+@NonNullApi
+@NonNullFields
+package org.springframework.data.relational.core.sql.render;
+
+import org.springframework.lang.NonNullApi;
+import org.springframework.lang.NonNullFields;
diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SelectBuilderUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SelectBuilderUnitTests.java
new file mode 100644
index 0000000000..4a446310cf
--- /dev/null
+++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SelectBuilderUnitTests.java
@@ -0,0 +1,150 @@
+/*
+ * 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 static org.assertj.core.api.Assertions.*;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.OptionalLong;
+
+import org.junit.Test;
+import org.springframework.data.relational.core.sql.Join.JoinType;
+
+/**
+ * Unit tests for {@link SelectBuilder}.
+ *
+ * @author Mark Paluch
+ */
+public class SelectBuilderUnitTests {
+
+ @Test // DATAJDBC-309
+ public void simpleSelect() {
+
+ SelectBuilder builder = StatementBuilder.select();
+
+ Table table = SQL.table("mytable");
+ Column foo = table.column("foo");
+ Column bar = table.column("bar");
+
+ Select select = builder.select(foo, bar).from(table).build();
+
+ CapturingSelectVisitor visitor = new CapturingSelectVisitor();
+ select.visit(visitor);
+
+ assertThat(visitor.enter).containsSequence(foo, table, bar, table, new From(table), table);
+ }
+
+ @Test // DATAJDBC-309
+ public void selectTop() {
+
+ SelectBuilder builder = StatementBuilder.select();
+
+ Table table = SQL.table("mytable");
+ Column foo = table.column("foo");
+
+ Select select = builder.top(10).select(foo).from(table).build();
+
+ CapturingSelectVisitor visitor = new CapturingSelectVisitor();
+ select.visit(visitor);
+
+ assertThat(visitor.enter).containsSequence(foo, table, new From(table), table);
+ assertThat(select.getLimit()).isEqualTo(OptionalLong.of(10));
+ }
+
+ @Test // DATAJDBC-309
+ public void moreAdvancedSelect() {
+
+ SelectBuilder builder = StatementBuilder.select();
+
+ Table table1 = SQL.table("mytable1");
+ Table table2 = SQL.table("mytable2");
+
+ Column foo = SQL.column("foo", table1).as("foo_from_table1");
+ Column bar = SQL.column("foo", table2).as("foo_from_table1");
+
+ Select select = builder.select(foo, bar).from(table1, table2).build();
+
+ CapturingSelectVisitor visitor = new CapturingSelectVisitor();
+ select.visit(visitor);
+
+ assertThat(visitor.enter).containsSequence(foo, table1, bar, table2, new From(table1, table2), table1, table2);
+ }
+
+ @Test // DATAJDBC-309
+ public void orderBy() {
+
+ SelectBuilder builder = StatementBuilder.select();
+
+ Table table = SQL.table("mytable");
+
+ Column foo = SQL.column("foo", table).as("foo");
+
+ OrderByField orderByField = OrderByField.from(foo).asc();
+ Select select = builder.select(foo).from(table).orderBy(orderByField).build();
+
+ CapturingSelectVisitor visitor = new CapturingSelectVisitor();
+ select.visit(visitor);
+
+ assertThat(visitor.enter).containsSequence(foo, table, new From(table), table, orderByField, foo);
+ }
+
+ @Test // DATAJDBC-309
+ public void joins() {
+
+ SelectBuilder builder = StatementBuilder.select();
+
+ Table employee = SQL.table("employee");
+ Table department = SQL.table("department");
+
+ Column name = employee.column("name").as("emp_name");
+ Column department_name = employee.column("name").as("department_name");
+
+ Select select = builder.select(name, department_name).from(employee).join(department)
+ .on(SQL.column("department_id", employee)).equals(SQL.column("id", department))
+ .and(SQL.column("tenant", employee)).equals(SQL.column("tenant", department))
+ .orderBy(OrderByField.from(name).asc()).build();
+
+ CapturingSelectVisitor visitor = new CapturingSelectVisitor();
+ select.visit(visitor);
+
+ assertThat(visitor.enter).filteredOn(Join.class::isInstance).hasSize(1);
+
+ Join join = visitor.enter.stream().filter(Join.class::isInstance).map(Join.class::cast).findFirst().get();
+
+ assertThat(join.getJoinTable()).isEqualTo(department);
+ assertThat(join.getOn().toString()).isEqualTo(
+ new SimpleSegment("employee.department_id = department.id AND employee.tenant = department.tenant").toString());
+ assertThat(join.getType()).isEqualTo(JoinType.JOIN);
+ }
+
+ static class CapturingSelectVisitor implements Visitor {
+
+ final List enter = new ArrayList<>();
+
+ @Override
+ public void enter(Visitable segment) {
+ enter.add(segment);
+ }
+
+ @Override
+ public void leave(Visitable segment) {
+ leave.add(segment);
+ }
+
+ final List leave = new ArrayList<>();
+ }
+}
diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SelectValidatorUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SelectValidatorUnitTests.java
new file mode 100644
index 0000000000..d30525bb58
--- /dev/null
+++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SelectValidatorUnitTests.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.relational.core.sql;
+
+import static org.assertj.core.api.Assertions.*;
+
+import org.junit.Test;
+
+/**
+ * Unit tests for {@link SelectValidator}.
+ *
+ * @author Mark Paluch
+ */
+public class SelectValidatorUnitTests {
+
+ @Test // DATAJDBC-309
+ public void shouldReportMissingTableViaSelectlist() {
+
+ Column column = SQL.table("table").column("foo");
+
+ assertThatThrownBy(() -> {
+ 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 []");
+ }
+
+ @Test // DATAJDBC-309
+ public void shouldReportMissingTableViaSelectlistCount() {
+
+ Column column = SQL.table("table").column("foo");
+
+ assertThatThrownBy(() -> {
+ 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 []");
+ }
+
+ @Test // DATAJDBC-309
+ public void shouldReportMissingTableViaSelectlistDistinct() {
+
+ Column column = SQL.table("table").column("foo");
+
+ assertThatThrownBy(() -> {
+ 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 []");
+ }
+
+ @Test // DATAJDBC-309
+ public void shouldReportMissingTableViaOrderBy() {
+
+ Column foo = SQL.table("table").column("foo");
+ Table bar = SQL.table("bar");
+
+ assertThatThrownBy(() -> {
+ StatementBuilder.select(bar.column("foo")) //
+ .from(bar) //
+ .orderBy(foo) //
+ .build();
+ }).isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("Required table [table] by a ORDER BY column not imported by FROM [bar] or JOIN []");
+ }
+
+ @Test // DATAJDBC-309
+ public void shouldReportMissingTableViaWhere() {
+
+ Column column = SQL.table("table").column("foo");
+ Table bar = SQL.table("bar");
+
+ assertThatThrownBy(() -> {
+ StatementBuilder.select(bar.column("foo")) //
+ .from(bar) //
+ .where(new SimpleCondition(column, "=", "foo")) //
+ .build();
+ }).isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("Required table [table] by a WHERE predicate not imported by FROM [bar] or JOIN []");
+ }
+}
diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/ConditionRendererUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/ConditionRendererUnitTests.java
new file mode 100644
index 0000000000..edf3dc80e7
--- /dev/null
+++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/ConditionRendererUnitTests.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.relational.core.sql.render;
+
+import static org.assertj.core.api.Assertions.*;
+
+import org.junit.Test;
+
+import org.springframework.data.relational.core.sql.Column;
+import org.springframework.data.relational.core.sql.StatementBuilder;
+import org.springframework.data.relational.core.sql.Table;
+
+/**
+ * Unit tests for rendered {@link org.springframework.data.relational.core.sql.Conditions}.
+ *
+ * @author Mark Paluch
+ */
+public class ConditionRendererUnitTests {
+
+ Table table = Table.create("my_table");
+ Column left = table.column("left");
+ Column right = table.column("right");
+
+ @Test // DATAJDBC-309
+ public void shouldRenderEquals() {
+
+ String sql = SqlRenderer
+ .render(StatementBuilder.select(left).from(table).where(left.isEqualTo(right)).build());
+
+ assertThat(sql).endsWith("WHERE my_table.left = my_table.right");
+ }
+
+ @Test // DATAJDBC-309
+ public void shouldRenderNotEquals() {
+
+ String sql = SqlRenderer
+ .render(StatementBuilder.select(left).from(table).where(left.isNotEqualTo(right)).build());
+
+ assertThat(sql).endsWith("WHERE my_table.left != my_table.right");
+
+ sql = SqlRenderer.render(StatementBuilder.select(left).from(table).where(left.isEqualTo(right).not()).build());
+
+ assertThat(sql).endsWith("WHERE my_table.left != my_table.right");
+ }
+
+ @Test // DATAJDBC-309
+ public void shouldRenderIsLess() {
+
+ String sql = SqlRenderer.render(StatementBuilder.select(left).from(table).where(left.isLess(right)).build());
+
+ assertThat(sql).endsWith("WHERE my_table.left < my_table.right");
+ }
+
+ @Test // DATAJDBC-309
+ public void shouldRenderIsLessOrEqualTo() {
+
+ String sql = SqlRenderer
+ .render(StatementBuilder.select(left).from(table).where(left.isLessOrEqualTo(right)).build());
+
+ assertThat(sql).endsWith("WHERE my_table.left <= my_table.right");
+ }
+
+ @Test // DATAJDBC-309
+ public void shouldRenderIsGreater() {
+
+ String sql = SqlRenderer
+ .render(StatementBuilder.select(left).from(table).where(left.isGreater(right)).build());
+
+ assertThat(sql).endsWith("WHERE my_table.left > my_table.right");
+ }
+
+ @Test // DATAJDBC-309
+ public void shouldRenderIsGreaterOrEqualTo() {
+
+ String sql = SqlRenderer
+ .render(StatementBuilder.select(left).from(table).where(left.isGreaterOrEqualTo(right)).build());
+
+ assertThat(sql).endsWith("WHERE my_table.left >= my_table.right");
+ }
+
+ @Test // DATAJDBC-309
+ public void shouldRenderIn() {
+
+ String sql = SqlRenderer.render(StatementBuilder.select(left).from(table).where(left.in(right)).build());
+
+ assertThat(sql).endsWith("WHERE my_table.left IN (my_table.right)");
+ }
+
+ @Test // DATAJDBC-309
+ public void shouldRenderLike() {
+
+ String sql = SqlRenderer.render(StatementBuilder.select(left).from(table).where(left.like(right)).build());
+
+ assertThat(sql).endsWith("WHERE my_table.left LIKE my_table.right");
+ }
+
+ @Test // DATAJDBC-309
+ public void shouldRenderIsNull() {
+
+ String sql = SqlRenderer.render(StatementBuilder.select(left).from(table).where(left.isNull()).build());
+
+ assertThat(sql).endsWith("WHERE my_table.left IS NULL");
+ }
+
+ @Test // DATAJDBC-309
+ public void shouldRenderIsNotNull() {
+
+ String sql = SqlRenderer.render(StatementBuilder.select(left).from(table).where(left.isNotNull()).build());
+
+ assertThat(sql).endsWith("WHERE my_table.left IS NOT NULL");
+
+ sql = SqlRenderer.render(StatementBuilder.select(left).from(table).where(left.isNull().not()).build());
+
+ assertThat(sql).endsWith("WHERE my_table.left IS NOT NULL");
+ }
+}
diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/OrderByClauseVisitorUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/OrderByClauseVisitorUnitTests.java
new file mode 100644
index 0000000000..2c7a2272ec
--- /dev/null
+++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/OrderByClauseVisitorUnitTests.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.relational.core.sql.render;
+
+import static org.assertj.core.api.Assertions.*;
+
+import org.junit.Test;
+import org.springframework.data.relational.core.sql.Column;
+import org.springframework.data.relational.core.sql.OrderByField;
+import org.springframework.data.relational.core.sql.SQL;
+import org.springframework.data.relational.core.sql.Select;
+import org.springframework.data.relational.core.sql.Table;
+
+/**
+ * Unit tests for {@link OrderByClauseVisitor}.
+ *
+ * @author Mark Paluch
+ */
+public class OrderByClauseVisitorUnitTests {
+
+ @Test // DATAJDBC-309
+ public void shouldRenderOrderByName() {
+
+ Table employee = SQL.table("employee").as("emp");
+ Column column = employee.column("name").as("emp_name");
+
+ Select select = Select.builder().select(column).from(employee).orderBy(OrderByField.from(column).asc()).build();
+
+ OrderByClauseVisitor visitor = new OrderByClauseVisitor(new SimpleRenderContext(NamingStrategies.asIs()));
+ select.visit(visitor);
+
+ assertThat(visitor.getRenderedPart().toString()).isEqualTo("emp_name ASC");
+ }
+
+ @Test // DATAJDBC-309
+ public void shouldApplyNamingStrategy() {
+
+ Table employee = SQL.table("employee").as("emp");
+ Column column = employee.column("name").as("emp_name");
+
+ Select select = Select.builder().select(column).from(employee).orderBy(OrderByField.from(column).asc()).build();
+
+ OrderByClauseVisitor visitor = new OrderByClauseVisitor(new SimpleRenderContext(NamingStrategies.toUpper()));
+ select.visit(visitor);
+
+ assertThat(visitor.getRenderedPart().toString()).isEqualTo("EMP_NAME ASC");
+ }
+}
diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/SqlRendererUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/SqlRendererUnitTests.java
new file mode 100644
index 0000000000..42920236b5
--- /dev/null
+++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/SqlRendererUnitTests.java
@@ -0,0 +1,274 @@
+/*
+ * 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.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 SqlRenderer}.
+ *
+ * @author Mark Paluch
+ * @author Jens Schauder
+ */
+public class SqlRendererUnitTests {
+
+ @Test // DATAJDBC-309
+ public void shouldRenderSingleColumn() {
+
+ Table bar = SQL.table("bar");
+ Column foo = bar.column("foo");
+
+ Select select = Select.builder().select(foo).from(bar).build();
+
+ assertThat(SqlRenderer.render(select)).isEqualTo("SELECT bar.foo FROM bar");
+ }
+
+ @Test // DATAJDBC-309
+ public void shouldRenderAliasedColumnAndFrom() {
+
+ Table table = Table.create("bar").as("my_bar");
+
+ Select select = Select.builder().select(table.column("foo").as("my_foo")).from(table).build();
+
+ assertThat(SqlRenderer.render(select)).isEqualTo("SELECT my_bar.foo AS my_foo FROM bar AS my_bar");
+ }
+
+ @Test // DATAJDBC-309
+ public void shouldRenderMultipleColumnsFromTables() {
+
+ Table table1 = Table.create("table1");
+ Table table2 = Table.create("table2");
+
+ Select select = Select.builder().select(table1.column("col1")).select(table2.column("col2")).from(table1)
+ .from(table2).build();
+
+ assertThat(SqlRenderer.render(select)).isEqualTo("SELECT table1.col1, table2.col2 FROM table1, table2");
+ }
+
+ @Test // DATAJDBC-309
+ public void shouldRenderDistinct() {
+
+ Table table = SQL.table("bar");
+ Column foo = table.column("foo");
+ Column bar = table.column("bar");
+
+ Select select = Select.builder().distinct().select(foo, bar).from(table).build();
+
+ assertThat(SqlRenderer.render(select)).isEqualTo("SELECT DISTINCT bar.foo, bar.bar FROM bar");
+ }
+
+ @Test // DATAJDBC-309
+ public void shouldRenderCountFunction() {
+
+ Table table = SQL.table("bar");
+ Column foo = table.column("foo");
+ Column bar = table.column("bar");
+
+ Select select = Select.builder().select(Functions.count(foo), bar).from(table).build();
+
+ assertThat(SqlRenderer.render(select)).isEqualTo("SELECT COUNT(bar.foo), bar.bar FROM bar");
+ }
+
+ @Test // DATAJDBC-309
+ public void shouldRenderSimpleJoin() {
+
+ Table employee = SQL.table("employee");
+ Table department = SQL.table("department");
+
+ Select select = Select.builder().select(employee.column("id"), department.column("name")).from(employee) //
+ .join(department).on(employee.column("department_id")).equals(department.column("id")) //
+ .build();
+
+ assertThat(SqlRenderer.render(select)).isEqualTo("SELECT employee.id, department.name FROM employee "
+ + "JOIN department ON employee.department_id = department.id");
+ }
+
+ @Test // DATAJDBC-309
+ public void shouldRenderSimpleJoinWithAnd() {
+
+ Table employee = SQL.table("employee");
+ Table department = SQL.table("department");
+
+ Select select = Select.builder().select(employee.column("id"), department.column("name")).from(employee) //
+ .join(department).on(employee.column("department_id")).equals(department.column("id")) //
+ .and(employee.column("tenant")).equals(department.column("tenant")) //
+ .build();
+
+ 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");
+ }
+
+ @Test // DATAJDBC-309
+ public void shouldRenderMultipleJoinWithAnd() {
+
+ Table employee = SQL.table("employee");
+ Table department = SQL.table("department");
+ Table tenant = SQL.table("tenant").as("tenant_base");
+
+ Select select = Select.builder().select(employee.column("id"), department.column("name")).from(employee) //
+ .join(department).on(employee.column("department_id")).equals(department.column("id")) //
+ .and(employee.column("tenant")).equals(department.column("tenant")) //
+ .join(tenant).on(tenant.column("tenant_id")).equals(department.column("tenant")) //
+ .build();
+
+ 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");
+ }
+
+ @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();
+
+ assertThat(SqlRenderer.render(select))
+ .isEqualTo("SELECT emp.name AS emp_name FROM employee AS emp ORDER BY emp_name ASC");
+ }
+
+ @Test // DATAJDBC-309
+ public void shouldRenderOrderLimitOffset() {
+
+ Table table = SQL.table("foo");
+ Column bar = table.column("bar");
+
+ Select select = Select.builder().select(bar).from("foo").limitOffset(10, 20).build();
+
+ assertThat(SqlRenderer.render(select)).isEqualTo("SELECT foo.bar FROM foo LIMIT 10 OFFSET 20");
+ }
+
+ @Test // DATAJDBC-309
+ public void shouldRenderIsNull() {
+
+ Table table = SQL.table("foo");
+ Column bar = table.column("bar");
+
+ Select select = Select.builder().select(bar).from(table).where(Conditions.isNull(bar)).build();
+
+ assertThat(SqlRenderer.render(select)).isEqualTo("SELECT foo.bar FROM foo WHERE foo.bar IS NULL");
+ }
+
+ @Test // DATAJDBC-309
+ public void shouldRenderNotNull() {
+
+ Table table = SQL.table("foo");
+ Column bar = table.column("bar");
+
+ Select select = Select.builder().select(bar).from(table).where(Conditions.isNull(bar).not()).build();
+
+ assertThat(SqlRenderer.render(select)).isEqualTo("SELECT foo.bar FROM foo WHERE foo.bar IS NOT NULL");
+ }
+
+ @Test // DATAJDBC-309
+ public void shouldRenderEqualityCondition() {
+
+ Table table = SQL.table("foo");
+ Column bar = table.column("bar");
+
+ Select select = Select.builder().select(bar).from(table).where(Conditions.isEqual(bar, SQL.bindMarker(":name")))
+ .build();
+
+ assertThat(SqlRenderer.render(select)).isEqualTo("SELECT foo.bar FROM foo WHERE foo.bar = :name");
+ }
+
+ @Test // DATAJDBC-309
+ public void shouldRendersAndOrConditionWithProperParentheses() {
+
+ Table table = SQL.table("foo");
+ Column bar = table.column("bar");
+ Column baz = table.column("baz");
+
+ 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(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
+ public void shouldInWithNamedParameter() {
+
+ 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"))).build();
+
+ 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
+ public void shouldRenderInSubselect() {
+
+ Table foo = SQL.table("foo");
+ Column bar = foo.column("bar");
+
+ Table floo = SQL.table("floo");
+ Column bah = floo.column("bah");
+
+ Select subselect = Select.builder().select(bah).from(floo).build();
+
+ Select select = Select.builder().select(bar).from(foo).where(Conditions.in(bar, subselect)).build();
+
+ 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");
+ }
+
+}