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 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 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 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 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 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 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 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: + *

    + *
  1. Self
  2. + *
  3. {@link Column SELECT columns}
  4. + *
  5. {@link Table FROM tables} clause
  6. + *
  7. {@link Join JOINs}
  8. + *
  9. {@link Condition WHERE} condition
  10. + *
  11. {@link OrderByField ORDER BY fields}
  12. + *
+ * + * @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 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 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 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 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 tables); + + @Override + SelectFromAndOrderBy orderBy(Column... columns); + + @Override + SelectFromAndOrderBy orderBy(OrderByField... orderByFields); + + @Override + SelectFromAndOrderBy orderBy(Collection orderByFields); + } + + /** + * Builder exposing {@code FROM}, {@code JOIN}, {@code WHERE} and {@code LIMIT/OFFSET} methods. + */ + interface SelectFromAndJoin extends SelectFromAndOrderBy, BuildSelect, SelectJoin, SelectWhere, SelectLimitOffset { + + /** + * Declare a {@link Table} to {@code SELECT … FROM}. Multiple calls to this or other {@code from} methods keep + * adding items to the select list and do not replace previously contained items. + * + * @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 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 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 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: + *

    + *
  1. Ignores elements that do not match the filter {@link Predicate}.
  2. + *
  3. {@link #enterMatched(Visitable) enter}/{@link #leaveMatched(Visitable) leave} matched callbacks for the + * {@link Visitable segment} that matches the {@link Predicate}.
  4. + *
  5. {@link #enterNested(Visitable) enter}/{@link #leaveNested(Visitable) leave} nested callbacks for direct/nested + * children of the matched {@link Visitable} until {@link #leaveMatched(Visitable) leaving the matched} + * {@link Visitable}.
  6. + *
+ * + * @author Mark Paluch + * @see TypedSubtreeVisitor + * @since 1.1 + */ +abstract class FilteredSubtreeVisitor extends DelegatingVisitor { + + private final Predicate filter; + + private @Nullable Visitable currentSegment; + + /** + * Creates a new {@link FilteredSubtreeVisitor} given the filter {@link Predicate}. + * + * @param filter filter predicate to identify when to {@link #enterMatched(Visitable) + * enter}/{@link #leaveMatched(Visitable) leave} the {@link Visitable segment} that this visitor is + * responsible for. + */ + FilteredSubtreeVisitor(Predicate filter) { + this.filter = filter; + } + + /** + * {@link Visitor#enter(Visitable) Enter} callback for a {@link Visitable} that this {@link Visitor} is responsible + * for. The default implementation retains delegation by default. + * + * @param segment the segment, must not be {@literal null}. + * @return delegation options. Can be either {@link Delegation#retain()} or + * {@link Delegation#delegateTo(DelegatingVisitor)}. + * @see Delegation#retain() + */ + Delegation enterMatched(Visitable segment) { + return Delegation.retain(); + } + + /** + * {@link Visitor#enter(Visitable) Enter} callback for a nested {@link Visitable}. The default implementation retains + * delegation by default. + * + * @param segment the segment, must not be {@literal null}. + * @return delegation options. Can be either {@link Delegation#retain()} or + * {@link Delegation#delegateTo(DelegatingVisitor)}. + * @see Delegation#retain() + */ + Delegation enterNested(Visitable segment) { + return Delegation.retain(); + } + + /** + * {@link Visitor#leave(Visitable) Leave} callback for the matched {@link Visitable}. The default implementation steps + * back from delegation by default. + * + * @param segment the segment, must not be {@literal null}. + * @return delegation options. Can be either {@link Delegation#retain()} or {@link Delegation#leave()}. + * @see Delegation#leave() + */ + Delegation leaveMatched(Visitable segment) { + return Delegation.leave(); + } + + /** + * {@link Visitor#leave(Visitable) Leave} callback for a nested {@link Visitable}. The default implementation retains + * delegation by default. + * + * @param segment the segment, must not be {@literal null}. + * @return delegation options. Can be either {@link Delegation#retain()} or {@link Delegation#leave()}. + * @see Delegation#retain() + */ + Delegation leaveNested(Visitable segment) { + return Delegation.retain(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.DelegatingVisitor#doEnter(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + public final Delegation doEnter(Visitable segment) { + + if (currentSegment == null) { + + if (filter.test(segment)) { + currentSegment = segment; + return enterMatched(segment); + } + } else { + return enterNested(segment); + } + + return Delegation.retain(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.DelegatingVisitor#doLeave(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + public final Delegation doLeave(Visitable segment) { + + if (currentSegment == null) { + return Delegation.leave(); + } else if (segment == currentSegment) { + currentSegment = null; + return leaveMatched(segment); + } else { + return leaveNested(segment); + } + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/FromClauseVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/FromClauseVisitor.java new file mode 100644 index 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: + *

    + *
  1. Ignores elements that do not match the filter {@link Predicate}.
  2. + *
  3. {@link #enterMatched(Visitable) enter}/{@link #leaveMatched(Visitable) leave} matched callbacks for the + * {@link Visitable segment} that matches the {@link Predicate}.
  4. + *
  5. {@link #enterNested(Visitable) enter}/{@link #leaveNested(Visitable) leave} nested callbacks for direct/nested + * children of the matched {@link Visitable} until {@link #leaveMatched(Visitable) leaving the matched} + * {@link Visitable}.
  6. + *
+ * + * @author Mark Paluch + * @since 1.1 + * @see FilteredSubtreeVisitor + */ +abstract class TypedSubtreeVisitor extends DelegatingVisitor { + + private final ResolvableType type; + private @Nullable Visitable currentSegment; + + /** + * Creates a new {@link TypedSubtreeVisitor}. + */ + TypedSubtreeVisitor() { + this.type = ResolvableType.forClass(getClass()).as(TypedSubtreeVisitor.class).getGeneric(0); + } + + /** + * {@link Visitor#enter(Visitable) Enter} callback for a {@link Visitable} that this {@link Visitor} is responsible + * for. The default implementation retains delegation by default. + * + * @param segment the segment, must not be {@literal null}. + * @return delegation options. Can be either {@link Delegation#retain()} or + * {@link Delegation#delegateTo(DelegatingVisitor)}. + * @see Delegation#retain() + */ + Delegation enterMatched(T segment) { + return Delegation.retain(); + } + + /** + * {@link Visitor#enter(Visitable) Enter} callback for a nested {@link Visitable}. The default implementation retains + * delegation by default. + * + * @param segment the segment, must not be {@literal null}. + * @return delegation options. Can be either {@link Delegation#retain()} or + * {@link Delegation#delegateTo(DelegatingVisitor)}. + * @see Delegation#retain() + */ + Delegation enterNested(Visitable segment) { + return Delegation.retain(); + } + + /** + * {@link Visitor#leave(Visitable) Leave} callback for the matched {@link Visitable}. The default implementation steps + * back from delegation by default. + * + * @param segment the segment, must not be {@literal null}. + * @return delegation options. Can be either {@link Delegation#retain()} or {@link Delegation#leave()}. + * @see Delegation#leave() + */ + Delegation leaveMatched(T segment) { + return Delegation.leave(); + } + + /** + * {@link Visitor#leave(Visitable) Leave} callback for a nested {@link Visitable}. The default implementation retains + * delegation by default. + * + * @param segment the segment, must not be {@literal null}. + * @return delegation options. Can be either {@link Delegation#retain()} or {@link Delegation#leave()}. + * @see Delegation#retain() + */ + Delegation leaveNested(Visitable segment) { + return Delegation.retain(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.DelegatingVisitor#doEnter(org.springframework.data.relational.core.sql.Visitable) + */ + @SuppressWarnings("unchecked") + @Override + public final Delegation doEnter(Visitable segment) { + + if (currentSegment == null) { + + if (this.type.isInstance(segment)) { + + currentSegment = segment; + return enterMatched((T) segment); + } + } else { + return enterNested(segment); + } + + return Delegation.retain(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.DelegatingVisitor#doLeave(org.springframework.data.relational.core.sql.Visitable) + */ + @SuppressWarnings("unchecked") + @Override + public final Delegation doLeave(Visitable segment) { + + if (currentSegment == null) { + return Delegation.leave(); + } else if (segment == currentSegment) { + currentSegment = null; + return leaveMatched((T) segment); + } else { + return leaveNested(segment); + } + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/WhereClauseVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/WhereClauseVisitor.java new file mode 100644 index 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"); + } + +}